Maven由Java语言编写,基于微内核架构和可扩展插件机制,是一款优秀且成熟的项目管理工具。经过十几年完善和发展,Maven在Java服务端项目管理上已经成为事实上的标准工具。
在Maven出现之前,Java语言的项目管理工具一直由Ant统治着;在此之后,又有Gradle逐渐在Android项目中作为配套打包工具流行开来。在目前看来,Maven依旧是Java服务器端项目管理工具中的王者。
因此,每一位高级工程师或软件架构师,都应该至少具备以下两项Maven技能:
- 熟练使用Maven构建项目
- 排查并调解项目依赖冲突
#1 依赖机制
1.1 依赖传递
依赖传递的发生有两种情况:一种是存在模块之间的继承关系,在继承父模块后同时引入了父模块中的依赖,可通过可选依赖机制放弃依赖传递到子模块;另一种是引包时附带引入该包所依赖的包,该方式是引起依赖冲突的主因。
除了包传递之外,依赖传递还可以传递其它pom元素。以下是一个较为常见的pom文件,该文件中能够传递的元素有、、等。读者可自行查阅相关资料来获取所有的可传递依赖元素。
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
| <groupId>com.github.miofy.examples</groupId> <artifactId>examples-maven-project</artifactId> <packaging>pom</packaging> <version>0.1-SNAPSHOT</version> <name>examples-maven-project</name> <description>Parent Project</description> <inceptionYear>2019</inceptionYear> <developers> <developer> <name>miofy</name> <email>limiaofei@51dojo.com</email> </developer> </developers> <modules> <module>examples-maven-module-a</module> <module>examples-maven-module-b</module> </modules> <properties> <jersey.version>2.28</jersey.version> <junit.version>4.12</junit.version> </properties> <dependencies> <dependency> <groupId>org.glassfish.jersey.core</groupId> <artifactId>jersey-server</artifactId> <scope>compile</scope> </dependency> <dependency> <groupId>org.glassfish.jersey.containers</groupId> <artifactId>jersey-container-grizzly2-http</artifactId> <exclusions> <exclusion> <groupId>org.glassfish.hk2.external</groupId> <artifactId>jakarta.inject</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> <version>2.6.0</version> <scope>runtime</scope> </dependency> <dependency> <groupId>servlet</groupId> <artifactId>servlet-api</artifactId> <version>3.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> <optional>false</optional> </dependency> </dependencies>
<dependencyManagement> <dependencies> <dependency> <groupId>org.glassfish.jersey</groupId> <artifactId>jersey-bom</artifactId> <version>${jersey.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
|
1.2 依赖范围
项目的编译、测试和运行都会对应各自的一套类路径(classpath),不同的类路径引入的包依赖也不同。Maven就是通过依赖范围来控制引包情况的。
1 2 3 4
| <dependency> ... <scope>compile/provided/runtime/test/import/system</scope> </dependency>
|
1.3 依赖优化
实际上Maven是比较“智能”的,它能够自动解析直接依赖和传递性依赖,根据预定义规则判断依赖范围的合理性,也可以对部分依赖进行适当调整来保证构件版本唯一。
即使这样,还会有些情况使Maven误判,因此手工进行依赖优化还是相当有必要的。读者可以使用maven-dependency-plugin提供的三个目标来实现依赖分析:
1 2 3
| $ mvn dependency:list $ mvn dependency:tree $ mvn dependency:analyze
|
若读者还需更精细的分析结果,可以在命令后使用诸如以下参数:
1 2
| -Dverbose -Dincludes=<groupId>:<artifactId>
|
1.4 依赖调解
依赖调解遵循以下两大原则:路径最短优先、声明顺序优先
把当前模块当作顶层模块,直接依赖的包则作为次层模块,间接依赖的包则作为次层模块的次层模块,依次递推…,最后构成一棵引用依赖树。
假设当前模块是A,两种依赖路径如下所示:
1 2
| A --> B --> X(1.1) // dist(A->X) = 2 A --> C --> D --> X(1.0) // dist(A->X) = 3
|
此时,Maven可以按照第一原则自动调解依赖,结果是使用X(1.1)作为依赖。
若冲突依赖的路径长度相同,那么第一原则就无法起作用了。
假设当前模块是A,两种依赖路径如下所示:
1 2
| A --> B --> X(1.1) // dist(A->X) = 2 A --> C --> X(1.0) // dist(A->X) = 2
|
当路径长度相同,则需要根据A直接依赖包在pom文件中的先后顺序来判定使用那条依赖路径,如果次级模块相同则向下级模块推,直至可以判断先后位置为止。
1 2 3 4 5 6 7
| <dependencies> ... dependency B ... dependency C </dependencies>
|
假设依赖B位置在依赖C之前,则最终会选择X(1.1)依赖。
若相同类型但版本不同的依赖存在于同一个pom文件,依赖调解两大原则都不起作用,需要采用覆盖策略来调解依赖冲突,最终会引入最后一个声明的依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
<dependencies> <dependency> <groupId>commons-cli</groupId> <artifactId>commons-cli</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>commons-cli</groupId> <artifactId>commons-cli</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>commons-cli</groupId> <artifactId>commons-cli</artifactId> <version>1.3</version> </dependency> </dependencies>
|
#2 依赖解调三板斧
2.1 问题定位
若启动应用时出现以下异常错误信息,很可能是发生了依赖冲突。
1 2 3
| NoClassDefFoundError NoSuchMethodError ClassNotFoundException
|
2.2 依赖排查
根据异常提示信息,找到相应类,定位该类所在包。使用以下命名显示该包涉及到的依赖树。
1
| $ mvn clean dependency:tree -Dverbose -Dincludes=<groupId>:<artifactId>
|
依赖树是以当前模块作为顶层节点,引入的其它模块作为子节点,一般的项目都会存在多层级依赖情况。查看依赖树时需要重点关注包冲突和包重复两个部分。
1 2
| Part 1: omitted for conflict with XXX Part 2: omitted for duplicate
|
如果是IDEA旗舰版用户,还可以使用Diagram来分析依赖,重点关注依赖图中红线连接部分,那里可能是发生依赖冲突的地方。这种方式虽然直观,但是依赖包过多时排查难度陡增。
IDEA Pom Diagram
还有一种方式是安装IDEA插件市场中提供的依赖分析插件。操作界面简单,排查冲突很方便。
Maven Dependency Plugin for IDEA user
Dependecy Analyzer
2.3 解决冲突
冲突解决方式简单粗暴,直接在pom文件中排除冲突依赖即可。
1 2 3 4 5 6 7 8 9 10 11 12
| <dependency> <groupId>org.glassfish.jersey.containers</groupId> <artifactId>jersey-container-grizzly2-http</artifactId> <exclusions> <exclusion> <groupId>org.glassfish.hk2.external</groupId> <artifactId>jakarta.inject</artifactId> </exclusion> ... </exclusions> </dependency>
|