1.背景
Maven的聚合特性能够把项目的各个模块聚合在一起构建,而Maven的继承特性则能帮助抽取各个模块相同的依赖和插件等配置。在简化POM的同时,还能促进各个模块配置的一致性。
一般来说,一个项目的子模块都应该使用同样的groupId,如果它们一起开发和发布,还应该使用同样的version,此外,它们的atrifactId还应该使用一致的前缀,以方便区分其他项目。
项目主代码:src/main/java
项目资源文件:src/main/resources
测试代码:src/test/java
测试资源文件:src/test/resources
2.聚合
一次构建两个项目,而不是到两个模块的目录下分别执行mvn命令,为了能一条命令就构建两个模块,需要创建一个额外的模块,然后通过该模块构建整个项目的所有模块。
4.0.0 com.gqshao.myapp aggregator 1.0.0-SNAPSHOT Project Aggregator pom project-a project-b
(1)上述POM中groupId、version与其他项目相同,artifactId不同;
(2)packaging:必须采用特殊的pom,而不是不进行声明,采用默认的jar。对于聚合模块来说,其打包方式packaging的值必须为pom,否则无法构建;
(3)modules:是实现聚合的最核心配置,这里每个module的值都是一个当前POM的相对目录,推荐模块所处目录名称应当与其artifactId一致;
(4)为了方便用户构建项目,通常将聚合模块放在项目目录的最顶层,其他模块则作为聚合模块的子目录存在;
(5)可以采用平行的目录结构,此时聚合模块的POM应该修改为<module>../project-a</module>
在执行mvn clean package命令后,Maven会首先解析聚合模块的POM、分析要构建的模块、并计算出一个反应堆构建顺序(Reactor Build Order),然后根据这个顺序依次构建各个模块。反应堆是所有模块组成的一个构建结构。
反应堆构建顺序描述输出的是各模块的名称,而不是artifactId,所以在POM配置合理的name字段,其目的是让Maven的构建输出更清晰。
3.继承
背景:多个模块的pom.xml中有很多相同的配置,例如它们有相同的groupId和version,有相同的spring-core、spring-beans,还有相同的maven-compiler-plugin等配置。
在aggregator下创建parent子目录,然后在该目录下建立一个所有除aggregator之外模块的父模块。为此在parent目录下创建pom.xml,内容如下:
4.0.0 com.gqshao.myapp parent 1.0-SNAPSHOT parent pom UTF-8
该POM十分简单,使用了与其他模块一直的groupId和version,使用artifactId为parent表示这是一个父模块,作为父模块,其打包类型也必须为pom
由于父模块只是为了帮助消除配置的重复,因此它本身不包含除POM之外的项目文件,也就不需要src/main/java之类的文件夹了。
让其它模块继承它,project-a和project-b的POM修改类似于下面这种形式
4.0.0 com.gqshao.myapp parent 1.0-SNAPSHOT ../parent或../parent/pom.xml project-a或project-b project-a ... ...
上述POM中使用parent元素声明父模块,parent下的子元素gourpId、artifactId和version指定了父模块的坐标,这三个元素是必须的。元素relativePath为../parent/(可以不写pom.xml,默认值../pom.xml),也就是说Maven默认父POM在上一层目录下。
最后,同样还需要把parent加入到聚合模块aggregator中
4.0.0 com.gqshao.myapp aggregator 1.0-SNAPSHOT aggregator http://maven.apache.org pom UTF-8 parent project-a project-b
(2)可继承的元素
(1)groupId:项目组ID,项目坐标的核心元素;
(2)version:项目版本,项目坐标的核心元素;
(3)description:项目的描述信息;
(4)organization:项目的组织信息;
(5)inceptionYear:项目的创伤年份;
(6)url:项目的URL地址;
(7)developers:项目的开发者信息;
(8)contributors:项目的贡献值信息;
(9)distributionManagement:项目的部署配置;
(10)issueManagement:项目的缺陷跟踪系统信息;
(11)ciManagement:项目的持续集成系统信息;
(12)scm:项目的版本控制系统信息;
(13)mailingLists:项目的邮件列表信息;
(14)properties:自定义的属性,重要;
(15)dependency:项目的依赖配置,重要;
(16)dependencyManagement:项目的依赖管理配置,重要;
(17)repositories:项目的仓库配置;
(18)build:包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等,重要;
(19)reporting:包括项目的报告输出目录配置、报告插件配置等。
(3)依赖管理
Maven提供的dependencyManagement元素既能让子模块继承到父模块的依赖配置,又能保证子模块依赖使用的灵活性。在dependencyManagement元素下的依赖声明不会引入实际的依赖,不过它能够约束dependencies下的依赖使用。
4.0.0 com.gqshao.myapp parent 1.0-SNAPSHOT parent pom UTF-8 2.5.6 4.7 org.springframework spring-core ${springframework.version} org.springframework spring-beans ${springframework.version} org.springframework spring-context ${springframework.version} org.springframework spring-context-support ${springframework.version} junit junit ${junit.version} test org.apache.maven.plugins maven-compiler-plugin org.apache.maven.plugins maven-resources-plugin UTF-8
首先该父POM,将springframework和junit依赖的版本以Maven变量的形式提取了出来,消除重复,并使各依赖的版本处于更加明显的位置。
这里使用的dependencyManagement既不会给parent引入依赖,也不会给它的子模块引入依赖,不过这段配置是会被继承的,现在修改project的POM如下:
4.0.0 com.gqshao.myapp parent 1.0-SNAPSHOT ../parent/pom.xml project-a project-a UTF-8 1.4.1 1.3.1b org.springframework spring-core org.springframework spring-beans org.springframework spring-context org.springframework spring-context-support junit junit javax.mail ${javax.mail.version} com.icegreen greenmail ${greenmail.version} test
上述的POM所有springframework依赖只配置了groupId和artifactId,省去了version,而junit依赖不仅省去了version,还省去了依赖范围scope。这是由于完整的依赖声明已经包含在父POM中,子模块只需要配置简单的groupId和artifactId就能获得对应的依赖,从而引入正确的依赖。这种方式是强烈推荐采用的。
如果子模块不声明依赖的使用,即使该依赖已经在父POM的dependencyManagement中声明了,也不会产生任何实际的效果。
依赖范围import:导入依赖范围,该依赖范围不会对三种classpath产生时间影响,该依赖范围只有在dependencyManagement元素下才 有效果,使用该范围的依赖通常指向一个POM,作用是将该POM目标中的dependencyManagement合并到当前POM的 dependencyManagement中。即在另一个模块中使用与另一个项目完全一样的dependencyManagement,除了复制配置或者 继承这两种方式外,还可以考虑import范围依赖将这一配置导入。
com.gqshao.myapp myapp-parent 1.0-SNAPSHOT pom import
注意:上述代码中依赖的type值为pom,import依赖范围由于其特殊性,一般都是指向打包类型为pom的模块。如果有多个项目,它们使用的依赖版本都是一致的,就可以定义一个使用dependencyManagement专门管理依赖的POM,然后在各个项目模块中导入这些依赖。
(4)插件管理
Maven提供了类似dependencyManagement元素帮助管理依赖的pluginManagement元素帮助管理插件。当POM中配置了真正的plugin元素,并且groupId和artifactId与pluginManagement中配置的插件匹配时,pluginManagement的配置才会影响实际的插件行为。
例如:parent中配置maven-source-plugin,将其jar-no-fork目标绑定到verity生命周期阶段,已生成源代码。
org.apache.maven.plugins maven-source-plugin 2.1.1 attach-sources verify jar-no-fork false
子项目中应该如下配置:
org.apache.maven.plugins maven-source-plugin
另一个例子:子项目中都使用了maven-compiler-plugin开启Java6编译支持和maven-resources-plugin使用UTF-8编码处理资源文件,可以采用如下配置:
org.apache.maven.plugins maven-compiler-plugin org.apache.maven.plugins maven-resources-plugin UTF-8
子项目可以完全移除关于maven-compiler-plugin和maven-resources-plugin的配置,而直接使用这两个插件。插件的行为,在parent的POM进行了统一配置。
4.聚合与继承的关系
聚合主要是为了方便快速构建项目,继承主要是为了消除重复配置。
对于聚合模块来说,它知道有哪些被聚合的模块,但那些被聚合的模块不知道这个聚合模块的存在。
对于继承关系的父POM来说,它不知道有哪些子模块继承于它,但那些子模块都必须知道自己的父POM。
两个特征的共同点是聚合POM和继承关系父POM中的packaging都必须是pom,同时聚合模块与继承关系中的父模块除了POM之外都没有实际的内容。
在一些实际项目中,为了方便,一个POM可以即是聚合POM,又是父POM。也就是说可以合并aggregator和parent项目,合并后的POM如下:
4.0.0 com.gqshao.myapp parent 1.0-SNAPSHOT parent pom parent project-a project-b UTF-8 2.5.6 4.7 org.springframework spring-core ${springframework.version} org.springframework spring-beans ${springframework.version} org.springframework spring-context ${springframework.version} org.springframework spring-context-support ${springframework.version} junit junit ${junit.version} test org.apache.maven.plugins maven-compiler-plugin org.apache.maven.plugins maven-resources-plugin UTF-8 org.apache.maven.plugins maven-source-plugin 2.1.1 attach-sources verify jar-no-fork false
该POM打包方式为pom,包含modules元素,用来聚合项目,还包含了properties、dependencyManagement和pluginManagement元素供子元素继承。
相应的,被聚合的项目和子项目的POM配置也要做微小的修改,本来parent和他们位于同级目录,因此要配置relativePath的值为../parent。但现在parent在上一层目录,这是Maven默认能识别的父模块位置,因此不需要在配置relativePath,POM修改如下:
com.gqshao.myapp Parent 1.0.0-SNAPSHOT project-a The Aroject A
5.约定优于配置
Maven假设用户的项目是
源码目录为:src/main/java/
编译输出目录为:target/classes/
打包方式为:jar
包输出目录为:target/
Maven允许自定义源码目录
src/java
该项目中源码目录就成了src/java,而不是默认的src/main/java
超级POM$M2_HOME/lib/maven-model-builder-x.x.x.jar中的org/apache/maven/model/pom-4.0.0.xml下
(1)首先超级POM通过<repositories>和<pluginRepositories>定义了仓库和插件仓库,并且都关闭了SNAPSHOT支持。
central Central Repository http://repo.maven.apache.org/maven2 default false central Central Repository http://repo.maven.apache.org/maven2 default false never
(2)超级POM依次定义了主输出目录、主代码输出目录、最终构件的名称形式、测试代码输出目录、主源码目录、脚本源码目录、测试源码目录、主资源目录和测试资源目录。
${project.basedir}/target ${project.build.directory}/classes ${project.artifactId}-${project.version} ${project.build.directory}/test-classes ${project.basedir}/src/main/java src/main/scripts ${project.basedir}/src/test/java ${project.basedir}/src/main/resources ${project.basedir}/src/test/resources
(3)超级POM为核心插件设定版本
maven-antrun-plugin 1.3 maven-assembly-plugin 2.2-beta-5 maven-dependency-plugin 2.1 maven-release-plugin 2.0
6.反应堆
在一个多模块的Maven项目中,反应堆(Reactor)是指所有模块组成的一个构建结构,对于单模块的项目,反应堆就是该模块本身,但对于多模块项目来说,反应堆就包含了各模块之间继承与依赖的关系,从而能够自动计算出合理的模块构建顺序。
(1)反应堆顺序
反应堆顺序为Maven自动进行计算,通过构建输出信息,可以知道反应堆顺序。
(2)剪裁反应堆
一般来说,用户会选择构建整个项目或者构建单独的模块。通过输入mvn -h可以了解剪裁反应堆的命令
1)-am,--also-make:同时构建所列模块的依赖模块;
2)-amd,--also-make-dependents:同时构建依赖于所列模块的模块;
3)-pl,--projects <arg>:构建指定的模块,模块间用逗号分隔;
4)-rf,--resume-from <arg>:从指定的模块回复反应堆。
例子:
默认情况下执行 mvn clean install会得到完整的反应堆,此时可以选择-pl选项指定构建几个模块
mvn clean package -pl project-a,project-b[INFO] Reactor Summary:[INFO][INFO] project-a ......................................... SUCCESS [1.529s][INFO] project-b ......................................... SUCCESS [0.780s
使用-am选项可以同时构建所列模块的依赖模块
mvn clean package -pl project-a -am[INFO] Reactor Summary:[INFO][INFO] parent ............................................ SUCCESS [0.328s][INFO] project-a ......................................... SUCCESS [1.388s]
使用-amd选项可以同时构建依赖于所列模块的模块
mvn clean package -pl parent -amd[INFO] Reactor Summary:[INFO][INFO] parent ............................................ SUCCESS [0.156s][INFO] project-a ......................................... SUCCESS [1.279s][INFO] project-b ......................................... SUCCESS [0.640s]
使用-rf选项可以在完整的反应堆构建顺序基础上指定从哪个模块开始构建
mvn clean package -rf project-b[INFO] Reactor Summary:[INFO][INFO] project-b ......................................... SUCCESS [1.263s][INFO] aggregator ........................................ SUCCESS [0.000s]
在-pl -am或者-pl -amd的基础上,还能应用-rf参数,以对剪裁后的反应堆再次剪裁
mvn clean package -pl parent -amd -rf project-a[INFO] Reactor Summary:[INFO][INFO] project-a ......................................... SUCCESS [1.358s][INFO] project-b ......................................... SUCCESS [0.639s]