敏捷开发简介

 开发  敏捷开发简介已关闭评论
7月 312021
 

重温下敏捷开发概念!

敏捷开发(agile development)是非常流行的软件开发方法。据统计,2018年90%的软件开发采用敏捷开发。

但是,到底什么是敏捷开发,能说清的人却不多。本文尝试用简洁易懂的语言,解释敏捷开发。

 

一、迭代开发

敏捷开发的核心是迭代开发(iterative development)。敏捷一定是采用迭代开发的方式。

那么什么是”迭代开发”呢?迭代的英文是 iterative,直译为”重复”,迭代开发其实就是”重复开发”。

对于大型软件项目,传统的开发方式是采用一个大周期(比如一年)进行开发,整个过程就是一次”大开发”;迭代开发的方式则不一样,它将开发过程拆分成多个小周期,即一次”大开发”变成多次”小开发”,每次小开发都是同样的流程,所以看上去就好像重复在做同样的步骤。

举例来说,SpaceX 公司想造一个大推力火箭,将人类送到火星。但是,它不是一开始就造大火箭,而是先造一个最简陋的小火箭 Falcon 1。结果,第一次发射就爆炸了,直到第四次发射,才成功进入轨道。然后,开发了中型火箭 Falcon 9,九年中发射了70次。最后,才开发 Falcon 重型火箭。如果 SpaceX 不采用迭代开发,它可能直到现在还无法上天。

迭代开发将一个大任务,分解成多次连续的开发,本质就是逐步改进。开发者先快速发布一个有效但不完美的最简版本,然后不断迭代。每一次迭代都包含规划、设计、编码、测试、评估五个步骤,不断改进产品,添加新功能。通过频繁的发布,以及跟踪对前一次迭代的反馈,最终接近较完善的产品形态。

二、增量开发

迭代开发只是要求将开发分成多个迭代,并没有回答一个重要的问题:怎么划分迭代,哪个任务在这个迭代,哪个任务在下个迭代?这时,一般采用”增量开发”(incremental development)划分迭代。

所谓”增量开发”,指的是软件的每个版本,都会新增一个用户可以感知的完整功能。也就是说,按照新增功能来划分迭代。

举例来说,房产公司开发一个10栋楼的小区。如果采用增量开发的模式,该公司第一个迭代就是交付一号楼,第二个迭代交付二号楼……每个迭代都是完成一栋完整的楼。而不是第一个迭代挖好10栋楼的地基,第二个迭代建好每栋楼的骨架,第三个迭代架设屋顶……

增量开发加上迭代开发,才算真正的敏捷开发。

三、敏捷开发的好处

3.1 早期交付

敏捷开发的第一个好处,就是早期交付,从而大大降低成本。

还是以上一节的房产公司为例,如果按照传统的”瀑布开发模式”,先挖10栋楼的地基、再盖骨架、然后架设屋顶,每个阶段都等到前一个阶段完成后开始,可能需要两年才能一次性交付10栋楼。也就是说,如果不考虑预售,该项目必须等到两年后才能回款。

敏捷开发是六个月后交付一号楼,后面每两个月交付一栋楼。因此,半年就能回款10%,后面每个月都会有现金流,资金压力就大大减轻了。

3.2 降低风险

敏捷开发的第二个好处是,及时了解市场需求,降低产品不适用的风险。

请想一想,哪一种情况损失比较小:10栋楼都造好以后,才发现卖不出去,还是造好第一栋楼,就发现卖不出去,从而改进或停建后面9栋楼?

对于软件项目来说,先有一个原型产品,了解市场的接受程度,往往是项目成功的关键。有一本书叫做《梦断代码》,副标题就是”20+个程序员,三年时间,4732个bug,100+万美元,最后失败的故事”,这就是没有采用敏捷开发的结果。相反的,Instagram 最初是一个地理位置打卡 App,后来发现用户不怎么在乎地理位置,更喜欢上传照片,就改做照片上传软件,结果成了独角兽。

由于敏捷开发可以不断试错,找出对业务最重要的功能,然后通过迭代,调整软件方向。相比传统方式,大大增加了产品成功的可能性。如果市场需求不确定,或者你对该领域不熟悉,那么敏捷开发几乎是唯一可行的应对方式。

四、如何进行每一次迭代

虽然敏捷开发将软件开发分成多个迭代,但是也要求,每次迭代都是一个完整的软件开发周期,必须按照软件工程的方法论,进行正规的流程管理。

具体来说,每次迭代都必须依次完成以下五个步骤。

  1. 需求分析(requirements analysis)
  2. 设计(design)
  3. 编码(coding)
  4. 测试(testing)
  5. 部署和评估(deployment / evaluation)

每个迭代大约持续2~6周。

五、敏捷开发的价值观

《敏捷软件开发宣言》里面提到四个价值观。

  • 程序员的主观能动性,以及程序员之间的互动,优于既定流程和工具。
  • 软件能够运行,优于详尽的文档。
  • 跟客户的密切协作,优于合同和谈判。
  • 能够响应变化,优于遵循计划。

六、十二条原则

该宣言还提出十二条敏捷开发的原则。

  1. 通过早期和持续交付有价值的软件,实现客户满意度。
  2. 欢迎不断变化的需求,即使是在项目开发的后期。要善于利用需求变更,帮助客户获得竞争优势。
  3. 不断交付可用的软件,周期通常是几周,越短越好。
  4. 项目过程中,业务人员与开发人员必须在一起工作。
  5. 项目必须围绕那些有内在动力的个人而建立,他们应该受到信任。
  6. 面对面交谈是最好的沟通方式。
  7. 可用性是衡量进度的主要指标。
  8. 提倡可持续的开发,保持稳定的进展速度。
  9. 不断关注技术是否优秀,设计是否良好。
  10. 简单性至关重要,尽最大可能减少不必要的工作。
  11. 最好的架构、要求和设计,来自团队内部自发的认识。
  12. 团队要定期反思如何更有效,并相应地进行调整。

 

转自: https://www.ruanyifeng.com/blog/2019/03/agile-development.html

文章一:使用ssh实现内网穿透入门及应用(附autossh方法)(转)

 ssh  文章一:使用ssh实现内网穿透入门及应用(附autossh方法)(转)已关闭评论
2月 142021
 

转发一篇关于使用ssh实现内网穿透的文章,写的通俗易懂,原链接见文章末尾。

 

“世界上最遥远的距离就是你在外网请求,我在内网测试。”

这句话的内容,对于开发人员来说,特别容易理解。很多情况下,我们的开发及测试环境在单位的内网下,只能通过位于内网的机器来连接操作,位于外网的机器是连不到内网环境的。比如说,如果我们周末在家工作,而家里的机器又不在单位内网环境下,那该如何连接内网的环境呢?难不成我们还要大周末的跑到单位去加班吗?

答案是否定的。这是种普遍又迫切的需求,叫“内网穿透”。这里我们使用SSH端口转发的技术,解决这种问题。

SSH本地端口转发

假设,host1和host2位于内网,host3位于外网,host3可以连接host1和host2,但host1不能连接host3和host2。我们要做的是,通过位于外网的host3,让host1来连接host2。

具体步骤

首先,在host1上进行如下操作:


ssh -L 2222:host2:22 user3@host3

其中,-L参数指定了“本地主机端口:目标主机:目标主机端口”。这表示,让host1作为sshd服务端,监听它自己的2222端口,然后将所有数据经由host3,转发到host2的22端口。

这种情况下,host1不能连接host3,但由于host1的配置,使得从host1到host3建立了一条“SSH隧道”

然后,在host1上进行如下操作:


ssh -p 2222 user2@localhost

其中,-p参数指定了ssh连接的端口,默认为22,这里指定了2222端口。这表示,让host1作为ssh客户端,连接它自己的2222端口,相当于连接host2的22端口。

一般情况下,host2与host3为一台主机,换句话说,我们只要实现连接host3,那么再连接host2也不成问题。

这时,命令分别转换为:


ssh -L 2222:localhost:22 user3@host3
ssh -p 2222 user3@localhost

SSH本地端口转发的本质

本质上,SSH本地端口转发,主要是实现以下两个方面:


1. 将本地主机的端口,转发到目标主机的端口
2. 在本地主机上,连接本地主机的端口,相当于连接目标主机的端口

SSH远程端口转发

假设,host1和host2位于内网,host3位于外网,host1可以连接host3和host2,但host3不能连接host1和host2。我们要做的是,通过位于内网的host1,让host3来连接host2,也就是实现所谓的“内网穿透”

具体步骤

首先,在host1上进行如下操作:


ssh -R 2222:host2:22 user3@host3

其中,-R参数指定了“远程主机端口:目标主机:目标主机端口”。这表示,让host3作为sshd服务端,监听它自己的2222端口,然后将所有数据经由host1,转发到host2的22端口。

这种情况下,host3不能连接host1,但由于host1的配置,使得从host1到host3建立了一条“SSH反向隧道”

然后,在host3上进行如下操作:


ssh -p 2222 user2@localhost

其中,-p参数指定了ssh连接的端口,默认为22,这里指定了2222端口。这表示,让host3作为ssh客户端,连接它自己的2222端口,相当于连接host2的22端口。

一般情况下,host2与host1为一台主机,换句话说,我们只要实现连接host1,那么再连接host2也不成问题。

这时,命令分别转换为:


ssh -R 2222:localhost:22 user3@host3
ssh -p 2222 user1@localhost

SSH远程端口转发的本质

本质上,SSH远程端口转发,主要是实现以下两个方面:


1. 将远程主机的端口,转发到目标主机的端口
2. 在远程主机上,连接远程主机的端口,相当于连接目标主机的端口

SSH实现内网穿透

内网穿透,简单来说就是,利用位于外网的主机,来连接位于内网的主机,这符合SSH远程端口转发的情况。但由于实际情况中,SSH连接经常由于这样那样的问题,导致连接断开,因此我们不得不重新去在内网主机上建立与外网主机的连接,也就是维持这条“SSH反向隧道”,autossh能实现连接断开之后自动重连功能。

自动重连

autossh与ssh用法类似,只要将ssh命令替换成autossh命令即可,如下所示:


autossh -M 2345 -NTR 2222:localhost:22 user3@host3

其中,-M参数指定了autossh监听的端口,注意这里与其转发的端口要区分开。

另外,-N表示禁止执行远程命令,-T表示禁止分配伪终端,这两个参数结合起来表示SSH连接不允许用户交互执行远程操作,只能用来传数据,从而保证了远程主机的安全。

自动登录

每次重新建立连接,autossh都需要确认一下登录身份。要保证自动重连,前提就是要实现自动登录

一种常见的做法,就是使用公钥登录进行免密登录,将host1上的公钥传送至host3上。这样,每次在进行SSH登录的时候,host3都会向host1发送一段随机字符串,host1用自己的私钥加密后将数据返回,然后host3用事先存好的公钥对返回的数据进行解密,如果成功,则证明host1的身份可信,允许直接登录,不再要求密码。

还有一种做法,就是利用sshpass将密码明文传输给autossh,如下所示:


sshpass -p "xxxxxx" autossh -M 2345 -NTR 2222:localhost:22 user3@host3

其中,-p参数指定了登录的密码。除了命令行输入密码的形式,sshpass还包含-f、-e等参数,分别支持文件输入密码及系统环境变量输入密码等形式,如图所示。

其他端口

实现内网穿透,除了转发22端口外,我们也可以转发其他应用的端口,如web服务的80端口、mysql的3306端口等,这里就不一一细说了。

 

来自:http://www.lining0806.com/ssh%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91%E5%AE%9E%E7%8E%B0%E5%86%85%E7%BD%91%E7%A9%BF%E9%80%8F/

ssh解析、原理、入门、运用(转)

 linux, ssh  ssh解析、原理、入门、运用(转)已关闭评论
1月 252021
 

转自网上的一篇文章, 描述ssh通俗易懂,原链接见文章末尾。

 

SSH是每一台Linux电脑的标准配置。

随着Linux设备从电脑逐渐扩展到手机、外设和家用电器,SSH的使用范围也越来越广。不仅程序员离不开它,很多普通用户也每天使用。

SSH具备多种功能,可以用于很多场合。有些事情,没有它就是办不成。本文是我的学习笔记,总结和解释了SSH的常见用法,希望对大家有用。

虽然本文内容只涉及初级应用,较为简单,但是需要读者具备最基本的”Shell知识”和了解”公钥加密”的概念。

 

一、什么是SSH?

简单说,SSH是一种网络协议,用于计算机之间的加密登录。

如果一个用户从本地计算机,使用SSH协议登录另一台远程计算机,我们就可以认为,这种登录是安全的,即使被中途截获,密码也不会泄露。

最早的时候,互联网通信都是明文通信,一旦被截获,内容就暴露无疑。1995年,芬兰学者Tatu Ylonen设计了SSH协议,将登录信息全部加密,成为互联网安全的一个基本解决方案,迅速在全世界获得推广,目前已经成为Linux系统的标准配置。

需要指出的是,SSH只是一种协议,存在多种实现,既有商业实现,也有开源实现。本文针对的实现是OpenSSH,它是自由软件,应用非常广泛。

此外,本文只讨论SSH在Linux Shell中的用法。如果要在Windows系统中使用SSH,会用到另一种软件PuTTY,这需要另文介绍。

二、最基本的用法

SSH主要用于远程登录。假定你要以用户名user,登录远程主机host,只要一条简单命令就可以了。

  $ ssh user@host

