Spring Boot Starter 依赖冲突排查:一场代码侦探之旅
大家好,今天我们来聊聊 Spring Boot 开发中一个让人头疼的问题:依赖冲突。Spring Boot 的 Starter 设计旨在简化依赖管理,但当项目变得复杂,引入多个 Starter 时,依赖冲突就像一颗不定时炸弹,随时可能引爆,导致应用启动失败或者运行时出现各种奇怪的行为。
与其在错误发生时手忙脚乱,不如掌握一些快速排查和解决依赖冲突的技巧,做一名优秀的“代码侦探”,将问题扼杀在摇篮里。
1. 理解依赖冲突的本质
在深入排查技巧之前,我们需要理解依赖冲突的本质。当项目中存在同一个 Jar 包的不同版本时,就会发生依赖冲突。JVM 在加载类时,只会加载第一个遇到的版本,这可能导致:
- ClassNotFoundException/NoClassDefFoundError: 如果代码尝试使用一个不存在的类,通常是因为所需的版本被低版本的 Jar 包覆盖。
- NoSuchMethodError/NoSuchFieldError: 如果代码尝试调用一个不存在的方法或访问一个不存在的字段,通常是因为方法的签名或字段在被加载的版本中发生了改变。
- 版本不兼容: 即使类和方法存在,但不同版本之间可能存在不兼容,导致运行时出现意想不到的错误。
2. 借助构建工具的力量:Maven/Gradle 的依赖分析
Maven 和 Gradle 都提供了强大的依赖分析工具,可以帮助我们快速定位冲突的 Jar 包。
2.1 Maven:
Maven 提供了 mvn dependency:tree 命令,可以生成项目的依赖树,清晰地展示每个依赖项的传递依赖关系。
用法:
在项目根目录下运行:
mvn dependency:tree
解读:
输出结果会以树状结构展示项目的依赖关系。如果同一个 Jar 包出现多个版本,就会在输出中有所体现。 例如:
[INFO] --- maven-dependency-plugin:3.1.2:tree (default-cli) @ my-spring-boot-app ---
[INFO] com.example:my-spring-boot-app:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.7.5:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:2.7.5:compile
[INFO] | | +- org.springframework.boot:spring-boot:jar:2.7.5:compile
[INFO] | | +- org.springframework.boot:spring-boot-autoconfigure:jar:2.7.5:compile
[INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:2.7.5:compile
[INFO] | | | +- ch.qos.logback:logback-classic:jar:1.2.11:compile
[INFO] | | | | +- ch.qos.logback:logback-core:jar:1.2.11:compile
[INFO] | | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.17.2:compile
[INFO] | | | | +- org.apache.logging.log4j:log4j-api:jar:2.17.2:compile
[INFO] | | | +- org.slf4j:jul-to-slf4j:jar:1.7.36:compile
[INFO] | | +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-json:jar:2.7.5:compile
[INFO] | | +- com.fasterxml.jackson.core:jackson-databind:jar:2.13.4:compile
[INFO] | | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.13.4:compile
[INFO] | | | +- com.fasterxml.jackson.core:jackson-core:jar:2.13.4:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.13.4:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.13.4:compile
[INFO] | | +- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.13.4:compile
[INFO] | +- org.springframework:spring-web:jar:5.3.23:compile
[INFO] | | +- org.springframework:spring-beans:jar:5.3.23:compile
[INFO] | +- org.springframework:spring-webmvc:jar:5.3.23:compile
[INFO] | | +- org.springframework:spring-aop:jar:5.3.23:compile
[INFO] | | +- org.springframework:spring-context:jar:5.3.23:compile
[INFO] | | +- org.springframework:spring-expression:jar:5.3.23:compile
[INFO] +- org.springframework.boot:spring-boot-starter-data-jpa:jar:2.7.5:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-aop:jar:2.7.5:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-jdbc:jar:2.7.5:compile
[INFO] | | +- com.zaxxer:HikariCP:jar:4.0.3:compile
[INFO] | | +- org.springframework:spring-jdbc:jar:5.3.23:compile
[INFO] | +- jakarta.transaction:jakarta.transaction-api:jar:1.3.3:compile
[INFO] | +- org.hibernate:hibernate-core:jar:5.6.12.Final:compile
[INFO] | | +- org.jboss.logging:jboss-logging:jar:3.4.3.Final:compile
[INFO] | | +- net.bytebuddy:byte-buddy:jar:1.12.17:compile
[INFO] | | +- antlr:antlr:jar:2.7.7:compile
[INFO] | | +- org.jboss:jandex:jar:2.2.3.Final:compile
[INFO] | | +- com.fasterxml:classmate:jar:1.5.1:compile
[INFO] | | +- org.hibernate.common:hibernate-commons-annotations:jar:5.1.2.Final:compile
[INFO] | | +- org.glassfish.jaxb:jaxb-runtime:jar:2.3.6:compile
[INFO] | | | +- org.glassfish.jaxb:txw2:jar:2.3.6:compile
[INFO] | | | +- com.sun.istack:istack-commons-runtime:jar:3.0.12:compile
[INFO] | +- org.springframework.data:spring-data-jpa:jar:2.7.5:compile
[INFO] | | +- org.springframework.data:spring-data-commons:jar:2.7.5:compile
[INFO] | | +- org.springframework:spring-orm:jar:5.3.23:compile
[INFO] | | +- org.springframework:spring-tx:jar:5.3.23:compile
[INFO] | +- org.aspectj:aspectjrt:jar:1.9.7:compile
[INFO] | +- org.slf4j:slf4j-api:jar:1.7.36:compile
[INFO] +- com.h2database:h2:jar:2.1.214:runtime
[INFO] +- org.projectlombok:lombok:jar:1.18.24:provided
[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:2.7.5:test
[INFO] | +- org.springframework.boot:spring-boot-test:jar:2.7.5:test
[INFO] | +- org.springframework.boot:spring-boot-test-autoconfigure:jar:2.7.5:test
[INFO] | +- com.jayway.jsonpath:json-path:jar:2.7.0:test
[INFO] | | +- net.minidev:json-smart:jar:2.4.8:test
[INFO] | | | +- net.minidev:accessors-smart:jar:2.4.8:test
[INFO] | | | | +- org.ow2.asm:asm:jar:9.1:test
[INFO] | +- org.hamcrest:hamcrest:jar:2.2:test
[INFO] | +- org.junit.jupiter:junit-jupiter:jar:5.8.2:test
[INFO] | | +- org.junit.jupiter:junit-jupiter-api:jar:5.8.2:test
[INFO] | | | +- org.opentest4j:opentest4j:jar:1.2.0:test
[INFO] | | | +- org.junit.platform:junit-platform-commons:jar:1.8.2:test
[INFO] | | | +- org.apiguardian:apiguardian-api:jar:1.1.2:test
[INFO] | | +- org.junit.jupiter:junit-jupiter-params:jar:5.8.2:test
[INFO] | | +- org.junit.jupiter:junit-jupiter-engine:jar:5.8.2:test
[INFO] | | | +- org.junit.platform:junit-platform-engine:jar:1.8.2:test
[INFO] | +- org.mockito:mockito-core:jar:4.5.1:test
[INFO] | | +- net.bytebuddy:byte-buddy-agent:jar:1.12.17:test
[INFO] | | +- org.objenesis:objenesis:jar:3.2:test
[INFO] | +- org.mockito:mockito-junit-jupiter:jar:4.5.1:test
[INFO] | +- org.skyscreamer:jsonassert:jar:1.5.1:test
[INFO] | | +- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test
[INFO] | +- org.springframework:spring-core:jar:5.3.23:compile
[INFO] | | +- org.springframework:spring-jcl:jar:5.3.23:compile
[INFO] | +- org.springframework:spring-test:jar:5.3.23:test
如果发现同一个 Jar 包的不同版本,例如:
[INFO] +- com.fasterxml.jackson.core:jackson-databind:jar:2.13.4:compile
[INFO] | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.13.4:compile
[INFO] | +- com.fasterxml.jackson.core:jackson-core:jar:2.13.4:compile
...
[INFO] +- com.fasterxml.jackson.core:jackson-databind:jar:2.12.7:compile <--- 冲突
[INFO] | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.12.7:compile
[INFO] | +- com.fasterxml.jackson.core:jackson-core:jar:2.12.7:compile
这意味着 jackson-databind 存在 2.13.4 和 2.12.7 两个版本。
2.2 Gradle:
Gradle 提供了 gradle dependencies 命令,可以生成项目的依赖树。
用法:
在项目根目录下运行:
gradle dependencies
解读:
Gradle 的输出同样会以树状结构展示依赖关系。与 Maven 类似,需要寻找同一个 Jar 包的不同版本。 Gradle 提供了更加灵活的配置,可以针对特定的配置(例如 compileClasspath、testRuntimeClasspath)查看依赖树。
gradle dependencies --configuration compileClasspath
使用 Gradle Dependency Analysis Plugin
更强大的是 Gradle 的 Dependency Analysis Plugin。这个插件会分析你的代码,并告诉你哪些依赖实际上被使用了,哪些没有被使用。这对于移除不必要的依赖和解决冲突非常有帮助。
在 build.gradle 文件中添加:
plugins {
id("com.autonomousapps.dependency-analysis") version "7.1.5" // 替换为最新版本
}
然后运行:
gradle analyzeDependencies
这个插件会生成报告,告诉你哪些依赖是 unused, declared 却 notUsed 等等。
3. 定位冲突根源:排除传递依赖
找到冲突的 Jar 包后,下一步是确定哪些依赖项引入了这些冲突的版本。Maven 和 Gradle 都提供了排除传递依赖的机制。
3.1 Maven:
使用 <exclusions> 标签可以排除传递依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.5</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
</exclusions>
</dependency>
上面的配置表示排除 spring-boot-starter-web 引入的 jackson-databind 依赖。
注意: 排除传递依赖可能会导致其他问题,需要仔细测试。
3.2 Gradle:
使用 exclude 方法可以排除传递依赖。
dependencies {
implementation('org.springframework.boot:spring-boot-starter-web:2.7.5') {
exclude group: 'com.fasterxml.jackson.core', module: 'jackson-databind'
}
}
与 Maven 类似,排除依赖需要谨慎。
4. 版本控制:强制使用特定版本
如果排除传递依赖不可行,可以强制使用特定版本的 Jar 包。
4.1 Maven:
使用 <dependencyManagement> 标签可以管理依赖版本。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.4</version>
</dependency>
</dependencies>
</dependencyManagement>
上面的配置表示强制使用 jackson-databind 的 2.13.4 版本。
4.2 Gradle:
在 dependencies 块中直接指定版本。
dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4'
}
注意: 强制指定版本可能会导致与其他依赖项不兼容,需要仔细测试。
5. 利用 IDE 的依赖分析工具
大多数 IDE(例如 IntelliJ IDEA 和 Eclipse)都提供了依赖分析工具,可以图形化地展示依赖关系,并帮助定位冲突。
IntelliJ IDEA:
- 打开
pom.xml或build.gradle文件。 - 在编辑器中右键单击,选择 "Maven" -> "Show Dependencies" 或 "Gradle" -> "Show Dependencies"。
- IDE 会打开一个依赖图,可以清晰地看到依赖关系和冲突。
Eclipse:
- 打开
pom.xml文件。 - 切换到 "Dependency Hierarchy" 选项卡。
- Eclipse 会展示依赖树,并用特殊颜色标记冲突的 Jar 包。
6. 运行时排查:使用类加载器分析工具
如果以上方法都无法解决问题,可以尝试在运行时使用类加载器分析工具,例如:
- JVM 类加载器日志: 可以通过配置 JVM 参数,开启类加载器日志,查看哪些类被加载,以及从哪个 Jar 包加载。
- JProfiler/YourKit: 这些商业 JVM 性能分析工具提供了强大的类加载器分析功能,可以帮助定位问题的根源。
开启 JVM 类加载器日志:
在启动 JVM 时添加以下参数:
-verbose:class
这会将所有加载的类的信息输出到控制台。
7. 常见依赖冲突场景及解决方案
7.1 Spring Data JPA 与 Hibernate 版本冲突:
Spring Data JPA 依赖于 Hibernate,但 Spring Boot 版本和 Hibernate 版本之间可能存在兼容性问题。
解决方案:
- 升级或降级 Spring Boot 版本,使其与 Hibernate 版本兼容。
- 使用
<dependencyManagement>或 Gradle 的版本管理功能,强制指定 Hibernate 版本。
7.2 Jackson 版本冲突:
多个 Starter 可能会引入不同版本的 Jackson,导致序列化和反序列化问题。
解决方案:
- 使用
<dependencyManagement>或 Gradle 的版本管理功能,强制指定 Jackson 版本。 - 排除传递依赖,只保留一个版本的 Jackson。
7.3 SLF4J 实现冲突:
SLF4J 只是一个日志接口,需要选择一个具体的实现(例如 Logback、Log4j 2)。如果项目中存在多个 SLF4J 实现,会导致冲突。
解决方案:
- 排除不需要的 SLF4J 实现。
- 确保只有一个 SLF4J 实现被引入。
8. 依赖冲突排查 checklist
为了方便大家快速排查依赖冲突,这里提供一个 checklist:
| 步骤 | 描述 | 工具/命令 |
|---|---|---|
| 1 | 运行 mvn dependency:tree 或 gradle dependencies |
mvn dependency:tree, gradle dependencies |
| 2 | 寻找同一个 Jar 包的不同版本 | 输出结果 |
| 3 | 使用 <exclusions> 或 exclude 排除传递依赖 |
pom.xml, build.gradle |
| 4 | 使用 <dependencyManagement> 或版本管理强制指定版本 |
pom.xml, build.gradle |
| 5 | 使用 IDE 的依赖分析工具 | IntelliJ IDEA, Eclipse |
| 6 | 开启 JVM 类加载器日志或使用 JVM 性能分析工具 | -verbose:class, JProfiler, YourKit |
9. 避免依赖冲突的良好实践
- 保持 Spring Boot 版本一致: 尽量使用最新的稳定版 Spring Boot,并保持项目中所有 Starter 的版本一致。
- 谨慎引入第三方依赖: 在引入第三方依赖之前,仔细评估其依赖关系,避免引入不必要的依赖。
- 定期检查依赖关系: 定期运行依赖分析工具,检查是否存在潜在的冲突。
- 编写单元测试: 编写充分的单元测试,可以帮助及早发现依赖冲突导致的问题。
- 模块化设计: 将项目拆分成多个模块,可以降低依赖冲突的风险。
一些思考:依赖管理,永无止境
依赖冲突是软件开发中不可避免的问题,尤其是在使用 Spring Boot 这样依赖管理复杂的框架时。我们需要不断学习新的工具和技巧,才能更好地应对这些挑战。希望今天的分享能够帮助大家在 Spring Boot 开发中少踩坑,提高开发效率。记住,解决依赖冲突就像一场代码侦探游戏,需要耐心、细致和一点点运气。