exclusions
本章承接"依赖冲突与调解",深入讲解 Maven 的依赖排除机制。理解
exclusions,是精确修剪依赖树、消除冗余传递依赖和解决版本冲突的必备技能。
核心机制
exclusions 用于显式阻止某个依赖的传递依赖进入当前项目的依赖树。被排除的依赖不会参与编译、测试、打包,也不会继续向上一层传递。
这句话的关键词是显式阻止。如果说依赖传递是 Maven 的"自动补全",那么 exclusions 就是开发者对自动补全结果的"人工审核和删除"。
为什么需要排除?
即使 Maven 的依赖调解能自动选择版本,以下场景仍然需要显式排除:
- 消除冗余依赖:A 传递了 B,但你的项目已经通过其他路径引入了 B,且不需要 A 带来的版本
- 解决版本冲突:A 传递了 C:1.0,B 传递了 C:2.0,调解结果不符合预期,需要切断其中一条路径
- 减少打包体积:A 传递了 10 个库,但你只用到 A 的核心功能,不需要它的全部生态
- 替换实现:A 传递了
log4j,但你想用logback,排除log4j后自行声明logback
exclusions 的语法
exclusions 是 <dependency> 的子元素,可以排除一个或多个传递依赖:
<dependency>
<groupId>com.example</groupId>
<artifactId>A</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>com.unwanted</groupId>
<artifactId>troublemaker</artifactId>
</exclusion>
<!-- 可以排除多个 -->
<exclusion>
<groupId>com.legacy</groupId>
<artifactId>old-lib</artifactId>
</exclusion>
</exclusions>
</dependency>
注意:exclusion 中不需要写 version。Maven 通过 groupId + artifactId 就能定位要排除的依赖,无论它是什么版本。
排除的生效范围
排除只在当前依赖声明中生效。如果你在其他地方也依赖了同一个库,那里的传递不受影响。例如:
<!-- 这里排除了 C -->
<dependency>
<groupId>com.example</groupId>
<artifactId>A</artifactId>
<exclusions>
<exclusion>
<groupId>com.shared</groupId>
<artifactId>C</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 这里 C 仍然会通过 B 传递进来 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>B</artifactId>
</dependency>
如果 B 也传递了 C,C 仍然会通过 B 进入依赖树。要彻底排除 C,需要在所有引入 C 的直接依赖中都加 exclusion,或者在父 POM 的 dependencyManagement 中统一处理。
生活类比:定制套餐的"不要洋葱"
想象你在餐厅点了一份"招牌汉堡套餐"(依赖 A),套餐默认包含薯条、可乐和洋葱圈(传递依赖)。但你:
- 对洋葱过敏(排除
troublemaker) - 已经自带了可乐(排除
old-lib,自行声明替代版本)
你对服务员说:"我要招牌汉堡套餐,不要洋葱圈"——这就是 exclusions。服务员(Maven)在配餐时,把洋葱圈从这份套餐里拿掉。但如果你同时点了"儿童套餐"(依赖 B),而儿童套餐也包含洋葱圈,除非你明确说"儿童套餐也不要洋葱圈",否则洋葱圈仍然会上桌。
图示
排除前后的依赖树对比
上图展示了排除 log4j-to-slf4j 的效果。spring-boot-starter-web 通过 spring-boot-starter-logging 传递了三个日志桥接库。如果你的项目已经直接声明了 log4j-core,不需要 log4j-to-slf4j 的桥接,就可以通过 exclusions 将其移除,减少依赖树的冗余。
完整示例
场景
飞翔科技的 employee-system 引入了 spring-context 和 mybatis。后端小崔发现 mybatis 传递了 javassist,但项目中已经通过 hibernate-core 引入了更新版本的 javassist,两个版本冲突。同时,spring-context 传递了 commons-logging,而公司统一使用 slf4j + logback,不需要 commons-logging。小崔需要排除这两个冗余依赖。
操作前:未排除的依赖树
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.21</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.9</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.9.Final</version>
</dependency>
</dependencies>
执行依赖树:
mvn dependency:tree
输出:
com.feixiang:employee-system:jar:1.0.0
+- org.springframework:spring-context:jar:5.3.21:compile
| +- ...
| \- commons-logging:commons-logging:jar:1.2:compile
+- org.mybatis:mybatis:jar:3.5.9:compile
| +- org.slf4j:slf4j-api:jar:1.7.32:compile
| \- org.javassist:javassist:jar:3.27.0-GA:compile
\- org.hibernate:hibernate-core:jar:5.6.9.Final:compile
\- org.javassist:javassist:jar:3.28.0-GA:compile
问题:
javassist出现两个版本:3.27.0-GA(来自 mybatis)和3.28.0-GA(来自 hibernate-core)。根据最短路径优先,两者路径长度都是 2,先声明的 mybatis 胜出——但3.27.0-GA比3.28.0-GA旧,可能缺少 hibernate 需要的 APIcommons-logging被spring-context传递进来,和公司统一的slf4j日志体系冲突,运行时可能出现"日志双发"或配置失效
操作后:精确排除
白歌指导小崔修改 pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.feixiang</groupId>
<artifactId>employee-system</artifactId>
<version>1.0.0</version>
<dependencies>
<!-- 排除 commons-logging,统一使用 slf4j -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.21</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 排除 javassist,由 hibernate-core 提供统一版本 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.9</version>
<exclusions>
<exclusion>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- hibernate-core 提供 javassist 3.28.0-GA -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.9.Final</version>
</dependency>
<!-- 统一日志:slf4j + logback -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.32</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
</dependencies>
</project>
再次执行依赖树:
mvn dependency:tree
输出:
com.feixiang:employee-system:jar:1.0.0
+- org.springframework:spring-context:jar:5.3.21:compile
| +- ...
| \- (commons-logging:commons-logging:jar:1.2:compile - omitted for conflict)
# 注意:commons-logging 已消失
+- org.mybatis:mybatis:jar:3.5.9:compile
| +- org.slf4j:slf4j-api:jar:1.7.32:compile
| \- (org.javassist:javassist:jar:3.27.0-GA:compile - omitted for conflict)
# 注意:mybatis 的 javassist 已消失
\- org.hibernate:hibernate-core:jar:5.6.9.Final:compile
\- org.javassist:javassist:jar:3.28.0-GA:compile
# 注意:只有 hibernate 的 javassist 3.28.0-GA
+- org.slf4j:slf4j-api:jar:1.7.32:compile
\- ch.qos.logback:logback-classic:jar:1.2.11:compile
变化分析:
commons-logging被彻底排除,不再出现在依赖树中。Spring 的日志通过slf4j桥接,统一由logback处理mybatis传递的javassist 3.27.0-GA被排除,依赖树中只有hibernate-core带来的javassist 3.28.0-GA- 打包产物中不再包含
commons-logging.jar和javassist 3.27.0-GA.jar,体积减小,运行时无版本冲突 - 李眉部署后,日志系统工作正常,没有再出现"日志配置不生效"的问题
易错点与常见问题
误区一:exclusion 需要写 version
错误认知:"我要排除 javassist 3.27.0-GA,所以 exclusion 里要写 <version>3.27.0-GA</version>。"
纠正:exclusion 不需要写 version。Maven 通过 groupId + artifactId 匹配,排除该构件的所有版本。这是设计上的简化——你不需要知道传递依赖的具体版本号,只需知道它的坐标。
误区二:排除后不需要替代方案
错误认知:"我把 spring-context 传递的 commons-logging 排除了,项目编译应该更干净。"
纠正:Spring 框架的代码里直接使用了 commons-logging 的 API。如果你排除了它而不提供替代实现,编译或运行时会报 ClassNotFoundException: org.apache.commons.logging.Log。正确的做法是排除 commons-logging 的同时,引入 jcl-over-slf4j(将 commons-logging 的 API 桥接到 slf4j),或者使用 Spring 的 spring-jcl(已经内嵌在 spring-core 中,替代了 commons-logging)。
误区三:exclusions 可以跨层级生效
错误认知:"我在父 POM 里排除了 commons-logging,所有子模块都不会有它了。"
纠正:exclusions 写在具体的 <dependency> 中,只对那份声明生效。父 POM 的 dependencyManagement 可以统一声明 exclusions,但子模块仍然需要在 dependencies 里引用父 POM 管理的依赖,排除才会生效。如果子模块自行声明了另一个也传递 commons-logging 的依赖,排除不会自动蔓延过去。
小结
exclusions 是 Maven 依赖树的"修剪剪刀",用于显式阻止不需要的传递依赖进入项目。它不需要指定版本,通过 groupId + artifactId 即可排除所有版本。典型场景包括消除冗余、解决冲突、替换实现和减少打包体积。排除后需要确保有替代方案,避免运行时类缺失。
本章与全局的关系:本章讲解了"如何切断不需要的传递依赖"。下一章"optional"将讲解"如何标记依赖为'可选',使其不向下传递"——这是另一种控制依赖树传播范围的工具。