如果本地用户名与远程用户名一致,登录时可以省略用户名。

  $ ssh host

SSH的默认端口是22,也就是说,你的登录请求会送进远程主机的22端口。使用p参数,可以修改这个端口。

  $ ssh -p 2222 user@host

上面这条命令表示,ssh直接连接远程主机的2222端口。

三、中间人攻击

SSH之所以能够保证安全,原因在于它采用了公钥加密。

整个过程是这样的:(1)远程主机收到用户的登录请求,把自己的公钥发给用户。(2)用户使用这个公钥,将登录密码加密后,发送回来。(3)远程主机用自己的私钥,解密登录密码,如果密码正确,就同意用户登录。

这个过程本身是安全的,但是实施的时候存在一个风险:如果有人截获了登录请求,然后冒充远程主机,将伪造的公钥发给用户,那么用户很难辨别真伪。因为不像https协议,SSH协议的公钥是没有证书中心(CA)公证的,也就是说,都是自己签发的。

可以设想,如果攻击者插在用户与远程主机之间(比如在公共的wifi区域),用伪造的公钥,获取用户的登录密码。再用这个密码登录远程主机,那么SSH的安全机制就荡然无存了。这种风险就是著名的“中间人攻击”(Man-in-the-middle attack)。

SSH协议是如何应对的呢?

四、口令登录

如果你是第一次登录对方主机,系统会出现下面的提示:

  $ ssh user@host

The authenticity of host ‘host (12.18.429.21)’ can’t be established.

RSA key fingerprint is 98:2e:d7:e0:de:9f:ac:67:28:c2:42:2d:37:16:58:4d.

Are you sure you want to continue connecting (yes/no)?

这段话的意思是,无法确认host主机的真实性,只知道它的公钥指纹,问你还想继续连接吗?

所谓”公钥指纹”,是指公钥长度较长(这里采用RSA算法,长达1024位),很难比对,所以对其进行MD5计算,将它变成一个128位的指纹。上例中是98:2e:d7:e0:de:9f:ac:67:28:c2:42:2d:37:16:58:4d,再进行比较,就容易多了。

很自然的一个问题就是,用户怎么知道远程主机的公钥指纹应该是多少?回答是没有好办法,远程主机必须在自己的网站上贴出公钥指纹,以便用户自行核对。

假定经过风险衡量以后,用户决定接受这个远程主机的公钥。

  Are you sure you want to continue connecting (yes/no)? yes

系统会出现一句提示,表示host主机已经得到认可。

  Warning: Permanently added ‘host,12.18.429.21’ (RSA) to the list of known hosts.

然后,会要求输入密码。

  Password: (enter password)

如果密码正确,就可以登录了。

当远程主机的公钥被接受以后,它就会被保存在文件$HOME/.ssh/known_hosts之中。下次再连接这台主机,系统就会认出它的公钥已经保存在本地了,从而跳过警告部分,直接提示输入密码。

每个SSH用户都有自己的known_hosts文件,此外系统也有一个这样的文件,通常是/etc/ssh/ssh_known_hosts,保存一些对所有用户都可信赖的远程主机的公钥。

五、公钥登录

使用密码登录,每次都必须输入密码,非常麻烦。好在SSH还提供了公钥登录,可以省去输入密码的步骤。

所谓”公钥登录”,原理很简单,就是用户将自己的公钥储存在远程主机上。登录的时候,远程主机会向用户发送一段随机字符串,用户用自己的私钥加密后,再发回来。远程主机用事先储存的公钥进行解密,如果成功,就证明用户是可信的,直接允许登录shell,不再要求密码。

这种方法要求用户必须提供自己的公钥。如果没有现成的,可以直接用ssh-keygen生成一个:

  $ ssh-keygen

运行上面的命令以后,系统会出现一系列提示,可以一路回车。其中有一个问题是,要不要对私钥设置口令(passphrase),如果担心私钥的安全,这里可以设置一个。

运行结束以后,在$HOME/.ssh/目录下,会新生成两个文件:id_rsa.pub和id_rsa。前者是你的公钥,后者是你的私钥。

这时再输入下面的命令,将公钥传送到远程主机host上面:

  $ ssh-copy-id user@host

好了,从此你再登录,就不需要输入密码了。

如果还是不行,就打开远程主机的/etc/ssh/sshd_config这个文件,检查下面几行前面”#”注释是否取掉。

  RSAAuthentication yes
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys

然后,重启远程主机的ssh服务。

  // ubuntu系统
service ssh restart

// debian系统
/etc/init.d/ssh restart

六、authorized_keys文件

远程主机将用户的公钥,保存在登录后的用户主目录的$HOME/.ssh/authorized_keys文件中。公钥就是一段字符串,只要把它追加在authorized_keys文件的末尾就行了。

这里不使用上面的ssh-copy-id命令,改用下面的命令,解释公钥的保存过程:

  $ ssh user@host ‘mkdir -p .ssh && cat >> .ssh/authorized_keys’ < ~/.ssh/id_rsa.pub

这条命令由多个语句组成,依次分解开来看:(1)”$ ssh user@host”,表示登录远程主机;(2)单引号中的mkdir .ssh && cat >> .ssh/authorized_keys,表示登录后在远程shell上执行的命令:(3)”$ mkdir -p .ssh”的作用是,如果用户主目录中的.ssh目录不存在,就创建一个;(4)’cat >> .ssh/authorized_keys’ < ~/.ssh/id_rsa.pub的作用是,将本地的公钥文件~/.ssh/id_rsa.pub,重定向追加到远程文件authorized_keys的末尾。

写入authorized_keys文件后,公钥登录的设置就完成了。

 

转自阮一峰的文章:http://www.ruanyifeng.com/blog/2011/12/ssh_remote_login.html

卷积神经网络 — CNN(Convolutional Neural Networks)的入门

 人工智能  卷积神经网络 — CNN(Convolutional Neural Networks)的入门已关闭评论
12月 032020
 

转发一篇介绍《卷积神经网络 — CNN(Convolutional Neural Networks)》的入门文章,觉得写的挺好的,来自: medium.com 

卷积神经网络 — CNN 最擅长的就是图片的处理。它受到人类视觉神经系统的启发。

CNN 有2大特点:

能够有效的将大数据量的图片降维成小数据量

能够有效的保留图片特征,符合图片处理的原则

目前 CNN 已经得到了广泛的应用,比如:人脸识别、自动驾驶、美图秀秀、安防等很多领域。

CNN 解决了什么问题?

在 CNN 出现之前,图像对于人工智能来说是一个难题,有2个原因:

  1. 图像需要处理的数据量太大,导致成本很高,效率很低

下面就详细说明一下这2个问题:

需要处理的数据量太大

图像是由像素构成的,每个像素又是由颜色构成的。

Image for post

现在随随便便一张图片都是 1000×1000 像素以上的, 每个像素都有RGB 3个参数来表示颜色信息。

假如我们处理一张 1000×1000 像素的图片,我们就需要处理3百万个参数!

1000×1000×3=3,000,000

这么大量的数据处理起来是非常消耗资源的,而且这只是一张不算太大的图片!

卷积神经网络 — CNN 解决的第一个问题就是「将复杂问题简化」,把大量参数降维成少量参数,再做处理。

更重要的是:我们在大部分场景下,降维并不会影响结果。比如1000像素的图片缩小成200像素,并不影响肉眼认出来图片中是一只猫还是一只狗,机器也是如此。

保留图像特征

图片数字化的传统方式我们简化一下,就类似下图的过程:

Image for post

假如有圆形是1,没有圆形是0,那么圆形的位置不同就会产生完全不同的数据表达。但是从视觉的角度来看,图像的内容(本质)并没有发生变化,只是位置发生了变化

所以当我们移动图像中的物体,用传统的方式的得出来的参数会差异很大!这是不符合图像处理的要求的。

而 CNN 解决了这个问题,他用类似视觉的方式保留了图像的特征,当图像做翻转,旋转或者变换位置时,它也能有效的识别出来是类似的图像。

那么卷积神经网络是如何实现的呢?在我们了解 CNN 原理之前,先来看看人类的视觉原理是什么?

人类的视觉原理

深度学习的许多研究成果,离不开对大脑认知原理的研究,尤其是视觉原理的研究。

1981 年的诺贝尔医学奖,颁发给了 David Hubel(出生于加拿大的美国神经生物学家) 和TorstenWiesel,以及 Roger Sperry。前两位的主要贡献,是“发现了视觉系统的信息处理”,可视皮层是分级的。

人类的视觉原理如下:从原始信号摄入开始(瞳孔摄入像素 Pixels),接着做初步处理(大脑皮层某些细胞发现边缘和方向),然后抽象(大脑判定,眼前的物体的形状,是圆形的),然后进一步抽象(大脑进一步判定该物体是只气球)。下面是人脑进行人脸识别的一个示例:

Image for post

对于不同的物体,人类视觉也是通过这样逐层分级,来进行认知的:

Image for post

我们可以看到,在最底层特征基本上是类似的,就是各种边缘,越往上,越能提取出此类物体的一些特征(轮子、眼睛、躯干等),到最上层,不同的高级特征最终组合成相应的图像,从而能够让人类准确的区分不同的物体。

那么我们可以很自然的想到:可以不可以模仿人类大脑的这个特点,构造多层的神经网络,较低层的识别初级的图像特征,若干底层特征组成更上一层特征,最终通过多个层级的组合,最终在顶层做出分类呢?

答案是肯定的,这也是许多深度学习算法(包括CNN)的灵感来源。

卷积神经网络-CNN 的基本原理

典型的 CNN 由3个部分构成:

  1. 卷积层

如果简单来描述的话:

卷积层负责提取图像中的局部特征;池化层用来大幅降低参数量级(降维);全连接层类似传统神经网络的部分,用来输出想要的结果。

Image for post

下面的原理解释为了通俗易懂,忽略了很多技术细节,如果大家对详细的原理感兴趣,可以看这个视频《卷积神经网络基础》。

卷积 — — 提取特征

卷积层的运算过程如下图,用一个卷积核扫完整张图片:

Image for post

这个过程我们可以理解为我们使用一个过滤器(卷积核)来过滤图像的各个小区域,从而得到这些小区域的特征值。

在具体应用中,往往有多个卷积核,可以认为,每个卷积核代表了一种图像模式,如果某个图像块与此卷积核卷积出的值大,则认为此图像块十分接近于此卷积核。如果我们设计了6个卷积核,可以理解:我们认为这个图像上有6种底层纹理模式,也就是我们用6中基础模式就能描绘出一副图像。以下就是25种不同的卷积核的示例:

Image for post

总结:卷积层的通过卷积核的过滤提取出图片中局部的特征,跟上面提到的人类视觉的特征提取类似。

池化层(下采样) — — 数据降维,避免过拟合

池化层简单说就是下采样,他可以大大降低数据的维度。其过程如下:

Image for post

上图中,我们可以看到,原始图片是20×20的,我们对其进行下采样,采样窗口为10×10,最终将其下采样成为一个2×2大小的特征图。

之所以这么做的原因,是因为即使做完了卷积,图像仍然很大(因为卷积核比较小),所以为了降低数据维度,就进行下采样。

总结:池化层相比卷积层可以更有效的降低数据维度,这么做不但可以大大减少运算量,还可以有效的避免过拟合。

全连接层 — — 输出结果

这个部分就是最后一步了,经过卷积层和池化层处理过的数据输入到全连接层,得到最终想要的结果。

经过卷积层和池化层降维过的数据,全连接层才能”跑得动”,不然数据量太大,计算成本高,效率低下。

Image for post

典型的 CNN 并非只是上面提到的3层结构,而是多层结构,例如 LeNet-5 的结构就如下图所示:

卷积层 — 池化层- 卷积层 — 池化层 — 卷积层 — 全连接层

Image for post

在了解了 CNN 的基本原理后,我们重点说一下 CNN 的实际应用有哪些。

CNN 有哪些实际应用?

卷积神经网络 — CNN 很擅长处理图像。而视频是图像的叠加,所以同样擅长处理视频内容。下面给大家列一些比较成熟的应用:

图像分类、检索

图像分类是比较基础的应用,他可以节省大量的人工成本,将图像进行有效的分类。对于一些特定领域的图片,分类的准确率可以达到 95%+,已经算是一个可用性很高的应用了。

典型场景:图像搜索…

Image for post

目标定位检测

可以在图像中定位目标,并确定目标的位置及大小。

典型场景:自动驾驶、安防、医疗…

Image for post

目标分割

简单理解就是一个像素级的分类。

他可以对前景和背景进行像素级的区分、再高级一点还可以识别出目标并且对目标进行分类。

典型场景:美图秀秀、视频后期加工、图像生成…

识别

人脸识别已经是一个非常普及的应用了,在很多领域都有广泛的应用。

典型场景:安防、金融、生活…

Image for post

骨骼识别

骨骼识别是可以识别身体的关键骨骼,以及追踪骨骼的动作。

典型场景:安防、电影、图像视频生成、游戏…

Image for post

总结

今天我们介绍了 CNN 的价值、基本原理和应用场景,简单总结如下:

CNN 的价值:

  1. 能够将大数据量的图片有效的降维成小数据量(并不影响结果)

CNN 的基本原理:

  1. 卷积层 — 主要作用是保留图片的特征

CNN 的实际应用:

  1. 图片分类、检索

