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

    • 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章 常见问题与最佳实践

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

exclusions

本章承接"依赖冲突与调解",深入讲解 Maven 的依赖排除机制。理解 exclusions,是精确修剪依赖树、消除冗余传递依赖和解决版本冲突的必备技能。


核心机制

exclusions 用于显式阻止某个依赖的传递依赖进入当前项目的依赖树。被排除的依赖不会参与编译、测试、打包,也不会继续向上一层传递。

这句话的关键词是显式阻止。如果说依赖传递是 Maven 的"自动补全",那么 exclusions 就是开发者对自动补全结果的"人工审核和删除"。

为什么需要排除?

即使 Maven 的依赖调解能自动选择版本,以下场景仍然需要显式排除:

  1. 消除冗余依赖:A 传递了 B,但你的项目已经通过其他路径引入了 B,且不需要 A 带来的版本
  2. 解决版本冲突:A 传递了 C:1.0,B 传递了 C:2.0,调解结果不符合预期,需要切断其中一条路径
  3. 减少打包体积:A 传递了 10 个库,但你只用到 A 的核心功能,不需要它的全部生态
  4. 替换实现: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 需要的 API
  • commons-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"将讲解"如何标记依赖为'可选',使其不向下传递"——这是另一种控制依赖树传播范围的工具。

上一页
依赖冲突与调解
下一页
optional