maven是java开发常用的构建工具,maven的依赖管理、冲突排包等工作又比较耗时费力,因此了解maven原理,熟悉maven的使用,可以有效提高开发人员的工作效率。本文通过介绍maven的原理,总结日常使用经验,分享出来,希望对大家有所帮助。
核心概念
学习和使用maven可以从两个配置文件入手:settings.xml 和pom.xml。其中settings.xml主要管理mvn仓库的相关配置,通常放在.m2/settings.xml文件。pom.xml主要管理项目的依赖和构建配置。
settings.xml
重点关注以下配置:
server: 指定远程maven仓库的连接用户和密码,示例如下。
<server>
<!--设置仓库id,和repository的id值匹配-->
<id>deploymentRepo</id>
<!--连接repository的用户名和密码-->
<username>repouser</username>
<password>repopwd</password>
</server>
repository: 设置远程仓库的id,地址以及下载的文件类型和更新策略,示例如下。实际使用过程中有时候会碰到SNAPSHOT依赖包不会自动更新,可以检查这个配置的updatePolicy值,改为always即可。
<profile>
<id>jdk-1.4</id>
<activation>
<jdk>1.4</jdk>
</activation>
<repositories>
<repository>
<!--设定仓库id,在server/mirror中根据这个id匹配仓库-->
<id>jdk14</id>
<name>Repository for JDK 1.4 builds</name>
<!--指定仓库的url地址-->
<url>http://www.myhost.com/maven/jdk14</url>
<snapshots>
<enabled>false</enabled>
<!--snapshot的依赖更新策略,可选interval,daily,never,always-->
<updatePolicy>always</updatePolicy>
</snapshots>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
</profile>
maven是java开发常用的构建工具,maven的依赖管理、冲突排包等工作又比较耗时费力,因此了解maven原理,熟悉maven的使用,可以有效提高开发人员的工作效率。本文通过介绍maven的原理,总结日常使用经验,分享出来,希望对大家有所帮助。
核心概念
学习和使用maven可以从两个配置文件入手:settings.xml 和pom.xml。其中settings.xml主要管理mvn仓库的相关配置,通常放在.m2/settings.xml文件。pom.xml主要管理项目的依赖和构建配置。
settings.xml
重点关注以下配置:
server: 指定远程maven仓库的连接用户和密码,示例如下。
<server>
<!--设置仓库id,和repository的id值匹配-->
<id>deploymentRepo</id>
<!--连接repository的用户名和密码-->
<username>repouser</username>
<password>repopwd</password>
</server>
repository: 设置远程仓库的id,地址以及下载的文件类型和更新策略,示例如下。实际使用过程中有时候会碰到SNAPSHOT依赖包不会自动更新,可以检查这个配置的updatePolicy值,改为always即可。
<profile>
<id>jdk-1.4</id>
<activation>
<jdk>1.4</jdk>
</activation>
<repositories>
<repository>
<!--设定仓库id,在server/mirror中根据这个id匹配仓库-->
<id>jdk14</id>
<name>Repository for JDK 1.4 builds</name>
<!--指定仓库的url地址-->
<url>http://www.myhost.com/maven/jdk14</url>
<snapshots>
<enabled>false</enabled>
<!--snapshot的依赖更新策略,可选interval,daily,never,always-->
<updatePolicy>always</updatePolicy>
</snapshots>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
</profile>
pom.xml
重点关注以下配置:
project: pom文件的根元素。核心有groupId、artifactId、version、packaging几个元素。其中groupId、artifactId、version指定了项目的唯一坐标。packaging指定了项目的打包类型,,即项目通过maven打包的输出文件的后缀名,包括jar、war、ear、pom等。
parentparent: pom文件通过parent来实现继承。同样包括groupId、artifactId、version三个元素,用来唯一指定继承的父pom文件的坐标。其次还有relativePath指定父pom.xml文件的相对位置,默认文件位置是../pom.xml。
build: 构建信息,主要包括项目构建所需路径管理,资源管理,插件管理元素等。
其中路径管理包括源码文件路径,测试代码路径,输出文件路径,脚本源码路径等。
资源管理包括资源文件的源目录,资源构建的模板路径。其中filtering指定是否使用参数值代替参数名,参数值取自文件里配置的属性。实际使用中可以通过filtering元素来动态替换配置文件中的配置值,比如不同环境使用不同的jdbc连接地址,可以通过filtering来实现。
插件管理元素包括项目中可以使用的mvn插件的声明以及相关配置。通常在父pom文件的pluginManagement声明使用的插件,子pom文件的plugin元素真正引入这些插件,并且可以对使用的插件配置进行覆盖。常用的插件有
java编译插件maven-compiler-plugin,
资源打包插件maven-resources-plugin,
源码打包插件maven-source-plugin
这些配置都有构建执行阶段phase, 执行目标goal等配置。常见的构建生命周期如下
阶段 处理 描述
验证 validate 验证项目 验证项目是否正确且所有必须信息是可用的
编译 compile 执行编译 源代码编译在此阶段完成
测试 Test 测试 使用适当的单元测试框架(例如JUnit)运行测试。
包装 package 打包 创建JAR/WAR包如在 pom.xml 中定义提及的包
检查 verify 检查 对集成测试的结果进行检查,以保证质量达标
安装 install 安装 安装打包的项目到本地仓库,以供其他项目使用
部署 deploy 部署 拷贝最终的工程包到远程仓库中,以共享给其他开发人员和工程
maven除了构建生命周期外,还有clean:项目清理生命周期,site: 项目文档的生命周期,有兴趣的同学可以上网查资料,这里不再赘述。
dependencies: 依赖管理是pom文件最常用的功能。
这里要注意dependencyManagement与dependencies的关系:
dependencyManagement里只是声明依赖,并不真正引入。子项目需要显式的声明需要用的依赖并且没有指定具体版本,才会从父项目中继承该项,并且version和scope都从父pom继承;如果子项目中指定了版本号,那么会使用子项目中指定的jar版本。
可以通过dependencyManagement实现pom依赖的多继承。子pom文件只能继承一个父pom文件,要想实现多继承,可以在父pom中指定依赖类型<type>pom</type>来继承三方pom中的依赖,示例如下
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencyManagement>
排包
使用maven过程中,经常会碰到 ClassNotFound或者MethodNotFound这样的异常,通常都是依赖包冲突引起的。解决冲突很简单,可以参考冲突解决,难点在于如何定位到冲突包的位置。关键是要理解maven依赖的引用顺序原则:最短路径,优先声明,依赖覆盖
加载顺序
最短路径
比如测试项目test依赖了A.jar, 又依赖了B.jar的1.0版本,同时A.jar也依赖了B.jar的2.0版本,那么实际使用的应该是B.jar的1.0版本。原因是test->B.jar 1.0 的路径长度小于test -> A.jar -> B.jar 2.0的路径长。如图
暂时无法在文档外展示此内容
优先声明
如果依赖路径长度都相同,那么先声明的依赖优先加载。示例如下
暂时无法在文档外展示此内容
test模块到c.jar 1.0 和 c.jar 2.0的路径长度一样,那么优先加载哪个版本?取决于pom文件中A.jar, B.jar的声明顺序。如果先声明了A,则优先使用C.jar 2.0。即先声明的优先生效。
<dependency>
<groupId>com.xxx</groupId>
<artifactId>A</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.xxx</groupId>
<artifactId>B</artifactId>
<version>1.0</version>
</dependency>
依赖覆盖
pom文件中,两个相同的依赖,即groupId, artifactId相同的依赖,后声明的会覆盖先声明的。示例如下,同时声明了两个A.jar依赖,后面声明的生效,最终加载1.0版本。
<dependency>
<groupId>com.xxx</groupId>
<artifactId>A</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>com.xxx</groupId>
<artifactId>A</artifactId>
<version>1.0</version>
</dependency>
了解maven依赖的顺序原则后,再来看如何定位包冲突。包冲突的表现都差不多,系统启动或者运行时抛出ClassNotFound或者MethodNotFound,如下图:
Description:
An attempt was made to call a method that does not exist. The attempt was made from the following location:
org.apache.dubbo.rpc.cluster.Cluster.getCluster(Cluster.java:58)
The following method did not exist:
org.apache.dubbo.common.extension.ExtensionLoader.getExtension(Ljava/lang/String;Z)Ljava/lang/Object;
The method's class, org.apache.dubbo.common.extension.ExtensionLoader, is available from the following locations:
jar:file:/Users/admin/.m2/repository/org/apache/dubbo/dubbo-common/2.7.7/dubbo-common-2.7.7.jar!/org/apache/dubbo/common/extension/ExtensionLoader.class
jar:file:/Users/admin/.m2/repository/org/apache/dubbo/dubbo/dewu-2.7.7/dubbo-dewu-2.7.7.jar!/org/apache/dubbo/common/extension/ExtensionLoader.class
It was loaded from the following location:
file:/Users/admin/.m2/repository/org/apache/dubbo/dubbo-common/2.7.7/dubbo-common-2.7.7.jar
Action:
Correct the classpath of your application so that it contains a single, compatible version of org.apache.dubbo.common.extension.ExtensionLoader
方法org.apache.dubbo.common.extension.ExtensionLoader.getExtension(Ljava/lang/String;Z)Ljava/lang/Object;不存在。这里使用JNI描述符描述,表示ExtensionLoader类的方法 Object getExtension(String, boolean)找不到。同时指出了冲突的两个jar:dubbo-common-2.7.7.jar 和 dubbo-dewu-2.7.7.jar, 以及真正加载的jar是dubbo-common-2.7.7.jar。
JNI描述符
JNI描述符是 JNI 对java 类型的编码,如
Ljava/lang/String;表示String 类型, Z表示布尔类型等等。
具体参考JNI描述符
通常依赖冲突都是由相同jar包的不同版本引起的,这类冲突可以通过maven命令 mvn dependency:tree|grep [artifactId] 查看引入该jar的依赖路径,从而排查冲突,如下
也可以通过maven helper插件快速定位冲突
冲突解决
根据依赖包的加载顺序的三个原则,我们可以有以下方式来解决冲突通过明确指定依赖包版本使用exclusions元素排查冲突的包,如下
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.4.0</version>
<exclusions>
<exclusion>
<artifactId>com.fasterxml.jackson.core</artifactId>
<groupId>jackson-databind</groupId>
</exclusion>
</exclusions>
</dependency>
通过移动依赖声明的顺序,把需要加载的依赖包提前,保证加载正确的版本(不推荐)。若非必要,不要依赖声明顺序来指定加载顺序。
使用规范
线上依赖jar包不要使用SNAPSHOT包。
说明:SNAPSHOT包即快照包是不稳定包,mvn仓库不会限制快照包的部署次数,提供方可能会随时修改包内容并重新发布,此时线上如果引用了快照包可能会触发接口兼容异常,导致线上故障。
SNAPSHOT依赖设置为自动刷新
说明:开发过程中经常碰到SNAPSHOT包不更新导致接口兼容问题,排查起来耗时费力。可以通过设置SNAPSHOT依赖自动刷新来解决。部署SNAPSHOT包时,使用-u命令强制刷新所有依赖:mvn deploy -U
settings.xml文件设置SNAPSHOT的依赖刷新策略为always,保证SNAPSHOT依赖包能够及时刷新。
<repositories>
<repository>
<id>xxx</id>
<name>xxxx name</name>
<url></url>
<snapshots>
<enabled>false</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
</repository>
</repositories>
开发测试SNAPSHOT包时,优先发布到本地。
通过在父项目中定义好 groupId ,version 等参数, 子项目不用定义这些参数,优先从父项目继承。
依赖尽量通过dependencyManagement提前在父POM中声明,项目中省略版本号设置。
优先使用import pom的方式管理依赖集合。可以有效减少pom文件的大小,屏蔽依赖包的复杂度。比如Spring boot的相关依赖可以通过以下方式管理
说明:开发测试SNAPSHOT时,会频繁修改包内容,可能影响其他依赖了该SNAPSHOT包的项目。因此开发测试该包时,先通过mvn install部署到本地仓库,本地自测确认没有兼容问题后再通过mvn deploy部署到远端仓库。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>