一. maven是什么?
maven这个词可以翻译成“专家”或“内行”,也可以翻译成“知识的积累”(犹太语),是一个跨平台的项目管理工具。作为Apache组织中的一个颇为成功的开源项目,maven主要服务于基于java平台的项目构建、依赖管理和项目信息管理。
二. maven都能做什么?
举几个例子直观感受一下:
a. 我们需要引用各种jar包,尤其是较大的工程,引用的jar包往往有几十个乃至上百个,都需要手动引入工程目录,而且经常遇到各种让人抓狂的jar包冲突,版本冲突。
b. 世界上没有不存在bug的代码,就像没有不喜欢美女的男人一样。写完了代码,我们还要写一些单元测试,然后一个个的运行来检验代码质量。
c. 再优雅的代码也是要出来卖的。我们后面还需要把代码与各种配置文件、资源整合到一起,定型打包。如果是web项目,还需要将之发布到服务器,供人蹂躏。
以上这些,maven都可以做到。
1.maven是优秀的构建工具
前面介绍了maven的用途之一是服务于构建,它是一个异常强大的构建工具,能够帮我们自动化构建过程,从清理、编译、测试到生成报告, 再到打包和部署。我们不需要也不应该一遍又一遍地输入命令,一次又一次地点击鼠标,我们要做的是使用maven配置好项目,然后输入 简单的命令(如mvn clean install),maven会帮助我们处理那些繁琐的任务。maven是跨平台达到,这意味着无论是在Windows上,还是Linux 或者Mac上,都可以使用同样的命令。
2.maven不仅是构建工具
maven不仅是构建工具,还是一个依赖管理工具和项目信息管理工具。它提供了中央仓库,能帮助我们自动下载构件。在这个开源的年代里, 几乎任何的java应用都会借用一些第三方的开源类库,这些类库都可以通过依赖的方式引入到项目中来。maven提供了一个优秀的解决方案, 它通过一个坐标系统准确地定为每一个构件,也就是通过一组坐标maven能够找到任何一个java类库(如jar文件)。
maven还能帮助我们管理原本分散在项目中各个角落的项目信息,包括项目描述、开发者列表、版本控制系统地址、许可证、缺陷管理系统地址等。
三. maven入门
1.编写POM
maven项目的核心是pom.xml。POM(Project Object Model ,项目对象模型)定义了项目的基本信息,用于描述项目如何构建,声明项目依赖等等。
代码的第一行是XML头,指定了该xml文档的版本和编码方式。
紧接着是project元素,project是所有pom.xml的根元素,他还声明了一些POM相关的命名空间及xsd元素,这些属性不是必须的,但使用这些属性能够让第三方工具(如IDE中的XML编辑器)帮助我们快速编辑POM。
modelVersion指定了当前POM模型的版本,对于maven2和maven3来说,它只能是4.0.0。
最重要的事groupId、artifactId、version。这三个元素定义了一个项目基本的坐标,在maven的世界,任何的jar、pom或者war都是以基于这些 基本的坐标进行区分的。
groupId定义了项目属于哪个组
artifactId定义了当前maven项目在组中的唯一ID
version指定了当前demo项目的版本SNAPSHOT意为快照,说明该项目还处于开发中,是不稳定的版本。
name元素声明了一个对于用户更为友好的项目名称,不是必须的。但推荐为每个pom声明name,方便信息交流。
2.编写主代码
项目主代码和测试代码不同,主代码会被打包到最终的构件中(如jar),而测试代码只在运行测试时用到,不会被打包。 遵循maven的约定,在绝大多数情况下,应该把项目的主代码放到src/main/java目录下,而无需额外的配置,maven会自动搜寻该目录 找到项目主代码。
使用maven进行编译,在项目的根目录下运行命令 mvn clean compile:
从输出中看到maven首先执行了clean任务,接着执行resources任务,最后执行compile任务。对应了一些maven插件和插件目标。 至此,maven在没有任何额外配置的情况下就执行了项目的清理和编译任务,接下来编写一些单元测试代码并让maven执行自动化测试。
3.编写测试代码
主代码和测试代码应该分别位于独立的目录中。maven项目中默认的主代码目录是src/main/java,对应的,maven项目中默认的测试代码目录是 src/test/java。
在java世界中,由Kent Beck和Erich Gamma建立的JUnit是事实上的单元测试标准。要使用JUnit,首先要为Hello World项目添加一个JUnit依赖, 修改项目的pom如下图:
这里添加了一个依赖——groupId是junit,artifactId是junit,version是4.7。前面提到groupId,artifactId,version是任何一个maven项目最基本的坐标。 有了这段声明,maven就能自动下载junit-4.7.jar。它会自动访问中央仓库,下载需要的文件。
scope元素为依赖范围,若依赖范围为test,则表示该依赖只对测试代码有效。换句话说,测试代码中的import JUnit代码是没有问题的,但是如果主代码中用import JUnit就会造成编译错误。如果不声明依赖范围,默认值是compile,表示该依赖对主代码和测试代码都有效。
以下为HelloWorld的测试代码:
在junit4中,需要执行的测试方法都应该以@Test进行标注。调用maven进行测试,运行 mvn clean test:
分析一下这段输出:命令行输入的是mvn clean test,而maven实际先执行的clean、resources、compile、testResources。暂时需要了解的是,在maven执行测试之前,他会先自动执行项目主资源处理、主代码编译、测试资源处理、测试代码编译等工作,这是maven生命周期的一个特性。后续讲解。可以看到,我们测试任务执行成功了。
4.打包和运行
将项目进行编译、测试后,下一个重要步骤就是打包。HelloWorld的POM中没有指定打包类型,使用默认打包类型jar。简单的执行命令mvn clean package 进行打包,可以看到如下输出:(简略版)
类似的,maven会在打包之前执行编译、测试等操作。这里看到--- maven-jar-plugin:2.4:jar (default-jar) @ mavendemo ---,实际上就是jar插件的jar目标将项目主代码打包成一个名为mavendemo-1.0-SNAPSHOT.jar的文件。
至此,打包成功,如果有需要的话就可以复制这个jar文件到其他项目的Classpath中从而使用HelloWorld类。但是,如何才能让其他的maven项目直接引用这个jar呢?还需要一个安装的步骤,执行 mvn clean install:
在打包之后,又执行了安装任务install:install。从输出中可以看到该任务将项目输入的jar安装到了maven本地仓库。我们说只有构件被下载到本地仓库后,才能被所有maven项目使用。
我们已经体验了maven最主要的命令:mvn clean compile,mvn clean test,mvn clean package,mvn clean install。执行test之前先执行的是compile,执行package之前会先执行test,执行install之前会先执行package。
注意到目前为止,还没有运行HelloWorld项目,不要忘了HelloWorld类可是有一个main方法的。默认打包生成的jar是不能够直接运行的,因为带有main方法的类信息不会添加到manifest中(打开jar文件中的META-INF/MANIFEST.MF文件,将无法看到Main-Class一行)。为了生成可执行的jar文件,需要借助maven-shade-plugin,在POM中配置该插件如下:
然后执行mvn clean install,构建完成后打开target/目录,可以看到mavendemo-1.0-SNAPSHOT.jar和original-mavendemo-1.0-SNAPSHOT.jar,前者是可运行的jar,后者是原始的jar。
现在在项目根目录下执行该jar文件:java -jar target/maven_demo-1.0-SNAPSHOT.jar
控制台输出:Hello Maven
到这,我们介绍了POM、maven项目结构及如何编译、测试、打包等。
四. 坐标和依赖
1.坐标
maven定义了这样一组规则:世界上任何一个构件都可以使用maven坐标唯一标识,maven坐标的元素包括groupId,artifactId,version,packaging,classifIer。我们只需要提供正确的坐标,maven就能找到对应的构件。从哪找呢?先看一组坐标定义:
groupId:定义当前maven项目隶属的实际项目。
artifactId:该元素定义实际项目中的一个maven项目(模块)。默认情况下,maven生成的构件,其文件名以artifactId作为开头,如spring-data-mongodb.jar;
version:该元素定义maven项目当前所属的版本。
packaging:该元素定义了maven项目的打包方式。上述的例子为jar,所有最终的文件名为spring-data-mongodb-2.1.5RELEASE.jar,而是用war打包方式的maven项目,最终生成的构件会有一个.war文件 ,不过这不是绝对的。默认packaging的值为jar。
classifier:该元素用来帮助定义构建输出的一些附属构件。附属构件与主构件对应,如名为A-2.0.0.jar的构件,可能还会使用一些插件生成如A-2.0.0-javadoc.jar、A-2.0.0-sources.jar这样一些附属构件。 这里注意:不能直接定义项目的classifier,因为附属构件不是项目默认生成的,而是由附加的插件帮助生成。
2.传递性依赖
假设你正在开发一个Spring Framework的项目,如果不用maven,那么在项目中就需要手动下载相关依赖。由于Spring Framework又会依赖其他开源类库,因此实际中往往会下载一个很大的如spring-framework-2.5.6-with-dependencies.zip的包,这里包含了所有Spring Framework的jar包,以及它依赖的其他jar包,这么做往往就引入了很多不必要的依赖。另一种做法是只下载spring-framework-2.5.6.zip这样一个包,这里不包含其他相关依赖,到实际使用的时候,再根据出错信息,或者查询相关文档,加入其他依赖。很显然,这是一个很麻烦的事情。
maven的传递性依赖可以很好地解决这一问题。假设A有一个compile范围的B依赖,B有一个compile范围的C依赖,那么我们说A对于B是第一直接依赖,B对于C是第二直接依赖,A对于C是传递性依赖。第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围。
maven引入的传递性依赖机制,大大简化和方便了依赖声明,另一方面在大部分情况下我们只需要关心项目的直接依赖是什么,而不用考虑这些直接依赖会引入什么传递性依赖。
3.依赖调节
当传递性依赖造成问题的时候,我们就需要清楚的知道该传递性依赖是从哪条依赖路径引入的。
例如项目A有这样的依赖关系:A -> B -> C -> X(1.0)、A -> D -> X(2.0),X是A的传递性依赖,但是两条依赖路径上有两个版本的X,那么哪个X会被maven解析使用呢?两个版本都使用显然是不对的,因为那会造成依赖重复。
maven依赖调解的第一原则是:路径最近者优先。该例中X(1.0)的路径为3,X(2.0)的路径长度为2,因此X(2.0)会被解析使用。
现在假设有这样的依赖关系:A -> B -> Y(1.0),A -> B -> Y(2.0),Y(1.0)和Y(2.0)的路径长度都是2,那到底谁会被解析使用呢?
从maven2.0.9开始,为了尽可能避免构建的不确定性,maven定义了依赖调解的第二原则:第一声明者优先。在依赖路径长度相等的前提下,在pom中依赖声明的顺序决定了谁会被调解使用,顺序最靠前的依赖被使用。
五.仓库
对于maven仓库来说,仓库只分为两类:本地仓库和远程仓库。当maven根据坐标寻找构件的时候,他首先会查看本地仓库,如果本地仓库存在此构件,则直接使用;如果不存在,或者需要查看是否有更新的构件版本,maven就会去远程仓库查找,发现需要的构件之后,下载到本地仓库在使用。如果本地仓库和远程仓库都没有需要的构件,maven就会报错。
介绍一些特殊的远程仓库:中央仓库是maven核心自带的远程仓库,它包含了绝大部分开源拿到构件。在默认配置下,当本地仓库没有maven需要的构件的时候,他会尝试从中央仓库下载。
私服是另一种远程仓库,为了节省带宽和时间,应该在局域网内架设一个私有的仓库服务器,用其代理所有外部的远程仓库。内部的项目还能部署到私服上供其他项目使用。除了中央仓库和私服,还有很多其他公开的远程仓库,不一一介绍了。
1.本地仓库
当maven执行编译或测试时,如果需要依赖文件,他会基于坐标使用本地仓库的依赖文件。默认情况下,不管是在Win上还是Linux上,每个用户在自己的用户目录下都有一个路径名为.m2/repository/的仓库目录。
自定义本地仓库目录地址
编辑~/.m2/settings.xml,设置localRepository元素的值为想要的仓库地址。需要注意的是,默认情况下,~/.m2/settings.xml文件是不存在的,用户需要从maven安装目录复制$M2_HOME/conf/settings.xml文件再进行编辑。
2.远程仓库
安装好maven后,如果不执行任何maven命令,本地仓库目录是不存在的。当用户输入第一条maven命令之后,mavne才会创建本地仓库,然后根据配置和需要,从远程仓库下载构件至本地仓库。
这好比藏书。我想看《程序员修炼之道》,我会先看看自己的书房是否有这本书。如果没有,那么我就会去书店买,买回来放到书房里。可能有一天我又想读英文版的了,书房里只有中文版,于是又去书店找,发现书店没有,然后我又在网上找,最后在京东买了一本,放到自己的书房。
书房就好比本地仓库。maven需要构件的时候先从本地仓库找。远程仓库就好比书店,当我无法从自己的书房里找到需要的书的时候,就会从书店买回来放到书房里。当maven无法从本地仓库找到需要的构件的时候,就会从远程仓库下载构件至本地仓库。一般来讲,对于每个人来说书房只有一个,但外面的书店有很多。类似的,对于maven来说,每个用户的本地仓库只有一个,但可以配置访问很多远程仓库。
3.中央仓库
由于最开始本地仓库是空的,maven必须知道至少一个可用的远程库,才能在执行maven命令的时候下载到需要的构件。中央仓库就是这样一个默认的远程仓库。中央仓库包含了这个世界上绝大多数流行的开源java构件,一般来说,一个简单maven项目所需要的依赖构件都可以从中央仓库下载到。这也解释了为什么maven能做到”开箱即用“。
4.私服
私服是一种特殊的远程仓库,它是架设在局域网内的仓库服务,私服代理广域网上的远程仓库,供局域网内的maven用户使用。当maven需要下载构件的时候,他从私服请求,如果私服上不存在此构件,则从外部的远程仓库下载,缓存在私服上之后,再为maven的下载请求提供服务。此外,一些无法从外部仓库下载到的构件也能从本地上传到私服上供大家使用。
私服的好处:
节省自己的外网带宽
加速maven构建
部署第三个构件
提高稳定性,增强控制
降低中央仓库的负荷
5.远程仓库的配置
有时,默认的中央仓库无法满足项目的需求,可能项目的构件在另一个远程仓库中,可以在pom中配置该仓库。如下:
该例中声明了一个id为jboss,名称为JBoss Repository的仓库。任何一个仓库的id必须是唯一的,尤其需要注意的是,maven自带的中央仓库使用的id为central,如果其他的仓库声明也用了这个id,就会覆盖中央仓库的配置。url为仓库地址。
该例子配置中的releases和snapshots元素比较重要,他们用来控制maven对于发布版构件和快照版构件的下载。releases的enabled值为true,表示开启JBoss仓库的发布版本下载支持,而snapshots的enabled值为false,表示关闭JBoss仓库的快照版本的下载支持。因此,根据该配置,maven只会从JBoss仓库下载发布版的构件,而不会下载快照版的构件。
6.远程仓库认证
假设组织内部有一个maven仓库服务器,该服务器为每个项目都提供独立的maven仓库,为了防止非法的仓库访问,管理员为每个仓库提供了一组用户名及密码。这是为了让maven访问仓库内容,就需要配置认证信息。
假设需要给一个id为my-proj的仓库配置认证信息,需要在settings.xml文件中配置:
重点:此id必须与pom中需要认证的repository元素的id完全一致。换句话说,正是这个id将认证信息与仓库配置联系在了一起。
7.部署至远程仓库
上文提到,私服的一大作用是部署第三方构件,包括组织内生成的构件,以及一些无法从外部仓库直接获取的构件。无论是日常开发中生成的构件,还是版本发布的构件,都需要配置到仓库中,供其他团队成员使用。
首先要编辑pom.xml文件,配置distributionManagement元素:
repository表示发布版本构件的仓库,snapshotRepository表示快照版本的仓库。id为该远程仓库的唯一标识,name是为了方便人阅读,关键的url表示该仓库的地址。
往远程仓库部署构件的时候,需要认证。和刚才一样,需要在settings.xml中创建一个server,其id与仓库的id一样。无论从远程仓库下载构件,还是部署构件至远程仓库,当需要认证的时候,配置的方式是一样的。
配置正确后,在命令行运行mvn clean deploy,maven就会将项目构建输出的构件部署到配置对应的远程仓库。
8.快照版本
maven为什么要区分稳定版本和快照版本?
试想一下:假设小张在开发模块A的2.1版本,该版本还未正式发布,与A一下开发的还有模块B,由小王开发,B的功能依赖于A。在开发的过程中,小张需要经常将项目构建、输出,交给小王,供她开发和集成调试。
这种情况下,小张只需要将模块的版本设定为2.1-SNAPSHOT,然后发布到私服中,再发布的过程中,maven会自动为构件打上时间戳。有了时间戳,maven就能随时找到仓库中该构件.1-SNAPSHOT版本最新的文件。基于快照版本机制,小张在构建成功之后才能将构件部署至仓库,而小王完全可以不用考虑模块A的构建,因为她能确保随时得到模块A的最新可用的快照构件。
在项目经过完善的测试后需要发布的时候,就应该将快照版本更改为发布版本。
六.生命周期
初学者往往会认为maven的生命周期是一个整体,其实不然,maven有三套相互独立的生命周期,分别为clean、default、site。
clean生命周期的目的是清理项目,它包含三个阶段:
pre-clean 执行一些清理前需要完成的任务。
clean 清理上一次构建生成的文件
post-clean 执行一些清理后需要完成的工作
default生命周期定义了真正构建时所需要执行的所有步骤,它是所有生命周期中最核心的部分,这里只对重要阶段进行解释:
compile 编译项目的主源码
test-compile 编译项目的测试代码
test 使用单元测试框架运行测试,测试代码不会被打包和部署
package 接受编译好的代码,打包成可发布的格式,如JAR
install 将包安装到maven本地仓库中,供本地其他maven项目使用
dapley 将最终的包复制到远程仓库,供其他开发人员和maven项目使用。
site生命周期
site生命周期的目的是建立和发布项目站点,maven能够基于pom所包含的信息,自动生成一个友好的站点,方便团队交流和发布项目信息。该生命周期包含如下阶段:
pre-site 执行一些在生成项目站点之前需要完成的工作
site 生成项目站点文档
post-site 执行一些在生成项目站点之后需要完成的工作
site-deploy 将生成的项目站点发布到服务器上