乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 介绍与核心概念

    • Maven是什么
    • 约定优于配置
  • 第2章 安装与配置

    • 安装与验证
    • settings.xml
    • 本地仓库与镜像
  • 第3章 POM与项目坐标

    • POM
    • GAV坐标
    • packaging
  • 第4章 标准目录布局

    • 标准目录布局
  • 第5章 依赖机制

    • dependencies
    • scope
    • 依赖传递
    • 依赖冲突与调解
    • exclusions
    • optional
    • dependencyManagement
  • 第6章 仓库

    • 仓库体系
    • 本地仓库
    • 远程仓库与镜像
    • 私服
  • 第7章 构建生命周期

    • 生命周期概述
    • clean 生命周期
    • default 生命周期
    • site 生命周期
    • 生命周期与插件绑定
  • 第8章 插件

    • 插件概述
    • maven-compiler-plugin
    • maven-surefire-plugin
    • maven-war-plugin
  • 第9章 继承与聚合

    • parent继承
    • 聚合
    • BOM
    • properties
  • 第10章 属性与资源过滤

    • 资源过滤
    • Profile
  • 第11章 常用命令

    • mvn compile
    • mvn test
    • mvn package
    • mvn clean
    • mvn install
    • mvn dependency:tree
  • 第12章 常见问题与最佳实践

    • 依赖冲突排查
    • 最佳实践

依赖冲突排查

本章是"常见问题与最佳实践"的开篇。系统化的依赖冲突排查能力,是 Maven 开发者从"入门"走向"熟练"的分水岭——依赖冲突不会消失,只会以不同形式反复出现。


核心机制

Maven 使用"最近定义"算法解决依赖冲突。如果在依赖树中发现同一个依赖的两个版本,离根项目最近的那个被选中。如果距离相同,先声明的那个胜出。

理解这一机制,是系统化排查冲突的理论基础。但"知道规则"和"快速定位问题"之间还有很长的距离——本章将填补这段距离。

依赖冲突的三种典型场景

场景描述示例
版本冲突不同分支引入同一 artifact 的不同版本A → C:1.0,B → C:2.0
范围冲突同一依赖在不同路径声明了不同 scopeA → C (compile),B → C (provided)
类型冲突同一坐标但 packaging 不同C:jar vs C:pom

日常开发中,版本冲突占 90% 以上,是排查的重点。

Maven 的冲突解决规则

  1. 最近优先(Nearest Definition):路径短的版本胜出
  2. 先声明优先(First Declaration):路径相同时,先声明的依赖所带的版本胜出
  3. dependencyManagement 覆盖:父 POM 或当前 POM 的 <dependencyManagement> 具有最高优先级,直接覆盖传递依赖的版本

排查工具箱

Maven 提供了多个工具用于不同层面的排查:

工具/命令作用使用场景
mvn dependency:tree查看完整依赖树了解依赖全貌
mvn dependency:tree -Dverbose查看被省略的版本定位具体冲突
mvn dependency:analyze分析依赖使用情况发现未使用依赖和缺失依赖
mvn dependency:analyze-duplicate查找重复依赖发现 pom.xml 中的重复声明
mvn dependency:analyze-dep-mgt分析 dependencyManagement检查锁定是否生效

生活类比:医院分诊系统

想象你(架构师白歌)是一家医院的主任医师,依赖冲突就是来就诊的各种病人:

  • dependency:tree 是门诊挂号系统,让你看到今天所有病人的名单和症状(依赖全貌)
  • dependency:tree -Dverbose 是详细病历,告诉你每个病人被转诊了几次、之前被哪些医院拒诊过(被省略的版本)
  • dependency:analyze 是体检中心,发现有些病人其实没病(未使用依赖),有些病人漏诊了(缺失依赖)
  • <dependencyManagement> 是专家会诊制度,由资深医生(父 POM)统一制定治疗方案,避免各科室各自为政

图示

上图展示了系统化的依赖冲突排查流程:从症状识别到诊断工具使用,再到治疗方案选择和最终验证。这个流程是飞翔科技技术部处理依赖问题的标准操作程序(SOP)。


完整示例

场景

飞翔科技的 employee-system 项目在升级到 Spring Boot 3.2 后,启动时抛出异常:

java.lang.NoSuchMethodError: 'void org.springframework.core.io.ResourceDescriptor.<init>(...)'

后端小崔排查了一天无果,架构师白歌介入,要求按标准流程系统化排查。

操作前:项目状态

pom.xml 中的相关依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>3.2.0</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>2.1.2</version>  <!-- 旧版本,兼容 Spring 5.x -->
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.83</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.2</version>
    </dependency>
</dependencies>

操作步骤

步骤一:查看依赖树定位冲突

mvn dependency:tree -Dverbose

关键输出:

[INFO] com.feixiang:employee-system:jar:1.0.0
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:3.2.0:compile
[INFO] |  \- org.springframework:spring-core:jar:6.1.1:compile
[INFO] +- org.mybatis:mybatis-spring:jar:2.1.2:compile
[INFO] |  \- org.springframework:spring-core:jar:5.3.21:compile
[INFO] |     \- (org.springframework:spring-core:jar:6.1.1:compile - omitted for conflict with 6.1.1)

诊断结论:mybatis-spring:2.1.2 依赖 spring-core:5.3.21,但 spring-boot-starter-web:3.2.0 已经传递引入了 spring-core:6.1.1。由于 spring-core:6.1.1 路径更短,它胜出,5.3.21 被省略。mybatis-spring:2.1.2 是为 Spring 5.x 编译的,在 Spring 6.x 下运行时出现 NoSuchMethodError。

步骤二:使用 dependency:analyze 检查依赖健康度

mvn dependency:analyze

输出:

