父POM已经管理了一个库的版本,子模块里却仍然没有这个依赖;把配置移到父POM的dependencies,子模块没写依赖也出现了它。这两种结果都可能是正确的,关键是先分清:声明要用什么,和管理用了以后采用什么配置,是两件事。
这次用Maven 3.9.12、JDK 21.0.2做了一个最小模型实验。只保留父POM的dependencyManagement时,子项目的有效POM没有直接依赖;子项目再声明同一个库,版本才补成1.4。整个过程不需要Spring项目,也不需要用一个大型依赖树掩盖配置关系。
2026-11-02修订:补充可复现的有效POM对照,纠正scope与打包的关系,并明确BOM导入和父POM继承的边界。
同一个坐标,先看它出现在哪里
dependencies里的声明参与构建项目的依赖关系。这个声明可以来自当前POM,也可以从父POM继承;此外还可能通过传递依赖引入其他库。
dependencyManagement提供的是依赖配置管理。它不会单凭一条管理记录就给当前项目增加依赖,但会影响匹配依赖的版本、scope等信息,也能控制依赖图中匹配的传递依赖版本。把它叫“版本清单”方便入门,不能理解成它只是一段不参与解析的文档。
创建两个目录,先保存parent-managed/pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>example.fixture</groupId> <artifactId>parent-managed</artifactId> <version>1.0</version> <packaging>pom</packaging> <dependencyManagement> <dependencies> <dependency> <groupId>example.fixture</groupId> <artifactId>demo-lib</artifactId> <version>1.4</version> </dependency> </dependencies> </dependencyManagement> </project>再保存managed-use/pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>example.fixture</groupId> <artifactId>parent-managed</artifactId> <version>1.0</version> <relativePath>../parent-managed/pom.xml</relativePath> </parent> <artifactId>managed-use</artifactId> <packaging>pom</packaging> <dependencies> <dependency> <groupId>example.fixture</groupId> <artifactId>demo-lib</artifactId> </dependency> </dependencies> </project>在两目录的上一级执行:
$effectiveFile = Join-Path $PWD 'effective-managed.xml' mvn -B -ntp -f managed-use/pom.xml org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-pom "-Doutput=$effectiveFile"effective-pom把继承、管理等处理后的项目模型写成XML。Help Plugin文档说明了输出参数;这里固定插件版本,避免命令简写的插件解析随环境变化。
本次生成的顶层dependencies中,example.fixture:demo-lib的版本是1.4。然后分别改变一处配置,得到以下结果:
| 配置 | 子项目有效POM中的直接依赖 |
|---|---|
| 父POM只管理版本,子项目没有声明依赖 | 没有demo-lib |
| 父POM管理1.4,子项目声明demo-lib并省略版本 | demo-lib:1.4 |
| 父POM在普通dependencies声明demo-lib:1.4,子项目不再声明 | 继承demo-lib:1.4 |
| 父POM管理1.4,子项目在直接依赖上明确写4.0 | demo-lib:4.0 |
这里的坐标是人工示例,实验观察的是项目模型,没有解析这个库的JAR,也没有编译业务代码。effective-pom成功不代表对应版本一定存在,更不代表它可以与项目其他库一起正常运行。
最后一行也说明,dependencyManagement不是无法绕过的锁。直接依赖显式写出的版本可以覆盖管理值。团队若要禁止越界版本,需要另外的规则和检查,不能只看父POM里写过一个版本。
图1:分别看有没有直接依赖、采用哪个版本。管理不会自动注入,直接声明也可以覆盖管理版本。
scope控制可见性,不能直接等同于“打进JAR”
先修正一个常见说法:普通Maven JAR项目执行打包,并不会自动把所有compile依赖的类塞进生成的JAR。依赖的classpath范围,与最终如何组装产物,需要分开看。
| scope | 主代码编译 | 测试编译/执行 | 应用运行classpath |
|---|---|---|---|
| compile,默认 | 包含 | 包含 | 包含 |
| provided | 包含 | 包含 | 不由这项依赖加入 |
| runtime | 不包含 | 包含 | 包含 |
| test | 不包含 | 包含 | 不包含 |
provided常用于明确由运行环境提供的API,前提是你的部署环境确实提供它。WAR、Spring Boot重打包、Shade等组装方式对依赖的处理还各有规则;不能仅凭scope就断言某个最终文件里一定有或一定没有某个JAR。system依赖本地路径,通常会增加可移植性问题;import则用于导入管理模型,不属于普通运行classpath。范围定义见Maven依赖机制说明。
BOM导入的是哪一部分
项目已经有父POM,但又希望使用另一套依赖版本管理,可以在dependencyManagement中导入BOM:type=pom,scope=import。它没有给项目增加第二个父POM。
为了把导入边界看清,下面的demo-bom/pom.xml故意多放了一个普通依赖、一个属性标记和一个插件管理标记。它们只用于观察哪些内容会进入消费者,不是建议把BOM设计成这样:
<project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>example.fixture</groupId> <artifactId>demo-bom</artifactId> <version>1.0</version> <packaging>pom</packaging> <properties> <demo.version>2.0</demo.version> <bom.marker>not-inherited</bom.marker> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>example.fixture</groupId> <artifactId>demo-lib</artifactId> <version>${demo.version}</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>example.fixture</groupId> <artifactId>bom-only-dep</artifactId> <version>9.9</version> </dependency> </dependencies> <build> <pluginManagement> <plugins> <plugin> <groupId>example.fixture</groupId> <artifactId>bom-only-plugin</artifactId> <version>9.9</version> </plugin> </plugins> </pluginManagement> </build> </project>消费者保存为import-use/pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>example.fixture</groupId> <artifactId>import-use</artifactId> <version>1.0</version> <packaging>pom</packaging> <dependencyManagement> <dependencies> <dependency> <groupId>example.fixture</groupId> <artifactId>demo-bom</artifactId> <version>1.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>example.fixture</groupId> <artifactId>demo-lib</artifactId> </dependency> </dependencies> </project>将这个自建BOM安装到示例专用仓库,再生成消费者的有效POM:
$demoRepository = Join-Path $PWD '.demo-repository' $effectiveBomFile = Join-Path $PWD 'effective-import.xml' mvn -B -ntp "-Dmaven.repo.local=$demoRepository" -f demo-bom/pom.xml org.apache.maven.plugins:maven-install-plugin:3.1.3:install mvn -B -ntp "-Dmaven.repo.local=$demoRepository" -f import-use/pom.xml org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-pom "-Doutput=$effectiveBomFile"第一次执行需要下载官方插件。这里安装的只是示例自己的POM,没有向远程仓库发布任何东西。查看effective-import.xml,本次结果是:
- 顶层直接依赖只有demo-lib:2.0,BOM里的
${demo.version}已用于求出这个版本。 bom-only-dep没有作为消费者的直接依赖出现。- 消费者没有继承
bom.marker属性,也没有导入bom-only-plugin的插件管理。 - 删除消费者的普通
dependencies以后,导入的管理记录仍在,顶层直接依赖为空。
导入不是整份POM继承。它引入目标的依赖管理内容,不会顺便拿到目标的构建配置;即便目标BOM使用属性组织版本,也不能把导入理解成那些属性都成为了消费者自己的属性。官方Importing Dependencies示例展示了这项机制。
本次另一个对照是在消费者自己的dependencyManagement里明确管理demo-lib:3.0,同时保留BOM导入;最终直接依赖采用3.0。需要覆盖BOM版本时,把覆盖写在消费者自己的管理项中,比依赖对方POM内部的属性名更容易审查。
图2:import导入BOM的依赖管理内容,不把普通依赖、属性标记或插件管理一起继承给消费者。
依赖还不对时,检查匹配和实际路径
没有父管理、没有BOM,又在直接依赖中省略版本,本次mvn validate就报出:
'dependencies.dependency.version' for example.fixture:demo-lib:jar is missing.管理项还需要匹配依赖身份。通常只写groupId和artifactId就够,是因为默认type为jar、classifier为空;完整匹配还包括type和classifier。本次只管理默认jar,把使用处改成type=war并省略版本,得到的仍是缺少版本错误,不会因为G、A相同就自动套用。
如果问题来自传递依赖,先确认是谁把它带进来。Maven通常按更近的路径选择版本,同深度再看先声明者;匹配的依赖管理还能影响传递版本。exclusions排除的是某条依赖路径下面的传递依赖,并不是从父POM继承的整个直接依赖列表中任意删除一项。另一路仍然引入同一库时,它也可能继续留在依赖图中。
因此,排查时先用有效POM确认继承与管理结果,再用dependency:tree观察真实解析路径;后者需要对应构件可解析,不能拿本文的人工坐标直接当作真实库。
BOM能集中表达发布者选择的一组版本,Maven本身不会替发布者证明这些版本已经兼容。升级后仍要跑应用的构建和关键测试。把依赖管理做好,解决的是版本选择有出处、覆盖可追踪;运行兼容性还要由实际验证回答。
接着可以看父POM、聚合POM与BOM的关系:同样写着packaging=pom,三者建立关系的方式并不相同。