一般的Java工程项目都使用了Meven进行依赖管理(另外一些会使用Gradle进行管理,例如Android项目等,这里不再阐述了)。Meven在引入依赖是很方便的,只要有(groupId, artifactId, version)三元组,就可以引入一个依赖。但是,实际使用中有时会碰到一些棘手的问题,可能会踩坑很久。比如:
- 依赖冲突问题,经典的log4j日志组件依赖冲突,网上有大量遇到这种问题的文章。
- 版本不对齐问题,例如使用Spring框架时需要引用spring-core,spring-context等子模块包,如果这些依赖的版本不一致,例如一个版本是3.1.2,另一个版本是4.1.3,就可能发生兼容问题。
- 多模块依赖管理问题,项目需要划分为多个子模块,同时希望这些子模块能够使用一样版本的依赖,如果在每个模块里每次都声明版本,则在后续的维护可能会出现疏漏。
- 依赖不规范问题,项目需要发布为Jar包提供给其他服务作为接口包或sdk接口使用,却因为引入了Spring-boot包,导致引入了这个包的服务因为本来并不是使用Spring-Boot框架的服务,启动出现失败,但这个接口或sdk又需要对Spring-Boot服务做一些支持逻辑而需要引用Spring-Boot包。
这里主要说一些在依赖管理上的一些最佳实践,帮助从工程上能够尽量减少这些问题的发生。
1. 在properties中声明一组相同版本的依赖
对一些一组相同版本的依赖,可以通过properties标签里声明一个环境变量,然后使用这个变量通配符声明版本,这个变量通配符就会映射为在properties定义的值。如下面配置所示。
<properties>
<spring.version>5.2.3.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
例如在使用Spring框架时通常需要引入一系列的包,这些包需要保证版本一致,通过定义变量spring.version
,如同编程语言里变量一样,后面声明时直接引用变量名,就可以保证所使用的版本就是变量的值5.2.3.RELEASE
而且在后续如果要升级Spring版本,只要修改spring.version
就可以了,不用再往下一个个翻着找Spring的依赖然后修改,(有时候pom文件会很长),减少升级成本。
一种规范化的做法是,所有的依赖的版本都声明在properties
里,按xxx.version
来定义,这个根据个人实际的喜好来,建议一些对项目比较重要的依赖将版本,以及一组相同依赖的版本声明到properties
标签里。
2. 注意依赖声明的顺序
依赖是有优先级判断原则的,如果有多个依赖间接使用了同一个依赖,那么会使用引用路径最短的版本,如果最短路径的依赖有多个,那就找最先声明的版本。
例如项目P依此引用A, B, C包,然后各个包的依赖依此是 A->D->E(1.0)
, B->E(1.0)
, C->E(2.0)
, 那实际引用的E的版本是1.0
。
有时候这种依赖判断原则会出现一些问题,例如,如果C是项目P的子模块,在C里已经声明了依赖E的版本是2.0
, 而B只是一个第三方工具依赖,它间接依赖了旧的版本1.0
, 在上面的声明顺序就会导致项目P引用了老的版本1.0
,从而可能到服务跑起来时才发现E的版本不对的问题,严重时会导致线上事故。
所以,依赖声明的顺序还是很重要的,下面给一个比较推荐的依赖声明顺序。
- 最先声明本模块依赖项目的其他项目子模块,例如
exapmle-dao
,example-service
,example-api
三个子模块,那不管三七二十一example-service
里先声明引用exapmle-dao
。 - 其次声明通用基础组件的依赖,比如spring、jackson (fastjson经常爆漏洞,不推荐再使用了)、log4j等。
- 其次声明公司基础框架依赖,例如RPC框架、Redis客户端等
- 其次声明本模块特定使用的依赖,保证这些特定的依赖不会覆盖上面的依赖。
- 最后声明中需要使用的测试依赖,例如junit、assert4j等
3. 使用bom统一管理基础依赖的版本1
除了像spring那样需要引用一致的版本之外,有时候一些不同依赖之间是也是有一定的版本要求的,这个经常发生在需要使用大量公司提供的基础组件依赖的场景里,这些基础组件相互之间存在引用,如果有其中的版本不对有可能会出现奇奇怪怪的问题。这时候可以使用bom将版本统一起来。
BOM 是Bill Of Materials的缩写,就是说在这个bom里把一系列组件的依赖的版本都声明好了,同时人工去保证这些版本之间不会出现严重的冲突或兼容性问题,确定好没有问题之后发布该bom文件,后续项目需要引用这些组件时,