[WARNING] Used undeclared dependencies found:
[WARNING]    org.springframework:spring-core:jar:6.1.1:compile
[WARNING]    org.springframework:spring-web:jar:6.1.1:compile
[WARNING] Unused declared dependencies found:
[WARNING]    com.alibaba:fastjson:jar:1.2.83:compile

诊断结论:

  • Used undeclared:项目代码直接使用了 spring-core 和 spring-web,但 pom.xml 中没有显式声明它们(是通过传递依赖引入的)。这很危险——如果某天传递路径变化,这些依赖可能消失。
  • Unused declared:fastjson 被声明了但没有被代码使用,属于冗余依赖,应删除。

步骤三:应用治疗方案

方案一:升级 mybatis-spring 到兼容版本(首选)

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>3.0.3</version>  <!-- 兼容 Spring 6.x / Spring Boot 3.x -->
</dependency>

方案二:用 dependencyManagement 锁定 spring-core 版本(兜底)

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>6.1.1</version>
        </dependency>
    </dependencies>
</dependencyManagement>

方案三:排除冲突依赖(当无法升级时使用)

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.1.2</version>
    <exclusions>
        <exclusion>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

清理冗余依赖:

<!-- 删除未使用的 fastjson -->
<!-- <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.83</version>
</dependency> -->

补充显式声明(解决 Used undeclared):

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
</dependency>

步骤四:验证修复

mvn clean dependency:tree -Dverbose

确认输出中不再出现 spring-core 的冲突标记:

[INFO] +- org.mybatis:mybatis-spring:jar:3.0.3:compile
[INFO] |  \- org.springframework:spring-core:jar:6.1.1:compile

然后执行测试和启动验证:

mvn clean test
mvn spring-boot:run

常见冲突场景和解决方案

场景症状根因解决方案
Spring 版本混用NoSuchMethodError / NoClassDefFoundErrorSpring Boot 3.x 与 Spring 5.x 组件混用升级组件到兼容 Spring 6.x 的版本
Jackson 版本冲突JSON 序列化异常多个组件依赖不同 Jackson 版本<dependencyManagement> 统一锁定
SLF4J 绑定冲突Multiple SLF4J bindings 警告同时引入 slf4j-log4j12 和 logback-classic排除其中一个绑定
Servlet API 冲突Tomcat 启动失败servlet-api 以 compile scope 引入,与容器冲突改为 <scope>provided</scope>
日志框架冲突日志不输出或重复输出log4j / log4j2 / logback 混用统一为 logback + slf4j,排除其他

易错点与常见问题

误区一:依赖冲突只发生在运行时

错误认知:"我的项目编译通过了,所以依赖没有冲突。"

纠正:Maven 的依赖冲突解决机制在编译期就生效了——它选择了一个版本进入 classpath,另一个被省略。如果选中的版本恰好满足编译需求(类名、方法签名兼容),项目可以编译通过。但运行时如果代码实际执行到了被省略版本才有的方法,就会抛出 NoSuchMethodError。因此,编译通过不等于没有冲突。

误区二:<dependencyManagement> 会引入依赖

错误认知:"我在 <dependencyManagement> 里声明了依赖,所以它会进入 classpath。"

纠正:<dependencyManagement> 只管理版本,不引入依赖。它定义了"如果某个依赖被引入,应该用什么版本"。真正的依赖引入仍然需要在 <dependencies> 中声明。例如:

<!-- 只管理版本,不引入依赖 -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>6.1.1</version>
        </dependency>
    </dependencies>
</dependencyManagement>

<!-- 这里才真正引入依赖 -->
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <!-- 版本从 dependencyManagement 继承,无需写 version -->
    </dependency>
</dependencies>

误区三:排除依赖是万能药

错误认知:"只要用 <exclusions> 把冲突的版本排除掉,问题就解决了。"

纠正:<exclusions> 是治标不治本的手段。它阻止了某个传递依赖的引入,但如果被排除的依赖是运行时必需的,会导致 ClassNotFoundException。正确的优先级是:

  1. 升级直接依赖:让组件使用兼容的版本(首选)
  2. dependencyManagement 锁定:统一版本(次选)
  3. exclusions 排除:无法升级时的兜底方案

误区四:dependency:analyze 的警告都要修复

错误认知:"dependency:analyze 报了很多 Used undeclared,我要全部显式声明。"

纠正:Used undeclared 确实意味着代码直接使用了某个传递依赖的类,但并非所有情况都需要显式声明。对于 Spring Boot 项目,spring-boot-starter-* 是一组精心设计的"starter"依赖,它们传递引入的组件是官方保证的。如果你显式声明了所有传递依赖,反而失去了 starter 的简化价值。建议只对你直接调用 API 的依赖进行显式声明。


小结

依赖冲突是 Maven 项目中最常见也最棘手的问题。系统化的排查流程是:

  1. 识别症状:NoClassDefFoundError、NoSuchMethodError 等
  2. 查看依赖树:mvn dependency:tree -Dverbose 定位冲突版本
  3. 分析依赖健康度:mvn dependency:analyze 发现未使用和缺失依赖
  4. 选择治疗方案:升级依赖 → dependencyManagement 锁定 → exclusions 排除
  5. 验证修复:重新查看依赖树,运行测试,部署验证

核心要点:

  • Maven 冲突解决原则是"最近优先、先声明优先"
  • -Dverbose 是排查冲突的必备参数
  • <dependencyManagement> 只管理版本,不引入依赖
  • 升级依赖版本是首选方案,exclusions 是兜底手段
  • 编译通过不等于没有冲突,运行时问题更隐蔽

本章与全局的关系:本章系统化了依赖冲突的排查方法。下一章"最佳实践"将从预防角度,总结 Maven 项目管理的规范清单,让冲突在发生前就被规避。

下一页
最佳实践