本文从实际工作中的一个 bug 出发, 讲解了业务的背景, 分析了问题产生的原因, 介绍了解决问题的思路, 同时介绍了 Maven 的依赖机制.
业务场景
最近在工作中, 使用 Dubbo 调用远程服务, 需要依赖被调用方 (dubbo service provider) 提供的一些 jar 包.
下面是 maven 和 dubbo 的相关配置.
pom.xml
- <!-- 远程 dubbo 服务 -->
- <dependency>
- <groupId>com.dubbo.service.provider</groupId>
- <artifactId>foo-API</artifactId>
- <version>1.0</version>
- </dependency>
- dubbo-serivce-provider.xml
- <dubbo:reference id="fooService" interface="com.dubbo.service.provider.FooService" check="false" url="${dubbo.foo.server.address}"/>
发现问题
项目启动后, 出现如下异常
java.lang.NoSuchMethodError: com.google.common.base.Platform.systemNanoTime()
问题原因
通过 Eclipse 查看依赖树发现, foo-API 所依赖的 jar 与项目中的 jar 发生了冲突.
可以将如上场景抽象为下面的逻辑:
A 依赖
-> B
D 依赖
- -> A
- -> B
因为 Maven 拥有传递依赖的特性, 因此真实的依赖树是:
A 依赖
-> B
D 依赖
- -> A
- -> B
- -> B
因此 D 项目发生了依赖冲突.
相关知识: 依赖传递(Transitive Dependencies)
依赖传递 (Transitive Dependencies) 是 Maven 2.0 开始的提供的特性, 依赖传递的好处是不言而喻的, 可以让我们不需要去寻找和发现所必须依赖的库, 而是将会自动将需要依赖的库帮我们加进来.
例如 A 依赖了 B,B 依赖了 C 和 D, 那么你就可以在 A 中, 像主动依赖了 C 和 D 一样使用它们. 并且传递的依赖是没有数量和层级的限制的, 非常方便.
但依赖传递也不可避免的会带来一些问题, 例如:
当依赖层级很深的时候, 可能造成循环依赖(cyclic dependency)
当依赖的数量很多的时候, 依赖树会非常大
针对这些问题, Maven 提供了很多管理依赖的特性:
依赖调节(Dependency mediation)
依赖调节是为了解决版本不一致的问题(multiple versions), 并采取就近原则(nearest definition).
举例来说, A 项目通过依赖传递依赖了两个版本的 D:
A -> B -> C -> ( D 2.0) , A -> E -> (D 1.0),
那么最终 A 依赖的 D 的 version 将会是 1.0, 因为 1.0 对应的层级更少, 也就是更近.
依赖管理(Dependency management)
通过声明 Dependency management, 可以大大简化子 POM 的依赖声明.
举例来说项目 A,B,C,D 都有共同的 Parent, 并有类似的依赖声明如下:
- A,B,C,D/pom.xml
- <dependencies>
- <dependency>
- <groupId>group-a</groupId>
- <artifactId>artifact-a</artifactId>
- <version>1.0</version>
- <exclusions>
- <exclusion>
- <groupId>group-c</groupId>
- <artifactId>excluded-artifact</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <dependency>
- <groupId>group-a</groupId>
- <artifactId>artifact-b</artifactId>
- <version>1.0</version>
- <type>bar</type>
- <scope>runtime</scope>
- </dependency>
- </dependencies>
如果父 pom 声明了如下的 Dependency management:
Parent/pom.xml
- <dependencyManagement>
- <dependencies>
- <dependency>
- <groupId>group-a</groupId>
- <artifactId>artifact-a</artifactId>
- <version>1.0</version>
- <exclusions>
- <exclusion>
- <groupId>group-c</groupId>
- <artifactId>excluded-artifact</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <dependency>
- <groupId>group-c</groupId>
- <artifactId>artifact-b</artifactId>
- <version>1.0</version>
- <type>war</type>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>group-a</groupId>
- <artifactId>artifact-b</artifactId>
- <version>1.0</version>
- <type>bar</type>
- <scope>runtime</scope>
- </dependency>
- </dependencies>
- </dependencyManagement>
那么子项目的依赖声明会非常简单:
- A,B,C,D/pom.xml
- <dependencies>
- <dependency>
- <groupId>group-a</groupId>
- <artifactId>artifact-a</artifactId>
- </dependency>
- <dependency>
- <groupId>group-a</groupId>
- <artifactId>artifact-b</artifactId>
- <!-- 依赖的类型, 对应于项目坐标定义的 packaging. 大部分情况下, 该元素不必声明, 其默认值是 jar.-->
- <type>bar</type>
- </dependency>
- </dependencies>
依赖范围(Dependency scope)
Maven 在编译主代码的时候需要使用一套 classpath, 在编译和执行测试的时候会使用另一套 classpath, 实际运行项目的时候, 又会使用一套 classpath.
依赖范围就是用来控制依赖与这三种 classpath(编译 classpath, 测试 classpath, 运行 classpath)的关系的, Maven 有以下几种依赖范围:
compile: 编译依赖范围.
如果没有指定, 就会默认使用该依赖范围.
使用此依赖范围的 Maven 依赖, 对于编译, 测试, 运行三种 classpath 都有效.
test: 测试依赖范围.
使用此依赖范围的 Maven 依赖, 只对于测试 classpath 有效, 在编译主代码或者运行项目的使用时将无法使用此类依赖.
典型例子是 JUnit, 它只有在编译测试代码及运行测试的时候才需要.
provided: 已提供依赖范围.
使用此依赖范围的 Maven 依赖, 对于编译和测试 classpath 有效, 但在运行时无效.
典型例子是 servlet-API, 编译和测试项目的时候需要该依赖, 但在运行项目的时候, 由于容器已经提供, 就不需要 Maven 重复地引入一遍.
runtime: 运行时依赖范围.
使用此依赖范围的 Maven 依赖, 对于测试和运行 classpath 有效, 但在编译主代码时无效.
典型例子是 JDBC 驱动实现, 项目主代码的编译只需要 JDK 提供的 JDBC 接口, 只有在执行测试或者运行项目的时候才需要实现上述接口的具体 JDBC 驱动.
system: 系统依赖范围.
该依赖与三种 classpath 的关系, 和 provided 依赖范围完全一致. 但使用 system 范围依赖时必须通过 systemPath 元素显式地指定依赖文件的路径. 由于此类依赖不是通过 Maven 仓库解析的, 而且往往与本机系统绑定, 可能造成构建的不可移植, 因此应该谨慎使用.
systemPath 元素可以引用环境变量:
- <dependency>
- <groupId>com.system</groupId>
- <artifactId>foo</artifactId>
- <version>1.0</version>
- <scope>system</scope>
- <systemPath>${maven.home}/lib/foo.jar</systemPath>
- </dependency>
import(Maven 2.0.9 及以上): 导入依赖范围.
我们知道, maven 的继承和 java 是一样的, 只能单继承. 因此, 父 pom 可能非常庞大, 如果你想把依赖分类清晰的进行管理, 就更不可能了.
import scope 依赖能解决这个问题. 你可以把 Dependency Management 放到单独用来管理依赖的 pom 中, 然后在需要使用依赖的模块中通过 import scope 依赖, 就可以引入 dependencyManagement.
例如, 父 pom.xml:
- <project>
- <modelVersion>4.0.0</modelVersion>
- <groupId>com.test.sample</groupId>
- <artifactId>base-parent1</artifactId>
- <packaging>pom</packaging>
- <version>1.0.0-SNAPSHOT</version>
- <dependencyManagement>
- <dependencies>
- <dependency>
- <groupId>junit</groupId>
- <artifactid>junit</artifactId>
- <version>4.8.2</version>
- </dependency>
- <dependency>
- <groupId>log4j</groupId>
- <artifactid>log4j</artifactId>
- <version>1.2.16</version>
- </dependency>
- </dependencies>
- </dependencyManagement>
- </project>
通过非继承的方式来引入这段依赖管理配置:
- <dependencyManagement>
- <dependencies>
- <dependency>
- <groupId>com.test.sample</groupId>
- <artifactid>base-parent1</artifactId>
- <version>1.0.0-SNAPSHOT</version>
- <type>pom</type>
- <scope>import</scope>
- </dependency>
- </dependencies>
- </dependencyManagement>
- <dependency>
- <groupId>junit</groupId>
- <artifactid>junit</artifactId>
- </dependency>
- <dependency>
- <groupId>log4j</groupId>
- <artifactid>log4j</artifactId>
- </dependency>
注意: import scope 只能用在 dependencyManagement 里面
排除依赖(Excluded dependencies)
排除不需要从所依赖的项目中传递过来的依赖, 好比你买车的时候, 主动跟卖车的说明不需要买车附加的保险业务. 下面在解决思路中会举例说明.
可选依赖(Optional dependencies)
被依赖的项目主动不把可以传递的依赖传递下去, 好比卖车的主动声明自己不会让买车的人买这辆车附加的保险业务. 下面在解决思路中会举例说明.
解决思路
有了上面的知识背景, 考虑使用 Maven 提供的 Optional 和 Exclusions 来控制依赖的传递.
- A
- -> B
- D
- -> A
- -> B
Optional 定义后, 该依赖只能在本项目中传递, 不会传递到引用该项目的父项目中, 父项目需要主动引用该依赖才行.
A/pom.xml
- <dependency>
- <groupId>com.bar</groupId>
- <artifactId>B</artifactId>
- <version>1.0</version>
- <optional>true</optional>
- </dependency>
这种情况下, A 对 B 的依赖将不会传递给 D.
Exclusions 则是主动排除子项目传递过来的依赖.
D/pom.xml
- <dependency>
- <groupId>com.bar</groupId>
- <artifactId>A</artifactId>
- <version>1.0</version>
- <exclusions>
- <exclusion>
- <groupId>com.bar</groupId>
- <artifactId>B</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
这种情况下, D 对 A 的依赖将不会包含 B.
开始提到的问题就是通过 exclusion 的方式解决的.
总结
Maven 的依赖机制 (Dependency Mechanism) 是 Maven 最著名的特性, 并且是 Maven 在依赖管理领域中最令人称道的. 因此, 对 Maven 的依赖机制有深入的理解, 对使用 Maven 非常必要.
-- 拉斐尔《雅典学院》
来源: https://juejin.im/post/5c7a3f1be51d45714352390e