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 的依赖 scope | A 对 B 的依赖 scope | B 通过这条路径传递后的 scope |
|---|---|---|
compile | compile | compile |
compile | runtime | runtime |
test | compile | test |
provided | compile | provided |
按 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 源码和依赖图构建实现。
题述现象可以按这个过程理解:
- 未过滤的树将 CDI API 的保留节点放在
quarkus-junit5:test分支下。 -Dscope=compile过滤掉quarkus-junit5:test,其下保留的 CDI API 节点也随之消失。- 其他
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。
备注:内容仅供参考。
