传递依赖为什么会被排除,被排除后会发生什么
Maven 的依赖机制是传递性的:当你引入 A 依赖时,A 自己依赖的 B、B 又依赖的 C,都会自动进入项目的 classpath。这种机制省去了手动管理大量 jar 的麻烦,但也带来了版本冲突、包冲突等问题。于是 Maven 提供了 exclusions 标签,允许开发者把某个不想要的传递依赖从依赖树中剔除。比如你引入了某个压缩处理库,它传递依赖了 Tar.jar,而你并不需要 Tar 相关功能,就可以声明排除。

问题在于,排除操作往往不是一个人做的,也不是一个时间点做的。几个月后另一个同事接手模块,需要用到 Tar 包里的 org.apache.commons.compress.archivers.tar 相关类,编译时却报出 ClassNotFoundException,翻遍代码也找不到哪里漏引了包。实际上根因就是某个依赖节点上挂着一个早已被遗忘的 exclusions 声明。更隐蔽的情况是运行期才暴露的错误,比如 NoSuchMethodError,这是由于排除导致实际加载的 jar 版本与预期不符。
被排除的传递依赖不会出现在 target 目录、不会打进 war 包或 fat jar,也不会出现在 IDE 的 External Libraries 中,这使得问题定位变得困难。理解了这一点,我们就有了明确的排查方向:找到排除声明在哪,然后决定是删除它,还是用别的方式把依赖补回来。
第一步:用 mvn dependency:tree 精确定位排除位置
定位的第一利器是 Maven 自带的依赖树命令。在项目根目录执行:
mvn dependency:tree -Dincludes=commons-compress -Dverbose
这条命令会只输出与 commons-compress(Tar.jar 所在的包)相关的依赖路径,并显示 verbose 模式下的冲突与省略信息。输出中如果有类似 commons-compress:commons-compress:jar:1.26.0 (omitted for conflict) 的字样,说明该依赖被排除或因冲突被省略。接下来用另一个命令反查排除声明的来源:
mvn dependency:tree -Dverbose | grep -B 5 "commons-compress"
通过向前查看几行,就能找到是哪个依赖节点下面挂着排除项。找到后打开对应的 pom.xml,通常能看到这样的配置:
<dependency>
<groupId>com.example</groupId>
<artifactId>some-legacy-lib</artifactId>
<version>2.3.1</version>
<exclusions>
<exclusion>
<groupId>commons-compress</groupId>
<artifactId>commons-compress</artifactId>
</exclusion>
</exclusions>
</dependency>除了当前项目的 pom,还要警惕父 pom 和 BOM 文件。dependencyManagement 中虽然不能直接写 exclusions,但父工程可能通过间接方式影响版本选择。建议在项目全局搜索关键字 exclusion 和相关 artifactId,一次性把所有相关声明找出来,避免只删了一处还有遗漏。
如果团队使用 IDEA,可以安装 Maven Helper 插件,打开 pom 文件后切换到 Dependency Analyzer 视图,图形化地看到依赖树,右键任意节点即可查看谁把它排除、谁与它冲突,比命令行直观得多。
第二步:恢复依赖的三种方式及各自适用场景
定位到排除声明后,恢复方式取决于排除它的动机。
方式一:直接删除 exclusion 声明。如果确认当初排除只是因为冲突,而现在可以统一版本,这是最干净的做法。删除后执行 mvn clean compile 验证,再用 dependency:tree 确认 Tar.jar 已回到依赖树中。注意删除排除可能引发新的版本仲裁变化,最好同步检查 dependencyManagement 中是否锁定了期望版本,例如:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>commons-compress</groupId>
<artifactId>commons-compress</artifactId>
<version>1.26.0</version>
</dependency>
</dependencies>
</dependencyManagement>这样即使依赖树上有多条路径引入了不同版本,最终也会统一到 1.26.0,彻底解决当初之所以要排除的版本冲突问题。
方式二:不删排除,改为直接声明依赖。如果那个 exclusion 是为了阻断某个已知的坏版本(比如旧版 commons-compress 有 CVE 漏洞),而第三方库你没权限控制,可以在当前项目 pom 中把 Tar.jar 作为第一级依赖显式声明出来。Maven 的规则是:直接声明的依赖优先级高于传递依赖,即使上游仍然排除,你自己声明的这个 jar 依然会进入 classpath。这是恢复被排除依赖时最稳妥、副作用最小的手段,也是笔者最推荐的做法。
方式三:使用 classifier 或重打包。极少数情况下,上游通过 shaded jar 把 Tar 相关类打了进去,此时真正的问题不是缺 jar 而是类重复。恢复前先解压检查现有 jar 中是否已包含 org.apache.commons.compress 包,避免引入两份类导致更难排查的冲突。
第三步:验证恢复效果并建立依赖治理习惯
恢复依赖后,验证工作不能省。除了编译通过,建议运行完整的单元测试,特别是涉及序列化、反射的场景,这些地方在开发期可能不报错,上线后才暴露。可以在打包产物中确认 jar 是否存在:
unzip -l target/app.jar | grep tar # 或者对 fat jar mvn dependency:copy-dependencies -DoutputDirectory=deps ls deps | grep compress
另外要留意模块间的传递影响。多模块工程中,A 模块恢复的依赖未必对 B 模块生效,因为 Maven 的传递依赖在模块边界可能会被再次过滤。必要时在 B 模块也显式声明。
从长远看,避免这类问题的根本方法是依赖治理:第一,所有 exclusion 必须写注释说明原因和日期,例如 <!-- 2024-03 排除旧版compress,存在CVE-2024-25710,改由顶层声明1.26.0 -->;第二,使用 maven-enforcer-plugin 的 banDuplicatePomDependencyVersions 和 requireUpperBoundDeps 规则,在构建期拦截版本冲突;第三,定期执行 mvn dependency:analyze 检查声明了但未使用、使用了但未声明的依赖,保持依赖树健康。
总结一下,恢复被排除的传递依赖并不复杂:先用 dependency:tree 和 Maven Helper 定位排除位置,再根据排除动机选择删除声明、显式引入或统一版本管理三种策略之一,最后通过编译、测试和产物检查完成验证。配合注释规范和 enforcer 插件,就能让团队的依赖树始终保持清晰可控。