PendingIntent的基本理解和使用

 android  PendingIntent的基本理解和使用已关闭评论
7月 032020
 

简书上看到的一篇PendingIntent的文章,收藏起来,链接:https://www.jianshu.com/p/a37f0ce2da2e

 

PendingIntent可以看作是对Intent的一个封装,但它不是立刻执行某个行为,而是满足某些条件或触发某些事件后才执行指定的行为(启动特定Service,Activity,BrcastReceive)。

我们可以把Pending Intent交给其他程序,其他程序按照PendingIntent进行操作。

Alarm定时器与Notification通知中都使用了PendingIntent

1.获得PendingIntent类内部静态方法获得PendingIntent实例:

//获得一个用于启动特定Activity的PendingIntent

public static PendingIntent getActivity(Context context, int requestCode,Intent intent, int flags)

//获得一个用于启动特定Service的PendingIntent

public static PendingIntent getService(Context context, int requestCode,Intent intent, int flags)

//获得一个用于发送特定Broadcast的PendingIntent

public static PendingIntent getBroadcast(Context context, int requestCode,Intent intent, int flags)

参数说明:

context:上下文对象。

requstCode:请求码,发件人的私人请求代码(当前未使用)。

intent:请求意图。用于要指明要启动的类以及数据的传递;

flags:这是一个关键的标志位:

主要常量

FLAG_CANCEL_CURRENT:如果当前系统中已经存在一个相同的PendingIntent对象,那么就将先将已有的PendingIntent取消,然后重新生成一个PendingIntent对象。

FLAG_NO_CREATE:如果当前系统中不存在相同的PendingIntent对象,系统将不会创建该PendingIntent对象而是直接返回null。

FLAG_ONE_SHOT:该PendingIntent只作用一次。在该PendingIntent对象通过send()方法触发过后,PendingIntent将自动调用cancel()进行销毁,那么如果你再调用send()方法的话,系统将会返回一个SendIntentException。

FLAG_UPDATE_CURRENT:如果系统中有一个和你描述的PendingIntent对等的PendingInent,那么系统将使用该PendingIntent对象,但是会使用新的Intent来更新之前PendingIntent中的Intent对象数据,例如更新Intent中的Extras。

注意:两个PendingIntent对等是指它们的operation一样, 且其它们的Intent的action, data, categories, components和flags都一样。但是它们的Intent的Extra可以不一样。

 

入门OkHttp使用

 okhttp  入门OkHttp使用已关闭评论
10月 222018
 

OkHttp入门资料,推荐!来自:https://blog.csdn.net/mynameishuangshuai/article/details/51303446

       OkHttp官网地址:http://square.github.io/okhttp/ 
       OkHttp GitHub地址:https://github.com/square/okhttp 
官网的自我介绍: 
       HTTP is the way modern applications network. It’s how we exchange data & media. Doing HTTP efficiently makes your stuff load faster and saves bandwidth. 
OkHttp is an HTTP client that’s efficient by default:

•   HTTP/2 support allows all requests to the same host to share a socket.
•   Connection pooling reduces request latency (if HTTP/2 isn’t available).
•   Transparent GZIP shrinks download sizes.
•   Response caching avoids the network completely for repeat requests.

       OkHttp perseveres when the network is troublesome: it will silently recover from common connection problems. If your service has multiple IP addresses OkHttp will attempt alternate addresses if the first connect fails. This is necessary for IPv4+IPv6 and for services hosted in redundant data centers. OkHttp initiates new connections with modern TLS features (SNI, ALPN), and falls back to TLS 1.0 if the handshake fails. 
       Using OkHttp is easy. Its request/response API is designed with fluent builders and immutability. It supports both synchronous blocking calls and async calls with callbacks. 
       OkHttp supports Android 2.3 and above. For Java, the minimum requirement is 1.7. 
       概括起来说OkHttp是一款优秀的HTTP框架,它支持get请求和post请求,支持基于Http的文件上传和下载,支持加载图片,支持下载文件透明的GZIP压缩,支持响应缓存避免重复的网络请求,支持使用连接池来降低响应延迟问题。

配置方法
(一)导入Jar包 
点击下面链接下载最新v3.2.0 JAR 
http://repo1.maven.org/maven2/com/squareup/okhttp3/okhttp/3.2.0/okhttp-3.2.0.jar 
(二)通过构建方式导入 
MAVEN

<dependency>
  <groupId>com.squareup.okhttp3</groupId>
  <artifactId>okhttp</artifactId>
  <version>3.2.0</version>
</dependency>

GRADLE

compile ‘com.squareup.okhttp3:okhttp:3.2.0’
基本要求
Requests(请求)
       每一个HTTP请求中都应该包含一个URL,一个GET或POST方法以及Header或其他参数,当然还可以含特定内容类型的数据流。

Responses(响应)
       响应则包含一个回复代码(200代表成功,404代表未找到),Header和定制可选的body。

基本使用
       在日常开发中最常用到的网络请求就是GET和POST两种请求方式。

HTTP GET
OkHttpClient client = new OkHttpClient();
String run(String url) throws IOException {
    Request request = new Request.Builder().url(url).build();
    Response response = client.newCall(request).execute();
    if (response.isSuccessful()) {
        return response.body().string();
    } else {
        throw new IOException(“Unexpected code ” + response);
    }
}
Request是OkHttp中访问的请求,Builder是辅助类,Response即OkHttp中的响应。 
Response类:

public boolean isSuccessful()
Returns true if the code is in [200..300), which means the request was successfully received, understood, and accepted.
response.body()返回ResponseBody类
可以方便的获取string

public final String string() throws IOException
Returns the response as a string decoded with the charset of the Content-Type header. If that header is either absent or lacks a charset, this will attempt to decode the response body as UTF-8.
Throws:
IOException
当然也能获取到流的形式: 
public final InputStream byteStream()

HTTP POST
POST提交Json数据

public static final MediaType JSON = MediaType.parse(“application/json; charset=utf-8”);
OkHttpClient client = new OkHttpClient();
String post(String url, String json) throws IOException {
    RequestBody body = RequestBody.create(JSON, json);
    Request request = new Request.Builder()
      .url(url)
      .post(body)
      .build();
    Response response = client.newCall(request).execute();
    f (response.isSuccessful()) {
        return response.body().string();
    } else {
        throw new IOException(“Unexpected code ” + response);
    }
}
使用Request的post方法来提交请求体RequestBody 
POST提交键值对 
OkHttp也可以通过POST方式把键值对数据传送到服务器

OkHttpClient client = new OkHttpClient();
String post(String url, String json) throws IOException {
    RequestBody formBody = new FormEncodingBuilder()
    .add(“platform”, “android”)
    .add(“name”, “bug”)
    .add(“subject”, “XXXXXXXXXXXXXXX”)
    .build();

    Request request = new Request.Builder()
      .url(url)
      .post(body)
      .build();

    Response response = client.newCall(request).execute();
    if (response.isSuccessful()) {
        return response.body().string();
    } else {
        throw new IOException(“Unexpected code ” + response);
    }
}
案例
布局文件:

<?xml version=”1.0″ encoding=”utf-8″?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
              android:layout_width=”match_parent”
              android:layout_height=”match_parent”
              android:orientation=”vertical”>

    <LinearLayout
        android:layout_width=”match_parent”
        android:layout_height=”wrap_content”
        android:gravity=”center_horizontal”
        android:orientation=”horizontal”>

        <Button
            android:id=”@+id/bt_get”
            android:layout_width=”wrap_content”
            android:layout_height=”wrap_content”
            android:text=”乌云Get请求”/>

        <Button
            android:id=”@+id/bt_post”
            android:layout_width=”wrap_content”
            android:layout_height=”wrap_content”
            android:text=”乌云Post请求”/>

    </LinearLayout>

    <TextView
        android:id=”@+id/tv_show”
        android:layout_width=”match_parent”
        android:layout_height=”wrap_content”/>
</LinearLayout>
 
