1. 依赖范围
1.1 classpath & 依赖范围
- 上一篇博客提到,在pom文件中引用其他依赖时,可以指定依赖的范围
- 例如,
test
范围只对测试代码有效,对编译或运行主代码无效 - 这里maven在编译、运行和测试时,会使用三套不同的classpath:编译classpath、运行classpath、测试classpath
- 编译时,Maven 会将与编译相关的依赖引入到编译 classpath 中;
- 测试时,Maven 会将与测试相关的的依赖引入到测试 classpath 中;
- 运行时,Maven 会将与运行相关的依赖引入到运行 classpath 中
- 依赖范围,就是用来控制引入的依赖与这三种classpath之间的关系 。
1.2 maven的中依赖范围
compile
-
编译依赖范围,若不明确指定
scope
,默认使用该值。 -
compile范围的依赖,对编译classpath、运行classpath和测试classpath都有效。如,我自己喜欢使用的
commons-lang
依赖<dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency>
test
- 测试依赖范围,只对测试classpath有效,如,我们单元测试时使用的
junit
provied
- 已提供依赖范围,对编译classpath和测试classpath有效,对运行classpath无效。
- 如
sevlet-api
,它在容器中已经提供,运行项目时就不需要该依赖了,但编译和测试时仍然需要该依赖。因此,需要将其申明为provided
依赖
runtime
- 运行时依赖,对运行classpath和测试classpath有效,对编译classpath无效
- 如,使用JDBC驱动,编译时使用的是jdk提供的JDBC接口,只有在测试和运行时才会使用实现了JDBC接口的具体驱动。
- 执行测试,就是在运行测试代码。因此,从这个角度可以认为,测试是一种特殊的运行
system
-
系统依赖,同三种classpath的关系与
provided
一致 -
但是,
system
依赖需要通过systemPath
元素显式指定依赖文件的路径,是maven仓库无法解析的 -
这些依赖往往与本机系统绑定,可能会造成项目的不可移植
<dependency> <groupId>javax.sql</groupId> <artifactId>jdbc-stdext</artifactId> <version>2.0</version> <scope>system</scope> <systemPath>${java.home}/lib/rt.jar</systemPath> </dependency>
import(maven 2.09及以上)
- 导入依赖,不会对三种classpath产生影响。
《maven实战》书中,说后续会讲到该依赖。
1.3 依赖范围与classpath的总结
-
总结起来,maven中几种依赖范围与三种classpath的关系如下:
依赖范围 编译classpath 测试classpath 运行classpath 例子 compile Yes Yes Yes commons-lang test No Yes No junit provided Yes Yes No servlet-api runtime No Yes Yes JDBC-connector system Yes Yes No 本地的,maven仓库之外的类库文件
2. 依赖的传递
2.1 絮絮叨叨
- 自己本科就读的学校,比较喜欢在暑期开展学生实践培训活动。
- 当时就读的软件工程专业,从大一到大三的暑假,都会待学生到某个类似培训学校的地方,开展某种编程技术的培训。
- 大一,好像是基于MFC的编程;大二,好像是基于C#的编程;大三,是基于Android的编程。
- 自己清楚地记得进行Andriod编程时,培训老师说:你们用U盘把这些jar拷贝到自己的电脑上,不然不然你们自己去找jar几乎都不可能成功的 😂
- 现在,学习了maven的相关知识后,自己也理解为什么了。
- 我相信很多人和我一样:
① 创建java项目时,很多时候只是简单的创建Java项目
② 创建一个lib
包,需要使用到的依赖A,则去网上搜索,然后下载、添加并Add as library
。很多时候,下载的jar很可能版本不对,还需要重新下载。
③ 很多时候,程序运行时会提示找不到某个类,这时候又开始重复步骤②
④ 最后,下载了N个jar后,程序终于能运行起来了。 - 如果我们创建Java项目时,使用的是maven项目就不会出现这样的问题了。
- 因为,maven能帮助我们解析直接依赖以及传递性依赖导致的间接依赖。
2.2 依赖的传递性
- 假设,我们在pom文件中引用了依赖A(scope为compile),而依赖A的pom文件中又医用了依赖B(scope为compile)。这样,我们的项目通过传递性依赖,间接引用了依赖B。
- 一般地,我们称项目对于依赖A是第一直接依赖,称依赖A对于依赖B是第二直接依赖,称项目对于依赖B是传递性依赖。
- 从前面可知,引入依赖时存在scope,第一直接依赖和第二直接依赖的scope直接决定了传递性依赖的scope。
- 下表总结了第一直接依赖和第二直接依赖的scope对传递性依赖scope的影响:
-
表格的第一列,表示第一直接依赖的scope
-
表格的第一行,表示第二直接依赖的scope
-
行列相交的格子,表示在第一直接依赖和第二直接依赖scope的影响下,传递性依赖的scope。
-
例如,当第一直接依赖的scope为
test
,第二直接依赖的scope为compile
时,传递性依赖的scope为test
。 -
——
表示依赖不传递/ compile test provided runtime compile compile —— —— runtime test test —— —— test provided provided —— provided provided runtime runtime —— —— runtime
- 从表格可以总结出如下规律:
- 第二直接依赖的scope为
compile
时,传递性依赖的scope与第一直接依赖的scope一致 - 第二直接依赖的scope为
test
时,无论第一直接依赖的scope为何值,依赖都不会传递 - 第二直接依的scope为
provided
时,只有第一直接依赖的scope也是provided
时,传递才会传递,并且传递性依赖的scope也为provided
。 - 第二直接依赖的scope为
runtime
时,传递性依赖的scope与第一直接依赖的scope一致,但compile
除外。若第一直接依赖的scope为compile
,则传递性依赖的scope为runtime
。
- 总结: maven的依赖传递性,帮我们大大地简化了依赖的声明,使编程人员只需要关注直接依赖,而无需过多关注传递性依赖。
2.3 依赖调解(两个原则)
- 依赖的使用中,经常会遇到这样的情况:A → B → C → X(version为1.20),A → D → X(verison为1.25)。
- 在maven的依赖解析中,是坚决不允许同一依赖存在两个不同verison的引用的,即不允许依赖重复。
- 遇到上述情况,maven在进行依赖解析时,会选择 路径最近者优先的原则,只引入1.25版本的依赖X。 —— 我喜欢叫做,最短路径优先 😂
- 除了上述情况,这样的情况也有可能存在:A → B → X(version为1.20),A → C → X(verison为1.25)。
- 从maven2.0.9开始,针对上述情况将采用第一声明者优先原则,会引入先声明的1.20版本的依赖X。 —— 我喜欢叫做,FIFO 😂
2.4 可选依赖
-
上一篇博客,我们还提到了
<optional>
标签,它用于标志依赖是否可选。 -
其实,我并未使用过或者遇到过 😂
-
书中举了这样一种情况:A → B → X(可选),A → C → Y(可选)
① 项目B同时实现了两种特性,分别依赖X和Y。
② 这两种特性是互斥的,用户不可能同时使用这两种特性
③ 例如,实现了访问MySQL数据库和PostgreSQL的依赖B,其pom文件声明如下:<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.48</version> <optional>true</optional> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.5</version> <optional>true</optional> </dependency>
-
可选依赖X和Y只会对依赖B产生影响,当项目A依赖B时,依赖不会被传递。
-
这时,用户需要根据需要,在项目A的pom文件中,显式地引入依赖X和依赖Y。
-
注意: 一般,不要使用
<optional>
定义可选依赖,项目需要同时支持两个互斥的、可选的特性时,可以创建两个不同的模块。
3. 依赖的一些最佳实践
3.1 排除依赖
- 排除不稳定的传递性依赖: A → B → X(snapshot版本)
① 传递性依赖X是不稳定的snapshot版本,其不稳定性很可能会影响当前项目
② 需要通过<exclusions>
标签,从依赖B中去除不稳定的依赖X
③ 然后,在项目A的pom文件中显式声明稳定版本的依赖X - 排除不在maven中央仓库中的传递性依赖:A → B → X
① 依赖X由于版权原因,在maven中央仓库中是不存在的,即maven无法解析传递性依赖X
② 需要通过<exclusions>
标签,从依赖B中去除不存在的依赖X
③ 找到依赖X的替代依赖——依赖Y,在项目A的pom文件中显式声明依赖Y
3.2 依赖归类(版本声明)
-
在Java编程中,我们会经常定义一些常量
public static final String HTTP_HEAD = "http://";
-
定义常量的目的是,可能很多地方都会使用到该常量的值,如果我们需要将
http
改为https
,只需要修改常量的定义即可,而无需逐个修改值。 -
尤其是在进行spring编程时,我们会使用同一version的
spring-core
、spring-context
等依赖 -
这时,我们可以在pom文件
<properties>
中定义version,方便统一管理依赖的version -
以
kafka-client
的version为例:<properties> <kafka.versoin>2.3.1</kafka.versoin> </properties> <dependencies> <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>${kafka.versoin}</version> </dependency> </dependencies>
3.3 依赖的优化
3.3.1 已解析依赖
- maven会自动帮我们解析直接依赖和传递性依赖,能根据规则判断每个依赖的范围 (依赖的传递性),能根据两个原则 (最短路径、FIFO) 对依赖冲突进行调节等。
- 我们将通过maven解析后,最终得到的项目的所有依赖称为已解析依赖
- 使用如下命令,可以查看项目已解析依赖:
mvn dependency:list
3.3.2 依赖树
-
一般地,我们将项目的直接依赖称为顶层依赖,将顶层依赖的依赖,称为第二层依赖。以此类推,还有第三层、第四层依赖等。
-
通过如下命令,可以查看项目已解析依赖的树形结构:
mvn dependency:tree
-
自己某个项目的依赖树实例:
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ test --- [INFO] org.example:test:jar:1.0-SNAPSHOT [INFO] +- org.apache.kafka:kafka-clients:jar:2.3.1:compile [INFO] | +- com.github.luben:zstd-jni:jar:1.4.0-1:compile [INFO] | +- org.lz4:lz4-java:jar:1.6.0:compile [INFO] | +- org.xerial.snappy:snappy-java:jar:1.1.7.3:compile [INFO] | \- org.slf4j:slf4j-api:jar:1.7.26:compile [INFO] +- org.slf4j:slf4j-jdk14:jar:1.7.25:compile [INFO] +- commons-lang:commons-lang:jar:2.6:compile [INFO] +- joda-time:joda-time:jar:2.10.5:compile [INFO] +- com.facebook.airlift:json:jar:0.194:compile [INFO] | +- com.google.inject:guice:jar:4.2.2:compile [INFO] | | \- aopalliance:aopalliance:jar:1.0:compile [INFO] | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.9.8:compile [INFO] | +- com.fasterxml.jackson.core:jackson-core:jar:2.9.8:compile [INFO] | +- com.fasterxml.jackson.core:jackson-databind:jar:2.9.8:compile [INFO] | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.9.8:compile [INFO] | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.9.8:compile [INFO] | +- com.fasterxml.jackson.datatype:jackson-datatype-guava:jar:2.9.8:compile [INFO] | +- com.fasterxml.jackson.datatype:jackson-datatype-joda:jar:2.9.8:compile [INFO] | +- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.9.8:compile [INFO] | +- javax.inject:javax.inject:jar:1:compile [INFO] | \- com.google.guava:guava:jar:26.0-jre:compile [INFO] | +- com.google.code.findbugs:jsr305:jar:3.0.2:compile [INFO] | +- org.checkerframework:checker-qual:jar:2.5.2:compile [INFO] | +- com.google.errorprone:error_prone_annotations:jar:2.1.3:compile [INFO] | +- com.google.j2objc:j2objc-annotations:jar:1.1:compile [INFO] | \- org.codehaus.mojo:animal-sniffer-annotations:jar:1.14:compile [INFO] \- io.airlift:units:jar:1.3:compile [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------
3.3.3 未声明或未使用的依赖
- 如果参与社区项目的二次开发,你会发现社区项目对依赖有着严格管理,禁止出现
used undeclared dependencies
或unuesd declared dependencies
- 这时,可以借助以下命令对项目的当前依赖进行分析,已了解为声明或未使用的依赖:
mvn dependency:analyze
-
结果实例:
[INFO] --- maven-dependency-plugin:2.8:analyze (default-cli) @ test --- [WARNING] Used undeclared dependencies found: [WARNING] com.fasterxml.jackson.core:jackson-annotations:jar:2.9.8:compile [WARNING] com.google.guava:guava:jar:26.0-jre:compile [WARNING] com.fasterxml.jackson.core:jackson-databind:jar:2.9.8:compile [WARNING] org.slf4j:slf4j-api:jar:1.7.26:compile [WARNING] Unused declared dependencies found: [WARNING] org.slf4j:slf4j-jdk14:jar:1.7.25:compile [WARNING] joda-time:joda-time:jar:2.10.5:compile [WARNING] com.facebook.airlift:json:jar:0.194:compile [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------
① 未声明的依赖
- 从分析中可以看出,有些依赖我并未显式声明就使用了。例如,依赖
com.fasterxml.jackson.core:jackson-annotations
。 - 为何我的程序还能正常运行呢?那是因为,
com.facebook.airlift:json
中声明了该依赖,使得com.fasterxml.jackson.core:jackson-annotations
作为传递性依赖被引入了。 - 但是,通过A → B → X的方式,引入依赖X是具有潜在风险的。例如,若依赖B升级,可能会导致依赖X升级。这时,依赖X的接口可能很可能就变化了,之前可以使用的方法
func()
,现在就无法再o使用了。 - 总结: 对于未声明就使用的依赖,我们应该在项目中显式声明,而不是传递性依赖去引入。
② 未使用的依赖
- 上面的分析结果中,
org.slf4j:slf4j-jdk14
是一个已声明、但未使用的依赖。但是,我想使用org.apache.kafka:kafka-clients
就必须显式声明该依赖,否则程序无法运行。 - 而
joda-time:joda-time
是一个我已经显式声明了,但并未使用的依赖。我可以直接删除该依赖,对项目的运行无任何影响。 - 总结:
mvn dependency:analyze
命令,只能分析项目主代码和测试代码编译时需要使用到的依赖,一些测试或运行时需要的依赖它无法检测到。对于未使用的依赖,我们不能一味的删除,而是需要通过分析后再谨慎删除。
4. 总结
- 依赖范围与三种classpath的关系
- 传递性依赖的概念,以及第一直接依赖、第二直接依赖的scope对传递性依赖scope的影响
- maven中如何保证依赖不重复的 —— 依赖调解(两个原则)
- 可选依赖:
<optional>
为true
,依赖无法传递,需要显式声明;通过将一个模块拆为多个模块,解决可选依赖 - 依赖的一些最佳实践:
① 去除不稳定的/maven中央仓库无法支持的依赖
② 依赖的归类:像声明Java常量一样,声明依赖的版本号
③ 依赖的优化:三种命令 —— 查看已解析依赖、以树形方式查看已解析依赖、分析依赖(未声明/未使用)