Maven - dependencyManagement
今天被maven的transitive依赖搞懵逼了,一下午未果。晚上又查了查,突然意识到自己对dependencyManagement
的理解不太完整,果然是栽到这个上面了……
依赖仲裁
dependency mediation
首先有个玩意儿叫 依赖仲裁,遵循就近原则:并不是版本高的留下,而是谁离root近谁留下……
正因为如此,直接定义的依赖会优先于transitive依赖,即使自己定义的依赖版本更低。
嗯,很合理,以前理解错了……
- https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html
dependencyManagement
一直错误低估dependencyManagement
标签的威力了……
- https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#dependency-management
Dependency management - this allows project authors to directly specify the versions of artifacts to be used when they are encountered in transitive dependencies or in dependencies where no version has been specified.
所以有俩功能,适用场景分别为:
- 依赖不指定版本的时候;
- 传递性依赖的版本问题;
功能一:统一管理child pom依赖
这个是之前理解的功能,child pom可以不指定依赖的版本了,默认用parent pom里dependencyManagement
的依赖的版本。
但是之前一直以为只有这一个功能,大错特错……
功能二:控制transitive依赖的版本
A second, and very important use of the dependency management section is to control the versions of artifacts used in transitive dependencies.
传递性依赖的版本,直接由dependencyManagement
敲定!dependency management takes precedence over dependency mediation for transitive dependencies。
依赖仲裁分为两部分:
- 直接引入的依赖,肯定nearest,它就是最终的版本;
- 传递性依赖,也适用于nearest,谁近用谁的版本。但是如果
dependencyManagement
声明了某个版本,它的优先级高于nearest,所以直接使用dependencyManagement
里声明的版本。
如果有多处dependencyManagement
,那么本项目的dependencyManagement
优先级高于parent的dependencyManagement
。
比如下面这个parent和child——
parent:
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
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>A</artifactId>
<packaging>pom</packaging>
<name>A</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>b</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>c</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>d</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
child:
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
<project>
<parent>
<artifactId>A</artifactId>
<groupId>maven</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>B</artifactId>
<packaging>pom</packaging>
<name>B</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>d</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>1.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>c</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
在child里,a必然是1.0,因为直接定义了。d也必然是1.0,因为没直接定义,那么一旦有d的传递性依赖,必然用1.0版本的d。
b,c和d一样,定义在了parent的dependencyManagement
里,所以一旦作为传递性依赖引入,也必然是1.0版本。
parent定义的d默认是1.2,它被child里定义的d 1.0覆盖了。
所以对于child来说,a/b(如果引入了)/c(如果引入了)/d(如果引入了)必然都是1.0版本。
dependencyManagement
的应用
下面这种情况,默认用D的1.0版本,因为1.0版本的D离root更近:
1
2
3
4
5
6
A
├── B
│ └── C
│ └── D 2.0
└── E
└── D 1.0
但是有两种方式可以让maven用D的2.0或者其他版本——
方法一:直接把想用的D的版本引入进来,它nearest,所以就用它的版本:
1
2
3
4
5
6
7
8
A
├── B
│ └── C
│ └── D 2.0
├── E
│ └── D 1.0
│
└── D x.y
这种方式最常见。
方法二:把想用的D的版本作为依赖放到dependencyManagement
里,那么不管D作为transitive依赖的版本是哪个,都用dependencyManagement
里定义的这个:
1
2
3
4
5
6
A
├── B
│ └── C
│ └── D 2.0
└── E
└── D 1.0
Instead, A can include D as a dependency in its dependencyManagement section and directly control which version of D is used when, or if, it is ever referenced.
这种写法的主要语义在于工程并没有直接使用D的东西,但是又想控制D作为传递性依赖实际使用的版本。
import
scope
只有dependencyManagement
部分才能指定依赖的scope=import
,同时只有type为pom
的依赖才能声明scope=import
,而import就是暴力替换,相当于把这个pom类型的依赖里定义的一堆依赖写入到这个dependencyManagement
里。
比如spring-boot-dependencies
的dependencyManagement
有个spring-data-bom
:
1
2
3
4
5
6
7
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-bom</artifactId>
<version>${spring-data-bom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
spring-data-bom
是spring-data相关工程依赖的集合,比如spring-data-elasticsearch
:
1
2
3
4
5
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>4.2.5</version>
</dependency>
spring-boot-dependencies
的dependencyManagement
里import了这个bom,相当于把spring-data-elasticsearch
等依赖都写到了spring-boot-dependencies
的dependencyManagement
。
- https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#dependency-scope
苦哈哈的栗子
工程A发了一个包,包含spring-data-elasticsearch:4.4.1,用了elaticsearch相关的依赖7.17.3。
工程B引入了这个包,且工程B的parent是:
1
2
3
4
5
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.1</version>
</parent>
结果,发现工程B依赖spring-data-elasticsearch:4.2.1,不是4.4.1,且elasticsearch相关的依赖版本是7.12.1。
当时我感觉最离谱的是,依赖分析表明,spring-data-elasticsearch:4.2.1是工程A的transitive依赖!这让我百思不得其解,把工程A都翻烂了,确实用的是spring-data-elasticsearch:4.4.1,实在不清楚4.2.1是哪来的……
当然,有了上面的知识,现在知道了,之所以用4.2.1,是因为工程B的parent是spring-boot-starter-parent:2.5.1,它的parent是:
1
2
3
4
5
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.5.1</version>
</parent>
spring-boot-dependencies:2.5.1的dependencyManagement
引入了spring-data-bom:
1
2
3
4
5
6
7
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-bom</artifactId>
<version>${spring-data-bom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
而这个spring-data-bom包含:
1
2
3
4
5
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>4.2.1</version>
</dependency>
也就是说,spring-data-elasticsearch:4.2.1在工程B的parent的parent的dependencyManagement
,在工程B没有直接引入spring-data-elasticsearch的情况下,不管transitive依赖的版本是啥,最终都用4.2.1。
所以最终的效果就是:对于工程B来说,spring-data-elasticsearch来自工程A的transitive依赖,其版本由工程B的parent敲定。所以看到了一个来自工程A的spring-data-elasticsearch:4.2.1,虽然实际上工程A用的版本是spring-data-elasticsearch:4.4.1。
解决方案:在不明白上述知识之前,我是直接把spring-data-elasticsearch:4.4.1作为直接依赖加入工程B的,结果发现工程里有两个spring-data-elasticsearch:来自B的4.4.1和来自A的4.2.1,冲突了,当然由于4.4.1更近,胜出。但是当时没有加elasticsearch client相关的依赖,所以虽然4.4.1胜出了,但是用的elasticsearch client相关的依赖都不是4.4.1里的7.17.3,而是4.2.1里的7.12.1,因为工程B的parent的dependencyManagement
还敲定了很多elasticsearch client相关的依赖的版本,为7.12.1。
所以最终的解决方案有两个:
- 按照之前的知识:把spring-data-elasticsearch和所有elasticsearch client相关的依赖都直接引入我想要的版本到工程B。但是这样会看到一个由工程A带来的spring-data-elasticsearch:4.2.1的冲突版本,虽然它并不会胜出;
- 按照现在的知识:把spring-data-elasticsearch和所有elasticsearch client加入
dependencyManagement
。这样工程A引入的spring-data-elasticsearch直接就被敲定为4.4.1,就看不到存在冲突了;
所以我最终选择了方法二:
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
<properties>
<spring-data-elasticsearch>4.4.1</spring-data-elasticsearch>
<!-- elasticsearch client version -->
<elasticsearch.client.version>7.17.3</elasticsearch.client.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>${elasticsearch.client.version}</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>${elasticsearch.client.version}</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>${elasticsearch.client.version}</version>
</dependency>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>${elasticsearch.client.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>${spring-data-elasticsearch}</version>
</dependency>
</dependencies>
</dependencyManagement>
覆盖properties以控制版本
在maven里,父pom里的properties是可以被子pom覆盖的!
- https://stackoverflow.com/a/19282752/7676237
- https://stackoverflow.com/a/18686480/7676237
所以还有一种更简单的控制依赖版本的方式:如果父pom的依赖的版本是通过properties定义的,在子项目里直接使用同名properties覆盖它即可!
比如spring-boot-dependencies里,所有的依赖都使用属性来控制版本,如果想修改junit-jupiter的版本,只需要覆盖junit-jupiter.version
属性即可:
1
2
3
<properties>
<junit-jupiter.version>5.10.0</junit-jupiter.version>
</properties>
父pom的dependencyManagement
里都已经写好了,子项目里override起来非常方便。但是如果父pom里没有使用变量,子pom就要用上面的方式,自己在dependencyManagement
里再写一遍依赖了,没办法直接复用。
其他属性覆盖
属性覆盖还可以用在一些非依赖管理的场合。
比如我们经常看到springboot3.x项目里这样设置属性:
1
2
3
4
5
6
7
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
实际上很多没必要设置。
首先java.version
来自springboot而非maven本身。
来自maven本身的一般都是以maven开头,比如
maven.compiler.source
。
springboot 3.x要求jdk17以上,所以spring-boot-starter-parent的属性是这么设置的:
1
2
3
4
5
6
7
<properties>
<java.version>17</java.version>
<resource.delimiter>@</resource.delimiter>
<maven.compiler.release>${java.version}</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
因此如果我们使用springboot3.x和jdk17,没必要再设置java.version
,springboot已经设置为17了。
又因为java.version
在spring-boot-starter-parent里用来设置了maven.compiler.release
,所以没必要再设置maven.compiler.source/target
。
maven.compiler.release
控制的是jdk9里javac引入的--release
,maven.compiler.source/target
控制的则是jdk9之前的javac里就有的-source
/-target
。--release
更好。
所以最终可以把上面的properties都省掉,没必要写了。
感想
我太菜了o(╥﹏╥)o
不过我又变强了(* ̄︶ ̄)