3.3 版本仲裁中央指挥部:dependencyManagement 与 BOM 本节摘要:dependencyManagement 只裁决版本不引入依赖,配合 scope 为 import 的 BOM 导入,可以把全团队的版本决定权集中到一份父 POM。enforcer 插件的收敛规则把仲裁结果变成硬门禁。本节完成 risk-engine 团队从丛林裁决到中央管制的建制全过程。 从丛林法则到中央管制 3.2 节战例修复后的第三周,另一个模块又打了同样一场仗——同样的 Jackson,不同的胜者。作战室意识到:逐次冲突逐次修,等于每个模块都在独立面对整棵依赖树,裁决结果永远取决于各自的路径深浅。冲突治理的终局不是修得快,而是裁决权统一:全团队所有模块,同名构件只有一个版本能入场。
本节摘要:dependencyManagement 只裁决版本不引入依赖,配合 scope 为 import 的 BOM 导入,可以把全团队的版本决定权集中到一份父 POM。enforcer 插件的收敛规则把仲裁结果变成硬门禁。本节完成 risk-engine 团队从丛林裁决到中央管制的建制全过程。
3.2 节战例修复后的第三周,另一个模块又打了同样一场仗——同样的 Jackson,不同的胜者。作战室意识到:逐次冲突逐次修,等于每个模块都在独立面对整棵依赖树,裁决结果永远取决于各自的路径深浅。冲突治理的终局不是修得快,而是裁决权统一:全团队所有模块,同名构件只有一个版本能入场。
建制三步走:先讲清 dependencyManagement 的语义(多数人对它有根本误解),再引入 BOM 实现裁决权的复用与继承,最后用 enforcer 把制度变成机器执法。
先破除最流行的误解——管理段不会引入任何依赖。它只做一件事:当某个依赖(无论直接还是传递)需要裁决版本而管理段钉过它时,按钉住的版本执行。 子模块里引用时可以不写版本,等于声明"版本听中央的":
<!-- 父 POM:中央裁决表 --> <dependencyManagement> <dependencies> <!-- 钉住 Jackson 全家:无论谁从哪条路径带入 一律 2.13.4 --> <dependency> <groupId>com.fasterxml.jackson</groupId> <artifactId>jackson-bom</artifactId> <version>2.13.4</version> <type>pom</type> <scope>import</scope> </dependency> <!-- 单独钉一个构件 --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>31.1-jre</version> </dependency> </dependencies> </dependencyManagement>
<!-- 子模块:不写版本 版本来自中央 --> <dependencies> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <!-- 无 version 声明 --> </dependency> </dependencies>
三条语义细节决定建制质量。其一,管理段的裁决优先于路径深浅:3.2 节的丛林规则只在管理段没管到的构件上生效。其二,子 POM 可以覆盖中央版本,但每一次覆盖都应被视为对建制的叛变,要走评审。其三,管理段同样管 exclusions 与 scope 的默认值,统一 provided 化某些构件也在这里做。
上面第一段示例其实已经用了 BOM:jackson 官方把"全家桶版本矩阵"发布成一个 type 为 pom 的构件,scope 写 import 即整表导入。这解决了中央裁决表的两个工程问题——表太大维护不动(交给官方或平台团队维护)与多团队复用(引同一份 BOM 即对齐裁决)。
多份 BOM 共存时注意顺序规则:先声明的 import 优先。risk-engine 的最终建制:
<dependencyManagement> <dependencies> <!-- 顺序即优先级:公司平台 BOM 压前 官方 BOM 殿后 --> <dependency> <groupId>com.shop.platform</groupId> <artifactId>shop-platform-bom</artifactId> <version>3.2.0</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.7.14</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>

制度若只靠 code review 盯,迟早被一个着急上线的深夜击穿。enforcer 插件把仲裁要求变成构建期硬闸:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> <version>3.3.0</version> <executions> <execution> <id>enforce-convergence</id> <goals> <goal>enforce</goal> </goals> <configuration> <rules> <!-- 依赖收敛:同名构件出现多版本直接失败 不再静默裁决 --> <dependencyConvergence/> <!-- 更宽松的替代品:只禁止低版本压过高版本 --> <requireUpperBoundDeps/> </rules> <fail>true</fail> </configuration> </execution> </executions> </plugin>
启用第一天的真实输出:
[ERROR] Rule 0: org.apache.maven.plugins.enforcer.DependencyConvergence failed with message: Dependencies convergence error com.fasterxml.jackson.core:jackson-databind 2.13.4 与 2.12.7 存在多版本 路径一 risk-engine 到 rule-sdk 2.1.0 到 jackson-databind 2.12.7 路径二 risk-engine 到 spring 相关 starter 到 jackson-databind 2.13.4 建议在管理段钉住唯一版本 [INFO] BUILD FAILURE
两条规则的取舍要说清:dependencyConvergence 是最严的"一个都不许多",适合建制初期做全量清点;requireUpperBoundDeps 只拦"低版本压高版本"的倒挂,日常运行成本低,适合作为长期门禁。risk-engine 团队的路线是先用收敛规则清完存量,再切到防倒挂规则守增量。
💡 关键直觉:仲裁建制的收益随模块数平方级放大。单模块时管理段是"多写几行 XML",十模块时它是"改一处、全队生效"的中央杠杆,而 enforcer 是防止杠杆被绕过的执法队。三件套(管理段、BOM、门禁)一起上,才算完成了从作战技巧到工程制度的跃迁。
战争的核心战术已经成军。但补给线本身也会出事——下一节转向"依赖下载失败"的处置:断粮、错粮、毒粮如何在仓库链路上逐一排查。