Java代码: 
由于android本身是不允许在UI线程做网络请求操作的,所以我们自己写个线程完成网络操作

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import com.squareup.okhttp.FormEncodingBuilder;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Button bt_get;
    private Button bt_post;

    final OkHttpClient client = new OkHttpClient();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);

        bt_get=(Button)findViewById(R.id.bt_get);
        bt_post=(Button)findViewById(R.id.bt_post);

        bt_get.setOnClickListener(this);
        bt_post.setOnClickListener(this);

    }

    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.bt_get:
                getRequest();
                break;

            case R.id.bt_post:
                postRequest();
                break;

        }
    }

    private void getRequest() {

        final Request request=new Request.Builder()
                .get()
                .tag(this)
                .url(“http://www.wooyun.org”)
                .build();

        new Thread(new Runnable() {
            @Override
            public void run() {
                Response response = null;
                try {
                    response = client.newCall(request).execute();
                    if (response.isSuccessful()) {
                        Log.i(“WY”,”打印GET响应的数据:” + response.body().string());
                    } else {
                        throw new IOException(“Unexpected code ” + response);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();

    }

    private void postRequest() {

        RequestBody formBody = new FormEncodingBuilder()
                .add(“”,””)
                .build();

        final Request request = new Request.Builder()
                .url(“http://www.wooyun.org”)
                .post(formBody)
                .build();

        new Thread(new Runnable() {
            @Override
            public void run() {
                Response response = null;
                try {
                    response = client.newCall(request).execute();
                    if (response.isSuccessful()) {
                        Log.i(“WY”,”打印POST响应的数据:” + response.body().string());
                    } else {
                        throw new IOException(“Unexpected code ” + response);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();

    }

}
执行结果: 

官方Recipes
Synchronous Get(同步Get)
下载一个文件,打印他的响应头,以string形式打印响应体。 
       响应体的 string() 方法对于小文档来说十分方便、高效。但是如果响应体太大(超过1MB),应避免适应 string()方法 ,因为他会将把整个文档加载到内存中。对于超过1MB的响应body,应使用流的方式来处理body。

private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url(“http://publicobject.com/helloworld.txt”)
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException(“Unexpected code ” + response);

    Headers responseHeaders = response.headers();
    for (int i = 0; i < responseHeaders.size(); i++) {
      System.out.println(responseHeaders.name(i) + “: ” + responseHeaders.value(i));
    }

    System.out.println(response.body().string());
  }
Asynchronous Get(异步Get)
       在一个工作线程中下载文件,当响应可读时回调Callback接口。读取响应时会阻塞当前线程。OkHttp现阶段不提供异步api来接收响应体。

private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url(“http://publicobject.com/helloworld.txt”)
        .build();

    client.newCall(request).enqueue(new Callback() {
      @Override public void onFailure(Call call, IOException e) {
        e.printStackTrace();
      }

      @Override public void onResponse(Call call, Response response) throws IOException {
        if (!response.isSuccessful()) throw new IOException(“Unexpected code ” + response);

        Headers responseHeaders = response.headers();
        for (int i = 0, size = responseHeaders.size(); i < size; i++) {
          System.out.println(responseHeaders.name(i) + “: ” + responseHeaders.value(i));
        }

        System.out.println(response.body().string());
      }
    });
  }
Accessing Headers(提取响应头)
典型的HTTP头 像是一个 Map

private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url(“https://api.github.com/repos/square/okhttp/issues”)
        .header(“User-Agent”, “OkHttp Headers.java”)
        .addHeader(“Accept”, “application/json; q=0.5”)
        .addHeader(“Accept”, “application/vnd.github.v3+json”)
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException(“Unexpected code ” + response);

    System.out.println(“Server: ” + response.header(“Server”));
    System.out.println(“Date: ” + response.header(“Date”));
    System.out.println(“Vary: ” + response.headers(“Vary”));
  }
Posting a String(Post方式提交String)
       使用HTTP POST提交请求到服务。这个例子提交了一个markdown文档到web服务,以HTML方式渲染markdown。因为整个请求体都在内存中,因此避免使用此api提交大文档(大于1MB)。

public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse(“text/x-markdown; charset=utf-8”);

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    String postBody = “”
        + “Releases\n”
        + “——–\n”
        + “\n”
        + ” * _1.0_ May 6, 2013\n”
        + ” * _1.1_ June 15, 2013\n”
        + ” * _1.2_ August 11, 2013\n”;

    Request request = new Request.Builder()
        .url(“https://api.github.com/markdown/raw”)
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException(“Unexpected code ” + response);

    System.out.println(response.body().string());
  }

Post Streaming(Post方式提交流)

       以流的方式POST提交请求体。请求体的内容由流写入产生。这个例子是流直接写入Okio的BufferedSink。你的程序可能会使用OutputStream,你可以使用BufferedSink.outputStream()来获取。.

public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse(“text/x-markdown; charset=utf-8”);

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    RequestBody requestBody = new RequestBody() {
      @Override public MediaType contentType() {
        return MEDIA_TYPE_MARKDOWN;
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        sink.writeUtf8(“Numbers\n”);
        sink.writeUtf8(“——-\n”);
        for (int i = 2; i <= 997; i++) {
          sink.writeUtf8(String.format(” * %s = %s\n”, i, factor(i)));
        }
      }

      private String factor(int n) {
        for (int i = 2; i < n; i++) {
          int x = n / i;
          if (x * i == n) return factor(x) + ” × ” + i;
        }
        return Integer.toString(n);
      }
    };

    Request request = new Request.Builder()
        .url(“https://api.github.com/markdown/raw”)
        .post(requestBody)
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException(“Unexpected code ” + response);

    System.out.println(response.body().string());
  }
Posting a File(Post方式提交文件)
以文件作为请求体是十分简单的。

public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse(“text/x-markdown; charset=utf-8”);

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    File file = new File(“README.md”);

    Request request = new Request.Builder()
        .url(“https://api.github.com/markdown/raw”)
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException(“Unexpected code ” + response);

    System.out.println(response.body().string());
  }
Posting form parameters(Post方式提交表单)
       使用FormEncodingBuilder来构建和HTML标签相同效果的请求体。键值对将使用一种HTML兼容形式的URL编码来进行编码。

private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    RequestBody formBody = new FormBody.Builder()
        .add(“search”, “Jurassic Park”)
        .build();
    Request request = new Request.Builder()
        .url(“https://en.wikipedia.org/w/index.php”)
        .post(formBody)
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException(“Unexpected code ” + response);

    System.out.println(response.body().string());
  }
Posting a multipart request(Post方式提交分块请求)
       MultipartBuilder可以构建复杂的请求体,与HTML文件上传形式兼容。多块请求体中每块请求都是一个请求体,可以定义自己的请求头。这些请求头可以用来描述这块请求,例如他的Content-Disposition。如果Content-Length和Content-Type可用的话,他们会被自动添加到请求头中。

private static final String IMGUR_CLIENT_ID = “…”;
  private static final MediaType MEDIA_TYPE_PNG = MediaType.parse(“image/png”);

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
    RequestBody requestBody = new MultipartBody.Builder()
        .setType(MultipartBody.FORM)
        .addFormDataPart(“title”, “Square Logo”)
        .addFormDataPart(“image”, “logo-square.png”,
            RequestBody.create(MEDIA_TYPE_PNG, new File(“website/static/logo-square.png”)))
        .build();

    Request request = new Request.Builder()
        .header(“Authorization”, “Client-ID ” + IMGUR_CLIENT_ID)
        .url(“https://api.imgur.com/3/image”)
        .post(requestBody)
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException(“Unexpected code ” + response);

    System.out.println(response.body().string());
  }
Parse a JSON Response With Gson(使用GSON解析JSON响应)
       Gson是一个在JSON和Java对象之间转换非常方便的api。这里我们用Gson来解析Github API的JSON响应。 
注意:ResponseBody.charStream()使用响应头Content-Type指定的字符集来解析响应体。默认是UTF-8。

private final OkHttpClient client = new OkHttpClient();
  private final Gson gson = new Gson();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url(“https://api.github.com/gists/c2a7c39532239ff261be”)
        .build();
    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException(“Unexpected code ” + response);

    Gist gist = gson.fromJson(response.body().charStream(), Gist.class);
    for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
      System.out.println(entry.getKey());
      System.out.println(entry.getValue().content);
    }
  }

  static class Gist {
    Map<String, GistFile> files;
  }

  static class GistFile {
    String content;
  }
Response Caching(响应缓存)
       为了缓存响应,你需要一个你可以读写的缓存目录,和缓存大小的限制。这个缓存目录应该是私有的,不信任的程序应不能读取缓存内容。 
       一个缓存目录同时拥有多个缓存访问是错误的。大多数程序只需要调用一次new OkHttp(),在第一次调用时配置好缓存,然后其他地方只需要调用这个实例就可以了。否则两个缓存示例互相干扰,破坏响应缓存,而且有可能会导致程序崩溃。 
       响应缓存使用HTTP头作为配置。你可以在请求头中添加Cache-Control: max-stale=3600 ,OkHttp缓存会支持。你的服务通过响应头确定响应缓存多长时间,例如使用Cache-Control: max-age=9600。

private final OkHttpClient client;

  public CacheResponse(File cacheDirectory) throws Exception {
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    Cache cache = new Cache(cacheDirectory, cacheSize);

    client = new OkHttpClient.Builder()
        .cache(cache)
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url(“http://publicobject.com/helloworld.txt”)
        .build();

    Response response1 = client.newCall(request).execute();
    if (!response1.isSuccessful()) throw new IOException(“Unexpected code ” + response1);

    String response1Body = response1.body().string();
    System.out.println(“Response 1 response:          ” + response1);
    System.out.println(“Response 1 cache response:    ” + response1.cacheResponse());
    System.out.println(“Response 1 network response:  ” + response1.networkResponse());

    Response response2 = client.newCall(request).execute();
    if (!response2.isSuccessful()) throw new IOException(“Unexpected code ” + response2);

    String response2Body = response2.body().string();
    System.out.println(“Response 2 response:          ” + response2);
    System.out.println(“Response 2 cache response:    ” + response2.cacheResponse());
    System.out.println(“Response 2 network response:  ” + response2.networkResponse());

    System.out.println(“Response 2 equals Response 1? ” + response1Body.equals(response2Body));
  }
       为了防止使用缓存的响应,可以用CacheControl.FORCE_NETWORK。为了防止它使用网络,使用CacheControl.FORCE_CACHE。需要注意的是:如果您使用FORCE_CACHE和网络的响应需求,OkHttp则会返回一个504提示,告诉你不可满足请求响应。 
       Canceling a Call(取消一个Call) 
       使用Call.cancel()可以立即停止掉一个正在执行的call。如果一个线程正在写请求或者读响应,将会引发IOException。当call没有必要的时候,使用这个api可以节约网络资源。例如当用户离开一个应用时。不管同步还是异步的call都可以取消。 
       你可以通过tags来同时取消多个请求。当你构建一请求时,使用RequestBuilder.tag(tag)来分配一个标签。之后你就可以用OkHttpClient.cancel(tag)来取消所有带有这个tag的call。.

 private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url(“http://httpbin.org/delay/2”) // This URL is served with a 2 second delay.
        .build();

    final long startNanos = System.nanoTime();
    final Call call = client.newCall(request);

    // Schedule a job to cancel the call in 1 second.
    executor.schedule(new Runnable() {
      @Override public void run() {
        System.out.printf(“%.2f Canceling call.%n”, (System.nanoTime() – startNanos) / 1e9f);
        call.cancel();
        System.out.printf(“%.2f Canceled call.%n”, (System.nanoTime() – startNanos) / 1e9f);
      }
    }, 1, TimeUnit.SECONDS);

    try {
      System.out.printf(“%.2f Executing call.%n”, (System.nanoTime() – startNanos) / 1e9f);
      Response response = call.execute();
      System.out.printf(“%.2f Call was expected to fail, but completed: %s%n”,
          (System.nanoTime() – startNanos) / 1e9f, response);
    } catch (IOException e) {
      System.out.printf(“%.2f Call failed as expected: %s%n”,
          (System.nanoTime() – startNanos) / 1e9f, e);
    }
  }
Timeouts(超时)
       没有响应时使用超时结束call。没有响应的原因可能是客户点链接问题、服务器可用性问题或者这之间的其他东西。OkHttp支持连接,读取和写入超时。

private final OkHttpClient client;

  public ConfigureTimeouts() throws Exception {
    client = new OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .writeTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url(“http://httpbin.org/delay/2”) // This URL is served with a 2 second delay.
        .build();

    Response response = client.newCall(request).execute();
    System.out.println(“Response completed: ” + response);
  }
Per-call Configuration(每个Call的配置)
       使用OkHttpClient,所有的HTTP Client配置包括代理设置、超时设置、缓存设置。当你需要为单个call改变配置的时候,clone 一个 OkHttpClient。这个api将会返回一个浅拷贝(shallow copy),你可以用来单独自定义。下面的例子中,我们让一个请求是500ms的超时、另一个是3000ms的超时。

private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url(“http://httpbin.org/delay/1”) // This URL is served with a 1 second delay.
        .build();

    try {
      // Copy to customize OkHttp for this request.
      OkHttpClient copy = client.newBuilder()
          .readTimeout(500, TimeUnit.MILLISECONDS)
          .build();

      Response response = copy.newCall(request).execute();
      System.out.println(“Response 1 succeeded: ” + response);
    } catch (IOException e) {
      System.out.println(“Response 1 failed: ” + e);
    }

    try {
      // Copy to customize OkHttp for this request.
      OkHttpClient copy = client.newBuilder()
          .readTimeout(3000, TimeUnit.MILLISECONDS)
          .build();

      Response response = copy.newCall(request).execute();
      System.out.println(“Response 2 succeeded: ” + response);
    } catch (IOException e) {
      System.out.println(“Response 2 failed: ” + e);
    }
  }
Handling authentication(处理验证)
       OkHttp会自动重试未验证的请求。当响应是401 Not Authorized时,Authenticator会被要求提供证书。Authenticator的实现中需要建立一个新的包含证书的请求。如果没有证书可用,返回null来跳过尝试。

 private final OkHttpClient client;

  public Authenticate() {
    client = new OkHttpClient.Builder()
        .authenticator(new Authenticator() {
          @Override public Request authenticate(Route route, Response response) throws IOException {
            System.out.println(“Authenticating for response: ” + response);
            System.out.println(“Challenges: ” + response.challenges());
            String credential = Credentials.basic(“jesse”, “password1”);
            return response.request().newBuilder()
                .header(“Authorization”, credential)
                .build();
          }
        })
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url(“http://publicobject.com/secrets/hellosecret.txt”)
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException(“Unexpected code ” + response);

    System.out.println(response.body().string());
  }
To avoid making many retries when authentication isn’t working, you can return null to give up. For example, you may want to skip the retry when these exact credentials have already been attempted:

if (credential.equals(response.request().header(“Authorization”))) {
    return null; // If we already failed with these credentials, don’t retry.
   }
You may also skip the retry when you’ve hit an application-defined attempt limit:
  if (responseCount(response) >= 3) {
    return null; // If we’ve failed 3 times, give up.
  }
This above code relies on this responseCount() method:
  private int responseCount(Response response) {
    int result = 1;
    while ((response = response.priorResponse()) != null) {
      result++;
    }
    return result;
  }
OkHttp官方文档:https://github.com/square/okhttp/wiki 
参考链接:http://www.cnblogs.com/ct2011/p/3997368.html

Google Protocol Buffers 入门(protobuf tutorial译文)

 protobuf  Google Protocol Buffers 入门(protobuf tutorial译文)已关闭评论
6月 122016
 

分享好文, 转自:http://www.cnblogs.com/shitouer/archive/2013/04/10/google-protocol-buffers-tutorial.html , google-protocol-buffer-tutorial的中文翻译文章!


1. 前言

这篇入门教程是基于Java语言的,这篇文章我们将会:

  1. 创建一个.proto文件,在其内定义一些PB message
  2. 使用PB编译器
  3. 使用PB Java API 读写数据

这篇文章仅是入门手册,如果想深入学习及了解,可以参看: Protocol Buffer Language GuideJava API ReferenceJava Generated Code Guide, 以及Encoding Reference

2. 为什么使用Protocol Buffers

接下来用“通讯簿”这样一个非常简单的应用来举例。该应用能够写入并读取“联系人”信息,每个联系人由name,ID,email address以及contact photo number组成。这些信息的最终存储在文件中。

如何序列化并检索这样的结构化数据呢?有以下解决方案:

  1.  使用Java序列化(Java Serialization)。这是最直接的解决方式,因为该方式是内置于Java语言的,但是,这种方式有许多问题(Effective Java 对此有详细介绍),而且当有其他应用程序(比如C++ 程序及Python程序书写的应用)与之共享数据的时候,这种方式就不能工作了。
  2. 将数据项编码成一种特殊的字符串。例如将四个整数编码成“12:3:-23:67”。这种方法简单且灵活,但是却需要编写独立的,只需要用一次的编码和解码代码,并且解析过程需要一些运行成本。这种方式对于简单的数据结构非常有效。
  3. 将数据序列化为XML。这种方式非常诱人,因为易于阅读(某种程度上)并且有不同语言的多种解析库。在需要与其他应用或者项目共享数据的时候,这是一种非常有效的方式。但是,XML是出了名的耗空间,在编码解码上会有很大的性能损耗。而且呢,操作XML DOM数非常的复杂,远不如操作类中的字段简单。

Protocol Buffers可以灵活,高效且自动化的解决该问题,只需要:

  1. 创建一个.proto 文件,描述希望数据存储结构
  2. 使用PB compiler 创建一个类,该类可以高效的,以二进制方式自动编码和解析PB数据

该生成类提供组成PB数据字段的getter和setter方法,甚至考虑了如何高效的读写PB数据。更厉害的是,PB友好的支持字段拓展,拓展后的代码,依然能够正确的读取原来格式编码的数据。

3. 定义协议格式

首先需要创建一个.proto文件。非常简单,每一个需要序列化的数据结构,编码一个PB message,然后为message中的字段指明一个名字和类型即可。该“通讯簿”的.proto 文件addressbook.proto定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package tutorial;
 
option java_package = “com.example.tutorial”;
option java_outer_classname = “AddressBookProtos”;
 
message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;
 
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
 
  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }
 
  repeated PhoneNumber phone = 4;
}
message AddressBook {
  repeated Person person = 1;
}

可以看到,语法非常类似Java或者C++,接下来,我们一条一条来过一遍每句话的含义:

  • .proto文件以一个package声明开始。该声明有助于避免不同项目建设的命名冲突。Java版的PB,在没有指明java_package的情况下,生成的类默认的package即为此package。这里我们生命的java_package,所以最终生成的类会位于com.example.tutorial package下。这里需要强调一下,即使指明了java_package,我们建议依旧定义.proto文件的package。
  • 在package声明之后,紧接着是专门为java指定的两个选项:java_package 以及 java_outer_classname。java_package我们已经说过,不再赘述。java_outer_classname为生成类的名字,该类包含了所有在.proto中定义的类。如果该选项不显式指明的话,会按照驼峰规则,将.proto文件的名字作为该类名。例如“addressbook.proto”将会是“Addressbook”,“address_book.proto”即为“AddressBook”
  • java指定选项后边,即为message定义。每个message是一个包含了一系列指明了类型的字段的集合。这里的字段类型包含大多数的标准简单数据类型,包括bool,int32,float,double以及string。Message中也可以定义嵌套的message,例如“Person” message 包含“PhoneNumber” message。也可以将已定义的message作为新的数据类型,例如上例中,PhoneNumber类型在Person内部定义,但他是phone的type。在需要一个字段包含预先定义的一个列表的时候,也可以定义枚举类型,例如“PhoneType”。
  • 我们注意到, 每一个message中的字段,都有“=1”,“=2”这样的标记,这可不是初始化赋值,该值是message中,该字段的唯一标示符,在二进制编码时候会用到。数字1~15的表示需求少于一个字节,所以在编码的时候,有这样一个优化,你可以用1~15标记最常使用或者重复字段元素(repeated elements)。用16或者更大的数字来标记不太常用的可选元素。再重复字段中,每一个元素都需重复编码标签数字,所以,该优化对重复字段最佳(repeat fileds)。

message的没一个字段,都要用如下的三个修饰符(modifier)来声明:

  1. required:必须赋值,不能为空,否则该条message会被认为是“uninitialized”。build一个“uninitialized” message会抛出一个RuntimeException异常,解析一条“uninitialized” message会抛出一条IOException异常。除此之外,“required”字段跟“optional”字段并无差别。
  2. optional:字段可以赋值,也可以不赋值。假如没有赋值的话,会被赋上默认值。对于简单类型,默认值可以自己设定,例如上例的PhoneNumber中的PhoneType字段。如果没有自行设定,会被赋上一个系统默认值,数字类型会被赋为0,String类型会被赋为空字符串,bool类型会被赋为false。对于内置的message,默认值为该message的默认实例或者原型,即其内所有字段均为设置。当获取没有显式设置值的optional字段的值时,就会返回该字段的默认值。
  3. repeated:该字段可以重复任意次数,包括0次。重复数据的顺序将会保存在protocol buffer中,将这个字段想象成一个可以自动设置size的数组就可以了。

 Notice:应该格外小心定义Required字段。当因为某原因要把Required字段改为Optional字段是,会有问题,老版本读取器会认为消息中没有该字段不完整,可能会拒绝或者丢弃该字段(Google文档是这么说的,但是我试了一下,将required的改为optional的,再用原来required时候的解析代码去读,如果字段赋值的话,并不会出错,但是如果字段未赋值,会报这样错误:Exception in thread “main” com.google.protobuf.InvalidProtocolBufferException: Message missing required fields:fieldname)。在设计时,尽量将这种验证放在应用程序端的完成。Google的一些工程师对此也很困惑,他们觉得,required类型坏处大于好处,应该尽量仅适用optional或者repeated的。但也并不是所有的人都这么想。

如果想深入学习.proto文件书写,可以参考Protocol Buffer Language Guide。但是不要妄想会有类似于类继承这样的机制,Protocol Buffers不做这个…

4. 编译Protocol Buffers

定义好.proto文件后,接下来,就是使用该文件,运行PB的编译器protoc,编译.proto文件,生成相关类,可以使用这些类读写“通讯簿”没得message。接下来我们要做:

  1. 如果你还没有安装PB编译器,到这里现在安装:download the package
  2. 安装后,运行protoc,结束后会发现在项目com.example.tutorial package下,生成了AddressBookProtos.java文件:
1
2
3
protoc -I=$SRC_DIR –java_out=$DST_DIR $SRC_DIR/addressbook.proto
#for example
protoc -I=G:\workspace\protobuf\message –java_out=G:\workspace\protobuf\src\main\java G:\workspace\protobuf\messages\addressbook.proto

  • -I:指明应用程序的源码位置,假如不赋值,则有当前路径(说实话,该处我是直译了,并不明白是什么意思。我做了尝试,该值不能为空,如果为空,则提示赋了一个空文件夹,如果是当前路径,请用.代替,我用.代替,又提示不对。但是可以是任何一个路径,都运行正确,只要不为空);
  • –java_out:指明目的路径,即生成代码输出路径。因为我们这里是基于java来说的,所以这里是–java_out,相对其他语言,设置为相对语言即可
  • 最后一个参数即.proto文件

Notice:此处运行完毕后,查看生成的代码,很有可能会出现一些类没有定义等错误,例如:com.google cannot be resolved to a type等。这是因为项目中缺少protocol buffers的相应library。在Protocol Buffers的源码包里,你会发现java/src/main/java,将这下边的文件拷贝到你的项目,大概可以解决问题。我只能说大概,因为当时我在弄得时候,也是刚学,各种出错,比较恶心。有一个简单的方法,呵呵,对于懒汉来说。创建一个maven的java项目,在pom.xml中,添加Protocol Buffers的依赖即可解决所有问题~在pom.xml中添加如下依赖(注意版本):

1
2
3
4
5
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>2.5.0</version>
</dependency>

 5. Protocol Buffer Java API

5.1 产生的类及方法

接下来看一下PB编译器创建了那些类以及方法。首先会发现一个.java文件,其内部定义了一个AddressBookProtos类,即我们在addressbook.proto文件java_outer_classname 指定的。该类内部有一系列内部类,对应分别是我们在addressbook.proto中定义的message。每个类内部都有相应的Builder类,我们可以用它创建类的实例。生成的类及类内部的Builder类,均自动生成了获取message中字段的方法,不同的是,生成的类仅有getter方法,而生成类内部的Builder既有getter方法,又有setter方法。本例中Person类,其仅有getter方法,如图所示:

 但是Person.Builder类,既有getter方法,又有setter方法,如图:

person.builderperson.builder

从上边两张图可以看到:

  1. 每一个字段都有JavaBean风格的getter和setter
  2. 对于每一个简单类型变量,还对应都有一个has这样的一个方法,如果该字段被赋值了,则返回true,否则,返回false
  3. 对每一个变量,都有一个clear方法,用于置空字段

对于repeated字段:

repeated filedrepeated filed

从图上看:

  1. 从person.builder图上看出,对于repeated字段,还有一个特殊的getter,即getPhoneCount方法,及repeated字段还有一个特殊的count方法
  2. 其getter和setter方法根据index获取或设置一个数据项
  3. add()方法用于附加一个数据项
  4. addAll()方法来直接增加一个容器中的所有数据项

注意到一点:所有的这些方法均命名均符合驼峰规则,即使在.proto文件中是小写的。PB compiler生成的方法及字段等都是按照驼峰规则来产生,以符合基本的Java规范,当然,其他语言也尽量如此。所以,在proto文件中,命名最好使用用“_”来分割不同小写的单词。

 5.2 枚举及嵌套类

从代码中可以发现,还产生了一个枚举:PhoneType,该枚举位于Person类内部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum PhoneType
        implements com.google.protobuf.ProtocolMessageEnum {
      /**
       * <code>MOBILE = 0;</code>
       */
      MOBILE(0,0),
      /**
       * <code>HOME = 1;</code>
       */
      HOME(1,1),
      /**
       * <code>WORK = 2;</code>
       */
      WORK(2,2),
      ;
      …
}

除此之外,如我们所预料,还有一个Person.PhoneNumber内部类,嵌套在Person类中,可以自行看一下生成代码,不再粘贴。

5.3 Builders vs. Messages

由PB compiler生成的消息类是不可变的。一旦一个消息对象构建出来,他就不再能够修改,就像java中的String一样。在构建一个message之前,首先要构建一个builder,然后使用builder的setter或者add()等方法为所需字段赋值,之后调用builder对象的build方法。

在使用中会发现,这些构造message对象的builder的方法,都又会返回一个新的builder,事实上,该builder跟调用这个方法的builder是同一方法。这样做的目的,仅是为了方便而已,我们可以把所有的setter写在一行内。

如下构造一个Person实例:

1
2
3
4
5
6
7
8
9
10
11
12
Person john = Person
        .newBuilder()
        .setId(1)
        .setName(“john”)
        .setEmail(“[email protected]”)
        .addPhone(
                PhoneNumber
                .newBuilder()
                .setNumber(“1861xxxxxxx”)
                .setType(PhoneType.WORK)
                .build()
        ).build();

5.4 标准消息方法

每一个消息类及Builder类,基本都包含一些公用方法,用来检查和维护这个message,包括:

  1.  isInitialized(): 检查是否所有的required字段是否被赋值
  2. toString(): 返回一个便于阅读的message表示(本来是二进制的,不可读),尤其在debug时候比较有用
  3. mergeFrom(Message other): 仅builder有此方法,将其message的内容与此message合并,覆盖简单及重复字段
  4. clear(): 仅builder有此方法,清空所有的字段

5.5 解析及序列化

对于每一个PB类,均提供了读写二进制数据的方法:

  1. byte[] toByteArray();: 序列化message并且返回一个原始字节类型的字节数组
  2. static Person parseFrom(byte[] data);: 将给定的字节数组解析为message
  3. void writeTo(OutputStream output);: 将序列化后的message写入到输出流
  4. static Person parseFrom(InputStream input);: 读入并且将输入流解析为一个message

这里仅列出了几个解析及序列化方法,完整列表,可以参见:Message API reference

6. 使用PB生成类写入

接下来使用这些生成的PB类,初始化一些联系人,并将其写入一个文件中。

下面的程序首先从一个文件中读取一个通讯簿(AddressBook),然后添加一个新的联系人,再将新的通讯簿写回到文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package com.example.tutorial;
 
import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintStream;
 
class AddPerson {
    // This function fills in a Person message based on user input.
    static Person PromptForAddress(BufferedReader stdin, PrintStream stdout)
            throws IOException {
        Person.Builder person = Person.newBuilder();
 
        stdout.print(“Enter person ID: “);
        person.setId(Integer.valueOf(stdin.readLine()));
 
        stdout.print(“Enter name: “);
        person.setName(stdin.readLine());
 
        stdout.print(“Enter email address (blank for none): “);
        String email = stdin.readLine();
        if (email.length() >0) {
            person.setEmail(email);
        }
 
        while (true) {
            stdout.print(“Enter a phone number (or leave blank to finish): “);
            String number = stdin.readLine();
            if (number.length() ==0) {
                break;
            }
 
            Person.PhoneNumber.Builder phoneNumber = Person.PhoneNumber
                    .newBuilder().setNumber(number);
 
            stdout.print(“Is this a mobile, home, or work phone? “);
            String type = stdin.readLine();
            if (type.equals(“mobile”)) {
                phoneNumber.setType(Person.PhoneType.MOBILE);
            }else if (type.equals(“home”)) {
                phoneNumber.setType(Person.PhoneType.HOME);
            }else if (type.equals(“work”)) {
                phoneNumber.setType(Person.PhoneType.WORK);
            }else {
                stdout.println(“Unknown phone type.  Using default.”);
            }
 
            person.addPhone(phoneNumber);
        }
 
        return person.build();
    }
 
    // Main function: Reads the entire address book from a file,
    // adds one person based on user input, then writes it back out to the same
    // file.
    public static void main(String[] args)throws Exception {
        if (args.length !=1) {
            System.err.println(“Usage:  AddPerson ADDRESS_BOOK_FILE”);
            System.exit(-1);
        }
 
        AddressBook.Builder addressBook = AddressBook.newBuilder();
 
        // Read the existing address book.
        try {
            addressBook.mergeFrom(new FileInputStream(args[0]));
        }catch (FileNotFoundException e) {
            System.out.println(args[0]
                    +”: File not found.  Creating a new file.”);
        }
 
        // Add an address.
        addressBook.addPerson(PromptForAddress(new BufferedReader(
                new InputStreamReader(System.in)), System.out));
 
        // Write the new address book back to disk.
        FileOutputStream output =new FileOutputStream(args[0]);
        addressBook.build().writeTo(output);
        output.close();
    }
}

 7. 使用PB生成类读取

运行第六部分程序,写入几个联系人到文件中,接下来,我们就要读取联系人。程序入下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.example.tutorial;
import java.io.FileInputStream;
 
import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
 
class ListPeople {
  // Iterates though all people in the AddressBook and prints info about them.
  static void Print(AddressBook addressBook) {
    for (Person person: addressBook.getPersonList()) {
      System.out.println(“Person ID: ” + person.getId());
      System.out.println(”  Name: ” + person.getName());
      if (person.hasEmail()) {
        System.out.println(”  E-mail address: ” + person.getEmail());
      }
 
      for (Person.PhoneNumber phoneNumber : person.getPhoneList()) {
        switch (phoneNumber.getType()) {
          case MOBILE:
            System.out.print(”  Mobile phone #: “);
            break;
          case HOME:
            System.out.print(”  Home phone #: “);
            break;
          case WORK:
            System.out.print(”  Work phone #: “);
            break;
        }
        System.out.println(phoneNumber.getNumber());
      }
    }
  }
 
  // Main function:  Reads the entire address book from a file and prints all
  //   the information inside.
  public static void main(String[] args)throws Exception {
    if (args.length !=1) {
      System.err.println(“Usage:  ListPeople ADDRESS_BOOK_FILE”);
      System.exit(-1);
    }
 
    // Read the existing address book.
    AddressBook addressBook =
      AddressBook.parseFrom(new FileInputStream(args[0]));
 
    Print(addressBook);
  }
}

至此我们已经可以使用生成类写入和读取PB message。

8. 拓展PB

当产品发布后,迟早有一天我们需要改善我们的PB定义。如果要做到新的PB能够向后兼容,同时老的PB又能够向前兼容,我们必须遵守如下规则:

  1. 千万不要修改现有字段后边的数值标签
  2. 千万不要增加或者删除required字段
  3. 可以删除optional或者repeated字段
  4. 可以添加新的optional或者repeated字段,但是必须使用新的数字标签(该数字标签必须从未在该PB中使用过,包括已经删除字段的数字标签)

如果违反了这些规则,会有一些相应的异常,可参见some exceptions,但是这些异常,很少很少会被用到。

遵守这些规则,老的代码可以正确的读取新的message,但是会忽略新的字段;对于删掉的optional的字段,老代码会使用他们的默认值;对于删除的repeated字段,则把他们置为空。

新的代码也将能够透明的读取老的messages。但是必须注意,新的optional字段在老的message中是不存在的,必须显式的使用has_方法来判断其是否设置了,或者在.proto 文件中以[default = value]形式提供默认值。如果没有指定默认值的话,会按照类型默认值赋值。对于string类型,默认值是空字符串。对于bool来说,默认值是false。对于数字类型,默认值是0。

9. 高级用法

Protocol Buffers的应用远远不止简单的存取以及序列化。如果想了解更多用法,可以去研究Java API reference

Protocol Message Class提供了一个重要特性:反射。不需要再写任何特殊的message类型就可以遍历一条message的所有字段以及操作字段的值。反射的一个非常重要的应用是可以将PBmessage与其他的编码语言进行转化,例如与XML或者JSON之间。

反射另外一个更加高级的应用应该是两个同一类型message的之间的不同,或者开发一种可以成为“Protocol Buffers 正则表达式”的应用,使用它,可以编写符合一定消息内容的表达式。

除此之外,开动脑筋,你会发现,Protocol Buffers能解决远远超过你刚开始对他的期待。

译自:https://developers.google.com/protocol-buffers/docs/javatutorial

X分钟学习clojure

 clojure, 开发  X分钟学习clojure已关闭评论
2月 172016
 

开始学习clojure,网上看到的一个快速入门学习的文章,推荐下,转自:https://segmentfault.com/a/1190000000414279

Clojure是运行在JVM上的Lisp家族中的一员。她比Common Lisp更强调纯函数式编程,且自发布时便包含了一组工具来处理状态。

这种组合让她能十分简单且自动地处理并发问题。

(你需要使用Clojure 1.2或更新的发行版)

	
; 注释以分号开始。

Clojure代码由一个个form组成, 即写在小括号里的由空格分开的一组语句。
Clojure解释器会把第一个元素当做一个函数或者宏来调用,其余的被认为是参数。

Clojure代码的第一条语句一般是用ns来指定当前的命名空间。

	
(ns learnclojure)

str会使用所有参数来创建一个字符串

	
(str "Hello" " " "World") ; => "Hello World"

数学计算比较直观

	
(+ 1 1) ; => 2 (- 2 1) ; => 1 (* 1 2) ; => 2 (/ 2 1) ; => 2

等号是 =

	
(= 1 1) ; => true (= 2 1) ; => false

逻辑非

	
(not true) ; => false

嵌套的form工作起来应该和你预想的一样

	
(+ 1 (- 3 2)) ; = 1 + (3 - 2) => 2

类型

Clojure使用Java的Object来描述布尔值、字符串和数字
用函数 class 来查看具体的类型

	
(class 1) ; 整形默认是java.lang.Long类型 (class 1.); 浮点默认是java.lang.Double类型的 (class ""); String是java.lang.String类型的,要用双引号引起来 (class false) ; 布尔值是java.lang.Boolean类型的 (class nil); "null"被称作nil

如果你想创建一组数据字面量,用单引号(‘)来阻止form被解析和求值

	
'(+ 1 2) ; => (+ 1 2)

单引号是quote的简写形式,故上式等价于(quote (+ 1 2))

可以对一个引用列表求值

	
(eval '(+ 1 2)) ; => 3

集合(Collection)和序列

List的底层实现是链表,Vector的底层实现是数组
二者也都是java类

	
(class [1 2 3]); => clojure.lang.PersistentVector (class '(1 2 3)); => clojure.lang.PersistentList

list本可以写成(1 2 3), 但必须用引用来避免被解释器当做函数来求值。
(list 1 2 3)等价于'(1 2 3)

集合其实就是一组数据
List和Vector都是集合:

	
(coll? '(1 2 3)) ; => true (coll? [1 2 3]) ; => true

序列 (seqs) 是数据列表的抽象描述
只有列表才可称作序列。

	
(seq? '(1 2 3)) ; => true (seq? [1 2 3]) ; => false

序列被访问时只需要提供一个值,所以序列可以被惰性加载——也就意味着可以定义一个无限序列:

	
(range 4) ; => (0 1 2 3) (range) ; => (0 1 2 3 4 ...) (无限序列) (take 4 (range)) ; (0 1 2 3)

cons用以向列表或向量的起始位置添加元素

	
(cons 4 [1 2 3]) ; => (4 1 2 3) (cons 4 '(1 2 3)) ; => (4 1 2 3)

conj将以最高效的方式向集合中添加元素。
对于列表,数据会在起始位置插入,而对于向量,则在末尾位置插入。

	
(conj [1 2 3] 4) ; => [1 2 3 4] (conj '(1 2 3) 4) ; => (4 1 2 3)

用concat来合并列表或向量

	
(concat [1 2] '(3 4)) ; => (1 2 3 4)

用filter来过滤集合中的元素,用map来根据指定的函数来映射得到一个新的集合

	
(map inc [1 2 3]) ; => (2 3 4) (filter even? [1 2 3]) ; => (2)

recuce使用函数来规约集合

	
(reduce + [1 2 3 4]) ; = (+ (+ (+ 1 2) 3) 4) ; => 10

reduce还能指定一个初始参数

	
(reduce conj [] '(3 2 1)) ; = (conj (conj (conj [] 3) 2) 1) ; => [3 2 1]

函数

用fn来创建函数。函数的返回值是最后一个表达式的值

	
(fn [] "Hello World") ; => fn

你需要再嵌套一组小括号来调用它

	
((fn [] "Hello World")) ; => "Hello World"

你可以用def来创建一个变量(var)

	
(def x 1) x ; => 1

将函数定义为一个变量(var)

	
(def hello-world (fn [] "Hello World")) (hello-world) ; => "Hello World"

你可用defn来简化函数的定义

	
(defn hello-world [] "Hello World")

中括号内的内容是函数的参数。

	
(defn hello [name] (str "Hello " name)) (hello "Steve") ; => "Hello Steve"

你还可以用这种简写的方式来创建函数:

	
(def hello2 #(str "Hello " %1)) (hello2 "Fanny") ; => "Hello Fanny"

函数也可以有多个参数列表。

	
(defn hello3 ([] "Hello World") ([name] (str "Hello " name))) (hello3 "Jake") ; => "Hello Jake" (hello3) ; => "Hello World"

可以定义变参函数,即把&后面的参数全部放入一个序列

	
(defn count-args [& args] (str "You passed " (count args) " args: " args)) (count-args 1 2 3) ; => "You passed 3 args: (1 2 3)"

可以混用定参和变参(用&来界定)

	
(defn hello-count [name & args] (str "Hello " name ", you passed " (count args) " extra args")) (hello-count "Finn" 1 2 3) ; => "Hello Finn, you passed 3 extra args"

哈希表

基于hash的map和基于数组的map(即arraymap)实现了相同的接口,hashmap查询起来比较快,
但不保证元素的顺序。

	
(class {:a 1 :b 2 :c 3}) ; => clojure.lang.PersistentArrayMap (class (hash-map :a 1 :b 2 :c 3)) ; => clojure.lang.PersistentHashMap

arraymap在足够大的时候,大多数操作会将其自动转换成hashmap,
所以不用担心(对大的arraymap的查询性能)。

map支持很多类型的key,但推荐使用keyword类型
keyword类型和字符串类似,但做了一些优化。

	
(class :a) ; => clojure.lang.Keyword (def stringmap {"a" 1, "b" 2, "c" 3}) stringmap ; => {"a" 1, "b" 2, "c" 3} (def keymap {:a 1, :b 2, :c 3}) keymap ; => {:a 1, :c 3, :b 2}

顺便说一下,map里的逗号是可有可无的,作用只是提高map的可读性。

从map中查找元素就像把map名作为函数调用一样。

	
(stringmap "a") ; => 1 (keymap :a) ; => 1

可以把keyword写在前面来从map中查找元素。

	
(:b keymap) ; => 2

但不要试图用字符串类型的key来这么做。

	
("a" stringmap) ; => Exception: java.lang.String cannot be cast to clojure.lang.IFn

查找不存在的key会返回nil。

	
(stringmap "d") ; => nil

用assoc函数来向hashmap里添加元素

	
(def newkeymap (assoc keymap :d 4)) newkeymap ; => {:a 1, :b 2, :c 3, :d 4}

但是要记住的是clojure的数据类型是不可变的!

	
keymap ; => {:a 1, :b 2, :c 3}

用dissoc来移除元素

	
(dissoc keymap :a :b) ; => {:c 3}

集合(Set)

	
(class #{1 2 3}) ; => clojure.lang.PersistentHashSet (set [1 2 3 1 2 3 3 2 1 3 2 1]) ; => #{1 2 3}

用conj新增元素

	
(conj #{1 2 3} 4) ; => #{1 2 3 4}

用disj移除元素

	
(disj #{1 2 3} 1) ; => #{2 3}

把集合当做函数调用来检查元素是否存在:

	
(#{1 2 3} 1) ; => 1 (#{1 2 3} 4) ; => nil

在clojure.sets模块下有很多相关函数。

常用的form

clojure里的逻辑控制结构都是用宏(macro)实现的,这在语法上看起来没什么不同。

	
(if false "a" "b") ; => "b" (if false "a") ; => nil

用let来创建临时的绑定变量。

	
(let [a 1 b 2] (> a b)) ; => false

用do将多个语句组合在一起依次执行

	
(do (print "Hello") "World") ; => "World" (prints "Hello")

函数定义里有一个隐式的do

	
(defn print-and-say-hello [name] (print "Saying hello to " name) (str "Hello " name)) (print-and-say-hello "Jeff") ;=> "Hello Jeff" (prints "Saying hello to Jeff")

let也是如此

	
(let [name "Urkel"] (print "Saying hello to " name) (str "Hello " name)) ; => "Hello Urkel" (prints "Saying hello to Urkel")

模块

用use来导入模块里的所有函数

	
(use 'clojure.set)

然后就可以使用set相关的函数了

	
(intersection #{1 2 3} #{2 3 4}) ; => #{2 3} (difference #{1 2 3} #{2 3 4}) ; => #{1}

你也可以从一个模块里导入一部分函数。

	
(use '[clojure.set :only [intersection]])

用require来导入一个模块

	
(require 'clojure.string)

用/来调用模块里的函数
下面是从模块clojure.string里调用blank?函数。

	
(clojure.string/blank? "") ; => true

在import里你可以给模块名指定一个较短的别名。

	
(require '[clojure.string :as str]) (str/replace "This is a test." #"[a-o]" str/upper-case) ; => "THIs Is A tEst."

#””用来表示一个正则表达式

你可以在一个namespace定义里用:require的方式来require(或use,但最好不要用)模块。
这样的话你无需引用模块列表。

	
(ns test (:require [clojure.string :as str] [clojure.set :as set]))

Java

Java有大量的优秀的库,你肯定想学会如何用clojure来使用这些Java库。

用import来导入java类

	
(import java.util.Date)

也可以在ns定义里导入

	
(ns test (:import java.util.Date java.util.Calendar))

用类名末尾加.的方式来new一个Java对象

	
(Date.) ; <a date object>

用.操作符来调用方法,或者用.method的简化方式。

	
(. (Date.) getTime) ; <a timestamp> (.getTime (Date.)) ; 和上例一样。

用/调用静态方法

	
(System/currentTimeMillis) ; <a timestamp> (system is always present)

用doto来更方便的使用(可变)类。

	
(import java.util.Calendar) (doto (Calendar/getInstance) (.set 2000 1 1 0 0 0) .getTime) ; => A Date. set to 2000-01-01 00:00:00

STM

软件内存事务(Software Transactional Memory)被clojure用来处理持久化的状态。
clojure里内置了一些结构来使用STM。
atom是最简单的。给它传一个初始值

	
(def my-atom (atom {}))

用swap!更新atom。
swap!会以atom的当前值为第一个参数来调用一个指定的函数,
swap其余的参数作为该函数的第二个参数。

	
(swap! my-atom assoc :a 1) ; Sets my-atom to the result of (assoc {} :a 1) (swap! my-atom assoc :b 2) ; Sets my-atom to the result of (assoc {:a 1} :b 2)

用@读取atom的值

	
my-atom ;=> Atom<#...> (返回Atom对象) @my-atom ; => {:a 1 :b 2}

下例是一个使用atom实现的简单计数器

	
(def counter (atom 0)) (defn inc-counter [] (swap! counter inc)) (inc-counter) (inc-counter) (inc-counter) (inc-counter) (inc-counter) @counter ; => 5

其他STM相关的结构是ref和agent.
; Refs: http://clojure.org/refs
; Agents: http://clojure.org/agents

进阶读物

本文肯定不足以讲述关于clojure的一切,但是希望足以让你迈出第一步。

Clojure.org官网有很多文章:
http://clojure.org/

Clojuredocs.org有大多数核心函数的文档,还带了示例哦:
http://clojuredocs.org/quickref/Clojure%20Core

4Clojure是个很赞的用来练习clojure/FP技能的地方:
http://www.4clojure.com/

Clojure-doc.org 有很多入门级的文章:
http://clojure-doc.org/

lucence入门

 lucence  lucence入门已关闭评论
8月 072015
 

一篇很好的lucence入门他文章,推荐下,转自:http://blog.csdn.net/chenghui0317/article/details/10052103

一、Lucene的介绍

     Lucene是一个全文检索的框架,apache组织提供了一个用java实现的全文搜索引擎的开源项目,其功能非常的强大,api非常简单,并且有了全文检索的功能支持可以非常方便的实现根据关键字来搜索整个应用系统的内容,大大提高了用户的体验效果。   使用Lucene来建立、搜索功能和操作数据库有一点像,这样就可想而知,Lucene使用起来还是蛮方便的。

    那么为什么要使用Lucene 呢? 因为如果没有使用Lucene ,那么就要根据某个关键字来搜索数据库表记录, 就要使用like 一个字符一个字符去匹配,这样子查询的方式的要累坏程序员不说,并且查询数据库的性能开销可想而知。

二、Lucene的执行流程

    前面说了Lucene的操作方式和操作数据库有点相似,所以如果要使用Lucene 就要先创建“数据库”,然后往这个“数据表”中一行一行的插入数据,数据插入成功之后,就可以操作这张“数据表”,实现增删改查操作了。

   总的来说,可以这样理解:

  1、创建一个索引文件目录,然后把需要检索的信息 用Field 对应匹配的 封装成一个Document文档对象,将这个对象放入索引文件目录中,这里既可以将索引存放到磁盘中,也可以放入内存中,如果放入内存,那么程序关闭索引就没有了,所以一般都是将索引放入磁盘中;

  2、如果发现信息有问题需要删除,那么索引文件也要删除,否则检索的时候还是会查询得到,这个时候需要根据索引id去删除对应的索引;

  3、如果发现信息被更新了,那么索引文件也要更新,这个时候需要先将旧的索引删除然后添加新的索引;

  4、最后重头戏是全文搜索了,这和查询数据库一样,先需要创建索引读取对象,然后封装Query查询对象,调用search()方法 得到检索结果。

三、使用Lucene的准备条件

   lucene-core-3.6.0.jar

   lucene-highlighter-3.6.0.jar

   lucene-memory-3.6.0.jar

下载地址:http://download.csdn.net/detail/ch656409110/5971413


四、使用Lucene实战

1、使用Lucene将索引 写入内存

实现的思路如下:

   <1>创建了内存目录对象RAMDirectory 和 索引写入器IndexWriter  ;

   <2>利用索引写入器将指定的数据存入内存目录对象中;

   <3>创建IndexSearch 索引查询对象,然后根据关键字封装Query查询对象;

   <4>调用search()方法,将查询的结果返回给TopDocs ,迭代里面所有的Document对象,显示查询结果;

   <5>关闭IndexWriter 写入器,关闭RAMDirectory目录对象。

具体代码如下:

  1. package com.lucene.test;  
  2.   
  3. import java.io.IOException;  
  4.   
  5. import org.apache.lucene.analysis.SimpleAnalyzer;  
  6. import org.apache.lucene.document.Document;  
  7. import org.apache.lucene.document.Field;  
  8. import org.apache.lucene.index.IndexWriter;  
  9. import org.apache.lucene.index.Term;  
  10. import org.apache.lucene.search.IndexSearcher;  
  11. import org.apache.lucene.search.Query;  
  12. import org.apache.lucene.search.TermQuery;  
  13. import org.apache.lucene.search.TopDocs;  
  14. import org.apache.lucene.store.RAMDirectory;  
  15.   
  16. /** 
  17.  * lucene 检索内存索引 非常简单的例子 
  18.  *  
  19.  * @author Administrator 
  20.  *  
  21.  */  
  22. public class RAMDirectoryDemo {  
  23.     public static void main(String[] args) throws IOException {  
  24.         long startTime = System.currentTimeMillis();  
  25.         System.out.println(“*****************检索开始**********************”);  
  26.         // 创建一个内存目录对象,所以这里生成的索引不会放在磁盘中,而是在内存中。  
  27.         RAMDirectory directory = new RAMDirectory();  
  28.         /* 
  29.          * 创建索引写入对象,该对象既可以把索引写入到磁盘中也可以写入到内存中。 参数说明:  
  30.          * public IndexWriter(Directory d, Analyzer a, boolean create, MaxFieldLength mfl) 
  31.          * directory:目录对象,也可以是FSDirectory 磁盘目录对象  
  32.          * analyzer:分词器,分词器就是将检索的关键字分割成一组组词组, 它是lucene检索查询的一大特色之一, new SimpleAnalyzer()这个是lucene自带的最为简单的分词器; create: 是否新建,这里肯定要设为true; 
  33.          * maxFieldLength:这个是分词器拆分最大长度,因为各种不同类型的分词器拆分的字符颗粒细化程度不一样,所以需要设置一个最长的拆分长度。IndexWriter.MaxFieldLength.UNLIMITED表示无限制; 
  34.          */  
  35.         IndexWriter writer = new IndexWriter(directory, new SimpleAnalyzer(),true, IndexWriter.MaxFieldLength.UNLIMITED);  
  36.         // 创建Document 文档对象,在lucene中创建的索引可以看成数据库中的一张表,表中也可以有字段,往里面添加内容之后可以根据字段去匹配查询  
  37.         // 下面创建的doc对象中添加了三个字段,分别为name,sex,dosomething,  
  38.         Document doc = new Document();  
  39.         /* 
  40.         * 参数说明 public Field(String name, String value, Store store, Index index)  
  41.         * name : 字段名称  
  42.         * value : 字段的值 store : 
  43.         *  Field.Store.YES:存储字段值(未分词前的字段值) Field.Store.NO:不存储,存储与索引没有关系 
  44.         *  Field.Store.COMPRESS:压缩存储,用于长文本或二进制,但性能受损  
  45.         * index : 建立索引的方式,是否建立分词等等 
  46.         *  Field.Index.ANALYZED:分词建索引 
  47.         *  Field.Index.ANALYZED_NO_NORMS:分词建索引,但是Field的值不像通常那样被保存,而是只取一个byte,这样节约存储空间  
  48.         *  Field.Index.NOT_ANALYZED:不分词且索引 ,一旦指定为这种类型后将会被lucenn录入索引中,但不会被作为关键搜索,除非输入所有的关键字 
  49.         *  Field.Index.NOT_ANALYZED_NO_NORMS:不分词建索引,Field的值去一个byte保存 
  50.         */  
  51.         doc.add(new Field(“name”“Chenghui”, Field.Store.YES,Field.Index.ANALYZED));  
  52.         doc.add(new Field(“sex”“男的”, Field.Store.YES,Field.Index.NOT_ANALYZED));  
  53.         doc.add(new Field(“dosometing”“I am learning lucene “,Field.Store.YES, Field.Index.ANALYZED));  
  54.         writer.addDocument(doc);  
  55.         writer.close(); // 这里可以提前关闭,因为dictory 写入内存之后 与IndexWriter 没有任何关系了  
  56.   
  57.         // 因为索引放在内存中,所以存放进去之后要立马测试,否则,关闭应用程序之后就检索不到了  
  58.         // 创建IndexSearcher 检索索引的对象,里面要传递上面写入的内存目录对象directory  
  59.         IndexSearcher searcher = new IndexSearcher(directory);  
  60.         // 根据搜索关键字 封装一个term组合对象,然后封装成Query查询对象  
  61.         // dosometing是上面定义的字段,lucene是检索的关键字  
  62.          Query query = new TermQuery(new Term(“dosometing”“lucene”));  
  63.         // Query query = new TermQuery(new Term(“sex”, “男”));  
  64.         // Query query = new TermQuery(new Term(“name”, “cheng”));   
  65.           
  66.         // 去索引目录中查询,返回的是TopDocs对象,里面存放的就是上面放的document文档对象  
  67.         TopDocs rs = searcher.search(query, null10);  
  68.         long endTime = System.currentTimeMillis();  
  69.         System.out.println(“总共花费” + (endTime – startTime) + “毫秒,检索到” + rs.totalHits + “条记录。”);  
  70.         for (int i = 0; i < rs.scoreDocs.length; i++) {  
  71.             // rs.scoreDocs[i].doc 是获取索引中的标志位id, 从0开始记录  
  72.             Document firstHit = searcher.doc(rs.scoreDocs[i].doc);  
  73.             System.out.println(“name:” + firstHit.getField(“name”).stringValue());  
  74.             System.out.println(“sex:” + firstHit.getField(“sex”).stringValue());  
  75.             System.out.println(“dosomething:” + firstHit.getField(“dosometing”).stringValue());  
  76.         }  
  77.         writer.close();  
  78.         directory.close();  
  79.         System.out.println(“*****************检索结束**********************”);  
  80.     }  
  81. }  

运行结果如下:

由上可知: 上面根据“lucene”关键字查询成功了,返回的是一个个Document封装的对象。

另外 如果在创建索引写入器IndexWriter的时候 create 参数指定为false的话 ,会报错,找不到索引文件,因为每一次读取都是”现存现取“的模式,具体如下:

Exception in thread “main” org.apache.lucene.index.IndexNotFoundException: no segments* file found in org.apache.lucene.store.RAMDirectory@156ee8e lockFactory=org.apache.lucene.store.SingleInstanceLockFactory@47b480: files: []

根据dosomething可以查询成功,同样根据sex字段 和name字段一样可以实现查询功能。

<1>如果把 Query query = new TermQuery(new Term(“sex”, “男”)); 取消注释 ,然后查询一下:

结果发现根本没有记录,这是因为在生成索引的时候指定的sex字段 是Field.Index.NOT_ANALYZED 类型的,所以Lucene没有为该字段建立索引,也就不能根据sex字段去查询。

<2>然后 将sex 字段改为Field.Index.ANALYZED类型的,再查询一下:

结果发现依然没有记录,为什么?

这是因为使用的分词器SimpleAnalyzer 没有那么智能化,它只会对关键字中包含空格的词组进行分词和匹配,简而言之:如果“中国”是搜索关键字,那么它只会匹配索引表中对应字段中包含“ 中国 ”这样的词组,而包含“中”或者“国”的汉字不会被匹配, 记住这里面前后都有空格分开。同样的,如果创建的索引中数据有“我是中国人” ,那么要根据“中国“这个关键字根本匹配不到,因为SimpleAnalyzer 这个分词器只会去匹配 以空格分开的词组,所以要想匹配成功,那么添加索引的数据应该改为“我是 中国 人”,这样才会被检索到。

所以所以,要想使用”男“这个关键字匹配成功,就需要在添加索引的时候 改为”男 的“才可以。

既然中文汉字 是这样,试试看英文字母 是否亦如此。

果不其然:

<1>如果录入name字段的索引为”chenghui“  关键字指定为“cheng”搜不到;

<2>如果录入name字段的索引为”cheng hui“  关键字指定为“cheng”可以搜到;

<3>如果录入name字段的索引为”cheng hui“  关键字指定为“Cheng”搜不到;

<4>如果录入name字段的索引为”Cheng hui“  关键字指定为“Cheng”搜不到;

<5>如果录入name字段的索引为”Cheng hui“  关键字指定为“cheng”可以搜到;


由此可见: 索引录入的时候会统一转换成小写,但是关键字 没有转换成小写去匹配,这才导致大小写匹配不到的情况。

所以英文与数字和中文一样 不是那么人性化,并且我检索的时候连字母的大小写都没有区分,同时 数字 也是一样子的问题,   可见Lucene对其搜索的不完善性。

有没有解决方案呢? 有,上面例子使用的是SimpleAnalyzer 分词器,这个分词器是一段一段话进行分,现在介绍另外一个分词器StandardAnalyzer分词器,标准分词拿来分中文和ChineseAnalyzer一样的效果。

把上述代码中传递的分词器对象换成 new StandardAnalyzer(Version.LUCENE_36) ,试试效果,发现 StandardAnalyzer 在 SimpleAnalyzer 的基础上进行了优化,遗憾的是只是中文方面的,比如:

<1>如果录入sex索引为”男的“  关键字指定为“男” 可以搜到;

但是仅仅这样,远远不能满足全文检索的需求,这就需要使用更加高级的分词器来实现该功能。

Apache Maven 入门篇

 MAVEN  Apache Maven 入门篇已关闭评论
8月 292013
 

写这个 maven 的入门篇是因为之前在一个开发者会的动手实验中发现挺多人对于 maven 不是那么了解,所以就有了这个想法。

这个入门篇分上下两篇。本文着重动手,用 maven 来构建运行 hellow world 程序,体会一下不用任何 IDE ,只用 maven 是咋回事。然后下篇就讲解一下 maven 的核心概念。写这两篇文章特意回避了复杂的示例,也不使用 IDE ,目的是排除干扰,着重于 maven 本身。

本文的源代码可从这里下载。  

Apache Maven 是做什么用的?

 

Maven 是一个项目管理和构建自动化工具。但是对于我们程序员来说,我们最关心的是它的项目构建功能。所以这里我们介绍的就是怎样用 maven 来满足我们项目的日常需要。

Maven 使用惯例优于配置的原则 。它要求在没有定制之前,所有的项目都有如下的结构:

 

目录

目的

${basedir}

存放 pom.xml和所有的子目录

${basedir}/src/main/java

项目的 java源代码

${basedir}/src/main/resources

项目的资源,比如说 property文件

${basedir}/src/test/java

项目的测试类,比如说 JUnit代码

${basedir}/src/test/resources

测试使用的资源

 

一个 maven 项目在默认情况下会产生 JAR 文件,另外 ,编译后 的 classes 会放在 ${basedir}/target/classes 下面, JAR 文件会放在 ${basedir}/target 下面。

这时有人会说了 , Ant 就没有那么多要求 ,它允许你可以自由的定义项目的结构。在这里不想引起口水战哈, 我个人觉得 maven 的这些默认定义很方便使用。 

好了 ,接下来我们来安装 maven 。

 

Maven 的安装

 

在安装 maven 前,先保证你安装了 JDK 。 JDK 6 可以从 Oracle 技术网上下载:

http://www.oracle.com/technetwork/cn/java/javase/downloads/index.html

Maven 官网的下载链接是 : http://maven.apache.org/download.html 。

该页的最后给出了安装指南。

 

安装完成后,在命令行运行下面的命令:  

   


 

$ mvn -v 

Apache Maven 3.0.3 (r1075438; 2011-03-01 01:31:09+0800)

Maven home: /home/limin/bin/maven3

Java version: 1.6.0_24, vendor: Sun Microsystems Inc.

Java home: /home/limin/bin/jdk1.6.0_24/jre

Default locale: en_US, platform encoding: UTF-8

OS name: "linux", version: "2.6.35-28-generic-pae", arch: "i386", family: "unix"





如果你看到类似上面的输出的话,就说明安装成功了。

接下来我们用 maven 来建立最著名的“Hello World!”程序 🙂

注意:如果你是第一次运行 maven,你需要 Internet 连接,因为 maven 需要从网上下载需要的插件。

我们要做的第一步是建立一个 maven 项目。在 maven 中,我们是执行 maven 目标 (goal) 来做事情的。

maven 目标和 ant 的 target 差不多。在命令行中执行下面的命令来建立我们的 hello world 项目

 


 

  ~$mvn archetype:generate -DgroupId=com.mycompany.helloworld -DartifactId=helloworld -Dpackage=com.mycompany.helloworld -Dversion=1.0-SNAPSHOT



archetype:generate 目标会列出一系列的 archetype 让你选择。 Archetype 可以理解成项目的模型。 Maven 为我们提供了很多种的项目模型,包括从简单的 Swing 到复杂的 Web 应用。我们选择默认的 maven-archetype-quickstart ,是编号 #106 ,如下图所示:

 

apache maven image1

 

 连打两个回车,这时候让你确定项目属性的配置,

 

apache maven image2

  

这些属性是我们在命令行中用 -D 选项指定的。该选项使用 -Dname=value 的格式。回车确认,就完成了项目的建立,如下图所示:

 

apache maven image3

  

这时候我们看一下 maven 给我们建立的文件目录结构:

apache maven image4





maven 的 archetype 插件建立了一个 helloworld 目录,这个名字来自 artifactId 。在这个目录下面,有一个 Project Object Model(POM) 文件 pom.xml 。这个文件用于描述项目,配置插件和管理依赖关系。

源代码和资源文件放在 src/main 下面,而测试代码和资源放在 src/test 下面。

Maven 已经为我们建立了一个 App.java 文件:

   

Java代码

    package com.mycompany.helloworld;   
      
    /**  
     * Hello world!  
     *  
     */   
    public class App {   

      
        public static void main( String[] args ) {   
            System.out.println( "Hello World!" );   
        }   
    }  

 

 正是我们需要的 Hello World 代码。所以我们可以构建和运行这个程序了。用下面简单的命令构建:


 

~$cd helloworld

~$mvn package 

当你第一次运行 maven 的时候,它会从网上的 maven 库 (repository) 下载需要的程序,存放在你电脑的本地库 (local repository) 中,所以这个时候你需要有 Internet 连接。Maven 默认的本地库是 ~/.m2/repository/ ,在 Windows 下是 %USER_HOME%.m2repository 。

 apache maven image5



 如果构建没有错误的话,就会得到类似下面的结果:

 

apache maven image6

 

 这个时候, maven 在 helloworld 下面建立了一个新的目录 target/ ,构建打包后的 jar 文件 helloworld-1.0-SNAPSHOT.jar 就存放在这个目录下。编译后的 class 文件放在 target/classes/ 目录下面,测试 class 文件放在 target/test-classes/ 目录下面。

 为了验证我们的程序能运行,执行下面的命令:

 ~$java -cp target/helloworld-1.0-SNAPSHOT.jar com.mycompany.helloworld.App

 

apache maven image8

 

运行成功!!

 

现在你可能会有不少的问题。所以下一篇文章会解释 maven 的核心概念,希望能回答你可能会有的一些疑问。

 

接下来我们介绍下面这几个核心概念:

  • POM (Project Object Model)
  • Maven 插件
  • Maven 生命周期
  • Maven 依赖管理
  • Maven 库

 

POM (Project Object Model)

 

一个项目所有的配置都放置在 POM 文件中:定义项目的类型、名字,管理依赖关系,定制插件的行为等等。比如说,你可以配置 compiler 插件让它使用 java 1.5 来编译。

现在看一下第一篇文章中示例的 POM

Xml 代码

        <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

     <modelVersion>4.0.0</modelVersion> 



     <groupId>com.mycompany.helloworld</groupId> 

     <artifactId>helloworld</artifactId> 

     <version>1.0-SNAPSHOT</version> 

     <packaging>jar</packaging> 



     <name>helloworld</name> 

     <url>http://maven.apache.org</url> 



     <properties> 

       <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 

     </properties> 



     <dependencies>

       <dependency> 

         <groupId>junit</groupId> 

         <artifactId>junit</artifactId> 

         <version>3.8.1</version> 

         <scope>test</scope> 

       </dependency> 

     </dependencies> 

    </project>    

 

在 POM 中,groupId, artifactId, packaging, version 叫作 maven 坐标,它能唯一的确定一个项目。有了 maven 坐标,我们就可以用它来指定我们的项目所依赖的其他项目,插件,或者父项目。一般 maven 坐标写成如下的格式:

    	groupId:artifactId:packaging:version

像我们的例子就会写成:

    	com.mycompany.helloworld: helloworld: jar: 1.0-SNAPSHOT

我们的 helloworld 示例很简单,但是大项目一般会分成几个子项目。在这种情况下,每个子项目就会有自己的 POM 文件,然后它们会有一个共同的父项目。这样只要构建父项目就能够构建所有的子项目了。子项目的 POM 会继承父项目的 POM。另外,所有的 POM都继承了一个 Super-POM。Super-POM 设置了一些默认值,比如在第一篇文章中提到的默认的目录结构,默认的插件等等,它遵循了惯例优于配置的原则。所以尽管我们的这个 POM 很简单,但是这只是你看得见的一部分。运行时候的 POM 要复杂的多。 如果你想看到运行时候的 POM 的全部内容的话,可以运行下面的命令:

$mvn help:effective-pom

Maven 插件

 

第一篇文章中,我们用了 mvn archetype:generate 命令来生成一个项目。那么这里的 archetype:generate 是什么意思呢?archetype 是一个插件的名字,generate是目标(goal)的名字。这个命令的意思是告诉 maven 执行 archetype 插件的 generate 目标。插件目标通常会写成pluginId:goalId

一个目标是一个工作单元,而插件则是一个或者多个目标的集合。比如说Jar插件,Compiler插件,Surefire插件等。从看名字就能知道,Jar 插件包含建立Jar文件的目标, Compiler 插件包含编译源代码和单元测试代码的目标。Surefire 插件的话,则是运行单元测试。

看到这里,估计你能明白了,mvn 本身不会做太多的事情,它不知道怎么样编译或者怎么样打包。它把构建的任务交给插件去做。插件定义了常用的构建逻辑,能够被重复利用。这样做的好处是,一旦插件有了更新,那么所有的 maven 用户都能得到更新。

Maven 生命周期

 

上一篇文章中,我们用的第二个命令是:mvn package。这里的 package 是一个maven的生命周期阶段 (lifecycle phase )。生命周期指项目的构建过程,它包含了一系列的有序的阶段 (phase),而一个阶段就是构建过程中的一个步骤。

那么生命周期阶段和上面说的插件目标之间是什么关系呢?插件目标可以绑定到生命周期阶段上。一个生命周期阶段可以绑定多个插件目标。当 maven 在构建过程中逐步的通过每个阶段时,会执行该阶段所有的插件目标。

maven 能支持不同的生命周期,但是最常用的是默认的Maven生命周期 (default Maven lifecycle )。如果你没有对它进行任何的插件配置或者定制的话,那么上面的命令 mvn package 会依次执行默认生命周期中直到包括 package 阶段前的所有阶段的插件目标:

  1. process-resources 阶段:resources:resources

  2. compile 阶段:compiler:compile

  3. process-classes 阶段:(默认无目标)

  4. process-test-resources 阶段:resources:testResources

  5. test-compile 阶段:compiler:testCompile

  6. test 阶段:surefire:test

  7. prepare-package 阶段:(默认无目标)

  8. package 阶段:jar:jar

     

Maven 依赖管理

 

之前我们说过,maven 坐标能够确定一个项目。换句话说,我们可以用它来解决依赖关系。在 POM 中,依赖关系是在 dependencies 部分中定义的。在上面的 POM 例子中,我们用 dependencies 定义了对于 junit 的依赖:

Xml 代码

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

那这个例子很简单,但是实际开发中我们会有复杂得多的依赖关系,因为被依赖的 jar 文件会有自己的依赖关系。那么我们是不是需要把那些间接依赖的 jar 文件也都定义在POM中呢?答案是不需要,因为 maven 提供了传递依赖的特性。

所谓传递依赖是指 maven 会检查被依赖的 jar 文件,把它的依赖关系纳入最终解决的依赖关系链中。针对上面的 junit 依赖关系,如果你看一下 maven 的本地库(我们马上会解释 maven 库)~/.m2/repository/junit/junit/3.8.1/ ,



part2-1

你会发现 maven 不但下载了 junit-3.8.1.jar,还下载了它的 POM 文件。这样 maven 就能检查 junit 的依赖关系,把它所需要的依赖也包括进来。

在 POM 的 dependencies 部分中,scope 决定了依赖关系的适用范围。我们的例子中 junit 的 scope 是 test,那么它只会在执行 compiler:testCompile and surefire:test 目标的时候才会被加到 classpath 中,在执行 compiler:compile 目标时是拿不到 junit 的。

我们还可以指定 scope 为 provided,意思是 JDK 或者容器会提供所需的jar文件。比如说在做web应用开发的时候,我们在编译的时候需要 servlet API jar 文件,但是在打包的时候不需要把这个 jar 文件打在 WAR 中,因为servlet容器或者应用服务器会提供的。

scope 的默认值是 compile,即任何时候都会被包含在 classpath 中,在打包的时候也会被包括进去。

 

Maven 

 

当第一次运行 maven 命令的时候,你需要 Internet 连接,因为它要从网上下载一些文件。那么它从哪里下载呢?它是从 maven 默认的远程库(http://repo1.maven.org/maven2) 下载的。这个远程库有 maven 的核心插件和可供下载的 jar 文件。

但是不是所有的 jar 文件都是可以从默认的远程库下载的,比如说我们自己开发的项目。这个时候,有两个选择:要么在公司内部设置定制库,要么手动下载和安装所需的jar文件到本地库。

本地库是指 maven 下载了插件或者 jar 文件后存放在本地机器上的拷贝。在 Linux 上,它的位置在 ~/.m2/repository,在 Windows XP 上,在 C:Documents and Settingsusername.m2repository ,在 Windows 7 上,在 C:Usersusername.m2repository。当 maven 查找需要的 jar 文件时,它会先在本地库中寻找,只有在找不到的情况下,才会去远程库中找。

运行下面的命令能把我们的 helloworld 项目安装到本地库:

     $mvn install

一旦一个项目被安装到了本地库后,你别的项目就可以通过 maven 坐标和这个项目建立依赖关系。比如如果我现在有一个新项目需要用到 helloworld,那么在运行了上面的 mvn install 命令后,我就可以如下所示来建立依赖关系:

Xml 代码

    <dependency>
      <groupId>com.mycompany.helloworld</groupId>
      <artifactId>helloworld</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>

好了,maven 的核心概念就简单的介绍到这里。至于在 Eclipse 中如何使用 maven,这个网上很多了,google 一下就行。

 

转自:http://www.oracle.com/technetwork/cn/community/java/apache-maven-getting-started-1-406235-zhs.html

 Posted by at 下午6:19  Tagged with: