这篇文章是5年前写的,从个人博客里又挖出来,多年后再看依然觉得不错,就发出来吧。当时maven的版本是3.5左右,现在已经到3.9了,不过核心机制没什么变化。本文应该能让你了解最核心的maven机制,以及解决常见问题
1 Maven的作用
Java开发绕不开的东西,用过的都清楚,几句话带过:
(不得不说,比go语言强太多,go到现在也没有比较好的依赖管理和构建工具)
2 Maven安装
现在IntellJ IDEA中已经集成了maven,如果都在IDE中操作,不下载maven也是可以的,但是settings.xml一定要改。
3 构件 Artifact
在项目中每一个pom(Project Object Model) 文件通过package
命令打包后都会生成一个构件(artifact 也可翻译为制品),构件的唯一性通过坐标来确定。而构件之间又有依赖关系,在pom文件中通过dependencies
来管理依赖,有些人也会不太规范地把构件称为依赖 dependency。
有一类比较特殊的构件叫“插件”,在插件章节中说明
3.1 坐标
Maven中通过坐标来标识构件,构成坐标的元素(4+1):groupId,artifactId,version,packaging,classifier。
构件的文件名规则:artifactId-version[-classifier].packaging
在本地仓库目录中,顺着groupId往下找,就能找到对应的构件
4要素通过pom文件直接定义,但classifier不能直接定义,需要通过附加插件生成。例如javadoc插件可以生成javadoc的classifier。
3.2 依赖范围
maven在引入依赖时是需要指定依赖范围的,也可以理解为作用域(scope),依赖只在指定作用域内生效。 依赖范围一共有以下几种:compile,test,provided,runtime,system,import
maven项目过程中有三种classpath:编译期classpath,测试期classpath,运行期classpath
dependencyManagement
结点中使用,用于import另一个pom的dependencyManagement3.3 依赖传递性
依赖的传递性可以简单理解为:A依赖了B,B依赖C,那么A会自动依赖C
依赖范围会对传递性的影响,如下表, 第一列表示A对B的依赖范围,第一行表示B对C的依赖范围,中间结果表示A对C的依赖范围
"-"表示不会传递依赖
3.4 依赖冲突
有依赖就会产生依赖冲突,例如存在C的两个版本C1和C2,现有以下两个依赖链路:
A -> B -> C1
A -> C2
这时C存在依赖冲突,maven有一个依赖仲裁机制:
上述情况因C2的依赖路径最短,因此C2最终生效。
如果依赖仲裁结果不是预期结果,可以通过调整依赖路径长度,或使用exclusions
来排除依赖
3.4 依赖分析插件
当依赖树特别复杂时,需要通过maven的dependency插件来对依赖进行分析,dependency插件是maven默认引入的,可以直接使用。
mvn dependency:list [-DoutputFile=dep.txt]
可以用来查看已解析的依赖列表,只输出依赖仲裁结果mvn dependency:tree
输出依赖树mvn dependency:analyze
依赖分析(可以分析出使用但未声明,声明但未使用,未使用的依赖可能可以删除)dependency:analyze分析出的结果可能并不准确,因为该命令只检索java代码,不能检索到xml文件中引用的类。
4 仓库 Repository
仓库是保存构件的地方。
4.1 仓库分类
按仓库所处位置来分类:
常用公共库地址:
按类型分:
按存储策略分:
release和snapshot
构件分为发布版本和快照版本,在version中如果以-SNAPSHOT结尾,则为快照版本,否则是发布版本。发布版本是稳定版,使用mvn deploy
部署到远程仓库时会自动部署到release仓库中,并且只能部署一次,再次部署会直接失败。快照版本使用mvn deploy
部署到远程仓库时会部署到snapshot仓库中,每次部署都会生成一个带时间戳的快照版本。
4.2 仓库配置
本地仓库地址,由settings.xml中的localRepository属性定义
远程仓库有几种配置方法:
<repository>
<id>releases</id> <!-- 仓库唯一标识 -->
<name>Releases</name> <!-- 仓库名称 -->
<releases> <!-- 配置发布版本策略 -->
<enabled>true</enabled> <!-- 允许从该仓库下载release的构件 -->
<updatePolicy>never</updatePolicy>
<checksumPolicy>warn</checksumPolicy>
</releases>
<snapshots> <!-- 配置快照版本策略 -->
<enabled>false</enabled> <!-- 禁止从该仓库下载snapshot的构件 -->
</snapshots>
<url>http://repo.yourdomain.com/nexus/content/repositories/releases/</url>
<layout>default</layout> <!-- 布局,使用默认 -->
</repository>
<强制更新> 有时会遇到jar包更新不到本地的情况,可以在执行maven命令时加上-U参数强制检查更新,如mvn clean package -U <插件仓库> 远程插件仓库配置:与普通仓库相同,只是要配置到pluginRepositories结点下
2. 在settings.xml中配置仓库
POM中配置的仓库只能给当前项目使用,如果要配置全局仓库,可以在settings.xml中配置profile
和repositories
,配置方式与pom相同
3. 配置仓库镜像
仓库镜像:如果仓库X可以提供仓库Y存储的所有内容,则X是Y的一个镜像。
如果公司有搭建私服,可以在私服上配置代理仓库,另外再配置一个仓库组,这个仓库组就可以作为所有仓库的镜像。本地开发时只需将这一个仓库配置为镜像即可。在settings中配置:
<mirrors>
<mirror>
<id>public</id>
<mirrorOf>*</mirrorOf>
<name>Public Repositories</name>
<url>http://repo.yourdomain.com/nexus/content/groups/public/</url>
</mirror>
</mirrors>
mirrorOf表示镜像的是哪个仓库,多个仓库用逗号隔开,*号表示镜像所有仓库,如上配置以后对所有其他仓库的访问都会转到该仓库上。
镜像仓库与其他仓库的区别
4.3 仓库的检索顺序
local_repo > settings_profile_repo > pom_profile_repo
> pom_repositories > settings_mirror > central
4.4 将构件发布到远程仓库
用mvn deploy
命令将构件发布到远程仓库,在此之前还需要在POM中配置distributionManagement
元素,需要同时配置repository
和snapshotRepository
,分别用于部署发布版本和快照版本。
<distributionManagement>
<repository>
<id>proj-release</id>
<name>Release</name>
<url>http://xxxxxxx</url>
</repository>
<snapshotRepository>
<id>proj-snapshot</id>
<name>Snapshot</name>
<url>http://xxxxxxx</url>
</snapshotRepository>
</distributionManagement>
仓库认证:通常发布构件需要私服的上传和发布权限,以nexus私服为例,默认发布账号是deployment,需要配置settings.xml中的servers
结点,server id即仓库ID
<servers>
<server>
<id>releases</id>
<username>deployment</username>
<password>deployment123</password>
</server>
<server>
<id>snapshots</id>
<username>deployment</username>
<password>deployment123</password>
</server>
</servers>
4.5 构件在仓库中的存储
存储位置:groupId/artifactId/version/artifactId-version[-classifier].packaging
解析依赖的机制:
构件的最新版本信息存储于仓库的元数据 中(maven-metadata.xml) release构件元数据:groupId/artifactId/maven-metadata.xml snapshot构件元数据:groupId/artifactId/version/maven-metadata.xml
5 生命周期 Lifecycle
maven将软件生命周期进行了抽象,生命周期是相对固定的,由一组阶段(Phase) 组成,每个阶段绑定插件来完成相应任务。有点像设计模式中的模板方法(Template Method),maven定好了生命周期的主体框架,保证框架的一致性,但每个阶段又能灵活定制。
maven有三套生命周期,每个生命周期由一组有顺序的阶段(Phase)组成,后面的阶段依赖前面的阶段,三套生命周期之间互相独立。
用于清理项目,有3个阶段组成:
用于构建项目,这个生命周期使用最多
用于建立项目站点,一般很少使用,有以下4个阶段组成
maven生命周期参考文档:http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
maven允许直接调用阶段,如mvn clean install site
,因三个生命周期相互独立,所以可以同时调用。
6 插件 Plugin
maven通过在生命周期的阶段上绑定插件目标的方式,把具体任务交给插件来完成。
每个插件一般都会提供多个插件目标(Goal) ,例如前文提到的dependency插件就有list
、tree
、analyze
等goal, 以及maven-compiler-plugin
插件提供的compile
goal,把goal与阶段绑定即可实现对插件的调用。
maven提供了一些阶段和插件目标的内置绑定关系(https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html#Built-in_Lifecycle_Bindings)可以应对绝大多数日常使用,因此调用mvn compile
实际上是调用的maven-compiler-plugin
插件的compile
goal。
插件的目标也可以直接调用,即使用mvn 插件名:目标的方式,例如上面提到的mvn dependency:list。但直接调用目标就脱离了maven的生命周期,一般只用于一些工具类的插件。
插件的默认绑定阶段
有一些插件的目标在编写时会绑定到默认阶段,例如上面提到的compile
就是默认绑定的。
使用maven-help-plugin插件来查看其它插件的详细信息以及默认绑定信息(help插件和被查询的插件都要在工程里声明),命令:
mvn help:describe -Dplugin=org.apache.maven.plugins:maven-source-plugin:2.1.1 -Ddetail
自定义绑定阶段
例如,把maven-help-plugin插件的describe目标绑定到verify阶段上(只是举个例子,一般不会这么绑定):
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-help-plugin</artifactId>
<version>2.2</version>
<executions>
<execution>
<id>help</id>
<phase>verify</phase>
<goals>
<goal>describe</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
插件groupId可以省略
有些项目中会看到引入插件并没有配置groupId,那是因为maven内置了两个groupId:org.apache.maven.plugins
和 org.codehaus.mojo
,即这两个groupId可以省略不写。
在settings.xml中的pluginGroups
可以配置额外的插件groupId
另,插件的元数据比较特殊,存储于groupId/maven-metadata.xml中
7 模块 Module
开发maven项目,通常都不会是单模块项目,其最佳实践是新建一个父模块,packaging设置为pom,并在父模块POM文件中配置modules
,如:
<groupId>com.xxx</groupId>
<artifactId>yyy</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>yyy-controller</module>
<module>yyy-service</module>
</modules>
每个module既是子模块,又是一个与当前pom的同级目录,添加子模块后,只需构建顶级模块,子模块会同时构建
7.1 模块的继承
子模块的好处:
能够被继承的元素有:
任何maven项目都隐式继承超级POM,类似Java的Object。超级POM位置:$MAVEN_HOME/lib/maven-model-builder-x.x.x.jar中的org/apache/maven/model/pom-4.0.0.xml
一些元素说明:dependencyManagement:该元素下的依赖声明不会引入实际的依赖,只是把声明继承给子模块,子模块在配置依赖时只需配置groupId和artifactId即可,未配置的属性(version、scope等)都从dependencyManagement中继承。
pluginManagement:与dependencyManagement类似
7.2 构建反应堆
如果把多模块maven项目中的所有模块的依赖关系用图来表示,这个图结构即为构建反应堆(Reactor) 。反应堆应该是一个有向非循环图,如果模块间出现循环依赖则会报错。
反应堆的构建顺序:
裁剪反应堆:有些项目非常大,构建时可以选择只构建某些模块以提高构建速度,通过在mvn命令中指定以下参数可以对反应堆进行裁剪。
-pl p1 [,p2 ,p3 ......] 选择指定模块
-am also make,同时构建所选模块的依赖模块
-amd also make dependecies,同时构建依赖于所选模块的模块
8 测试 Test
maven的default生命周期中有一个test阶段专门用于执行单元测试,默认单元测试插件:maven-surefire-plugin
,绑定目标test
该插件会自动检测src/test/java下以Test开头的类、以Test结尾的类、以TestCase结尾的类用于执行单元测试。在插件的includes
和excludes
,可以配置额外包含或排除的测试类。
使用命令mvn test -Dtest=测试类名
可以只测试某个类
使用-DskipTests
或-Dmaven.test.skip=true
参数可以跳过单元测试
生成测试覆盖率报告(与其他报告相同,输出在site目录下) 使用到插件:cobertura-maven-plugin
9 站点 Site
这里的站点是指包含本项目信息的一些静态界面,包括项目的基本信息、依赖信息、测试报告、代码检查报告等,maven支持在构建的过程中同时对这些信息进行输出。
9.1 生成站点
使用mvn site
命令可以将项目信息生成站点到target/site
针对多模块项目,可能更希望把所有模块的信息汇总一个目录,可以使用mvn site:stage
,默认汇总到target/staging,但必须先执行mvn site
在站点中增加静态检查报告:(findbugs必须先package生成classes)
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<locales>zh_CN</locales>
<reportPlugins>
<reportPlugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>2.17</version>
<configuration>
<configLocation>mucfc-checkstyle-1.0.xml</configLocation>
</configuration>
</reportPlugin>
<reportPlugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pmd-plugin</artifactId>
<version>2.7.1</version>
<configuration>
<targetJdk>${jdk.version}</targetJdk>
</configuration>
</reportPlugin>
<reportPlugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>findbugs-maven-plugin</artifactId>
<version>3.0.4</version>
</reportPlugin>
<!-- 在报告中查看源码插件 -->
<reportPlugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jxr-plugin</artifactId>
<version>2.5</version>
</reportPlugin>
</reportPlugins>
</configuration>
</plugin>
自定义生成其他项目信息:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>2.9</version>
<reportSets>
<reportSet>
<reports>
<report>dependencies</report>
</reports>
</reportSet>
</reportSets>
</plugin>
report可配置如下值:
9.2 发布站点
使用mvn site-deploy
命令可以生成站点,并部署到远程服务器。
把站点部署到本地文件系统:
<distributionManagement>
<site>
<id>site</id>
<url>file:E:\\site</url>
</site>
</distributionManagement>
<distributionManagement>
<site>
<id>stagingSite</id>
<url>scp://192.168.0.100/home/report/test</url>
</site>
</distributionManagement>
10 属性 Property
在maven pom文件中可以通过${}
来引用属性,属性分为以下几类
${basedir}:项目根目录,pom所在目录
可以引用POM文件中对应元素的值,如${project.groupId}对应project->groupId
的值
以settings开头,引用settings.xml中的元素
如${settings.localRepository}:本地仓库地址
如${user.home}
以env.开头,如${env.JAVA_HOME}
在pom的properties
结点中定义的属性
Java系统属性和环境变量属性都可以用mvn help:system
查看
在springboot中,properties文件中可以通过@xxx@直接引用pom属性
11 一些最佳实践
11.1 配置编码格式
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<jdk.version>1.8</jdk.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
11.2 版本号约定
版本号:主版本.次版本.增量版本-里程碑版本。 如2.0.1-alpha-1,3.1.0-release。 增量版本和里程碑版本可以省略。
通常快照版本以-SNAPSHOT
结尾
11.3 父子工程版本号保持一致
一般父工程pom定义如下:
<groupId>com.xxx</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
子工程pom定义:
<parent>
<groupId>com.xxx</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>child</artifactId>
这样子工程可以与父工程保持一致的版本,但有个问题是子工程需显式引用父工程的版本号,每次版本变更时需要把所有子工程的parent-version字段同时更新一遍。
在maven3.5以后提供了对revision的支持,可以让项目版本号只需配置一次。
父工程定义:
<groupId>com.xxx</groupId>
<artifactId>parent</artifactId>
<version>${revision}</version>
<properties>
<revision>1.0.0</revision>
</properties>
子工程定义:
<parent>
<groupId>com.xxx</groupId>
<artifactId>parent</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>child</artifactId>
END