PAYATHON 2026

Maven dependency:tree -Dscope=compile 为空,为何仍能编译?

排障赵师傅

结论

dependency:tree -Dscope=compile 输出为空,并不代表该 JAR 不在 compile classpath 中。

题述场景中,依赖树保留的 CDI API 节点位于 test 分支下。scope 过滤会截断这个分支,其他位置的重复节点又已被省略,所以过滤后看不到它。实际构建时,Maven 仍可能根据其他 compile 路径,将该 artifact 的最终 scope 确定为 compile。

要确认依赖是否参与编译,使用 dependency:build-classpath -DincludeScope=compile;要追溯引入路径,使用不带 scope 过滤的 dependency:tree -Dverbose。排查时需要结合这两种输出。

Maven 如何处理混合 scope 的传递依赖

路径上的 scope 推导、版本选择和最终 scope 的确定,需要分开看。

单条依赖路径上的常见规则如下:

当前项目对 A 的依赖 scopeA 对 B 的依赖 scopeB 通过这条路径传递后的 scope
compilecompilecompile
compileruntimeruntime
testcompiletest
providedcompileprovided

按 Maven 的传递依赖 scope 规则,如果只有下面这条路径,CDI API 就不会成为生产源码的编译依赖:

project
\- quarkus-junit5:test
   \- ...
      \- jakarta.enterprise.cdi-api

同一个 artifact 通过多条路径引入时,Maven 还会处理冲突:

  • 版本选择通常遵循 nearest-wins;距离相同时,先声明的依赖优先。
  • 最终 scope 另行确定。Maven 3 使用的 JavaScopeSelector 会综合冲突节点的 scope。在题述传递依赖场景中,同时存在 compile 和 test 路径时,可以选出 compile。如果直接依赖也参与冲突,其 scope 有优先规则,不能简单理解为“总取最宽 scope”。参见 JavaScopeSelector。

树中保留哪个节点,与最终采用什么 scope,是相关但不同的结果。保留节点即使显示在测试依赖下面,最终 scope 也可能受到其他编译依赖路径的影响。

omitted for duplicate 表示这个位置不再重复保留相同依赖,不代表这条路径没有参与 scope 计算。

为什么 scope 过滤会让它消失

dependency:tree 按树形结构输出依赖,scope 过滤可能从父节点处截断整个子树。

以 Dependency Plugin 3.8.1 为例,TreeMojo 将 scope 转换成 ScopeArtifactFilter,调用的依赖图构建代码只会继续遍历通过过滤的子节点。父节点未通过过滤,其后代也不会进入输出树。参见 TreeMojo 源码和依赖图构建实现。

题述现象可以按这个过程理解:

  1. 未过滤的树将 CDI API 的保留节点放在 quarkus-junit5:test 分支下。
  2. -Dscope=compile 过滤掉 quarkus-junit5:test,其下保留的 CDI API 节点也随之消失。
  3. 其他 compile 分支上的 CDI API 已作为重复节点省略,普通输出不会补回这些节点。

混合 scope、重复节点省略和父分支过滤共同作用时,过滤后的依赖树可能漏掉实际属于编译依赖集合的 artifact,因此不能只靠它判断 compile classpath。

仅知道 Maven 版本是 3.9.15,还不足以确定全部实现细节。dependency:tree 来自独立的 Maven Dependency Plugin,排查时还应记录执行日志中的插件版本,不能直接认定这是 Maven 3.9.15 特有的问题。

如何确认它确实参与生产源码编译

1. 输出编译依赖的 classpath

在实际编译源码的模块中执行:

mvn dependency:build-classpath -DincludeScope=compile

检查输出中是否包含:

jakarta.enterprise.cdi-api-4.0.1.jar

这里使用的参数是 includeScope。它按 classpath 用途过滤已解析的依赖,其中 compile 包含 compile、provided 和 system,与生产源码编译所需的依赖范围一致。参见 build-classpath 参数说明。

如果只想检查目标 artifact,可以使用:

mvn dependency:list \
  -DincludeScope=compile \
  -DincludeGroupIds=jakarta.enterprise \
  -DincludeArtifactIds=jakarta.enterprise.cdi-api \
  -DoutputAbsoluteArtifactFilename=true

这个命令可以同时显示解析后的版本、scope 和文件路径。注意,list 使用 includeGroupIds、includeArtifactIds 过滤依赖,不能直接沿用 tree 的 includes。参见 dependency:list 文档。

2. 查看所有引入路径

执行不带 scope 过滤的 verbose tree:

mvn dependency:tree \
  -Dverbose \
  -Dincludes=jakarta.enterprise:jakarta.enterprise.cdi-api

沿各条路径向上查找项目直接声明的入口依赖,例如 quarkus-arc。保留节点和标有 omitted for duplicate 的节点都要看。

同一个依赖可能有多条来源路径:某条路径决定保留节点或版本,其他路径影响最终 scope。扁平的 classpath 只保留最终 artifact,不记录唯一的来源路径。Maven 官方文档也指出,verbose tree 会保留冲突信息,不能直接将其视为最终解析出的文件集合。参见依赖解析常见误解。

3. 有疑问时核对实际编译过程

如果依赖报告与编译行为仍有出入,再执行:

mvn -X clean compile

在编译插件的调试输出中检查实际使用的 classpath;使用 Java 模块系统时,还要检查 module path。

clean 可以排除旧 .class 文件或增量编译跳过源码带来的干扰。build-classpath 输出的是依赖 JAR 路径,并非编译器的完整参数;自定义插件或生命周期配置还可能调整编译环境。

排查时的注意事项

所有命令都应使用相同的模块、profile、settings 和相关 -D 参数。多模块项目如果在聚合根目录执行命令,还要确认所查看的输出属于哪个模块。

移除 quarkus-arc 后编译失败,符合“编译路径使 CDI API 可用”的解释,但移除扩展也可能同时移除其他依赖。要获得更直接的证据,可以对比移除前后的编译 classpath,并检查具体缺失的类。

如果生产源码直接使用 CDI API,可以按项目约定显式声明该依赖,版本应与项目使用的 Quarkus BOM 保持一致。无需仅为让依赖树更直观而调整依赖顺序或修改 scope。先确认最终 classpath,再决定是否需要改动 POM。

备注:内容仅供参考。