目录
一、什么是BOM?
二、为什么需要BOM?
2.1 没有BOM时的痛点
2.2 使用BOM后的效果
三、BOM的两种使用方式
3.1 方式一:dependencyManagement + import(推荐)
3.2 方式二:通过 parent 继承
3.3 两种方式对比
四、如何自定义BOM?
4.1 创建BOM项目
4.2 发布BOM到私服
4.3 在业务项目中使用
五、多个BOM共存与版本优先级
5.1 版本优先级规则
5.2 版本覆盖示例
六、常见的开源BOM盘点
七、最佳实践总结
✅ 推荐做法
❌ 避免的做法
八、排查依赖版本问题的实用命令
九、总结
一、什么是BOM?
BOM,全称Bill of Materials(物料清单),这个词最早其实来源于制造业,指的是生产一个产品所需的所有原材料清单。Maven借用了这个概念,用来表示一种特殊的POM文件,它的核心作用就是统一管理一组相关依赖的版本号。
打个生活中的比方:你去餐厅吃饭,可以选择单点,也可以选择套餐。单点的话,每道菜你都得自己选、自己搭配,万一搭配不好还可能"串味"。而套餐呢,厨师已经帮你把菜品和分量都搭配好了,你只管下单就行。BOM就相当于这个"套餐菜单"——它帮你把一组依赖的版本都预先定义好,你在项目里引用的时候,只需要写依赖的坐标,版本号都不用操心。
那它到底能帮我们解决什么问题呢?简单来说有三点:
- 统一版本管理:所有依赖的版本号集中在一个地方维护,一改全改
- 简化依赖声明:引用依赖时不用再写
<version>标签,配置更清爽 - 保持多模块一致性:在大型多模块项目中,确保所有子模块用的都是同一套版本
二、为什么需要BOM?
可能有同学会说:"我直接在每个依赖上写版本号不也挺好的吗?为什么非要搞个BOM呢?"
别急,我们来看一个真实场景你就明白了。
2.1 没有BOM时的痛点
假设你正在开发一个Spring Boot项目,用到了Web、JPA、Security、Test这几个Starter。如果不用BOM,你的pom.xml大概长这样:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.2.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>3.2.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>3.2.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>3.2.5</version> </dependency> </dependencies>乍一看好像也没什么问题,但仔细想想:
问题 | 描述 |
版本散落各处 | 同样是 |
升级成本高 | 哪天要升级到 |
版本不一致风险 | 团队里多人协作,张三改了这个模块的版本,李四改了那个模块的版本,最后合到一起就炸了 |
传递依赖冲突 | 不同版本的Starter底层可能依赖了不同版本的Spring Framework,一旦混用就会出现各种诡异的 |
这些问题在小项目里可能还不明显,但一旦项目规模上来了,依赖管理就会变成一场噩梦。
2.2 使用BOM后的效果
现在我们用BOM来改造一下:
<!-- 在dependencyManagement中引入BOM --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>3.2.5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <!-- 使用依赖时无需指定版本,BOM已经帮你定好了 --> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>看到区别了吗?版本号只出现了一次,就在引入BOM的那个地方。下面所有的依赖声明都干干净净,只有groupId和artifactId。将来要升级版本?改一个地方就够了,所有依赖自动跟着变。
这就是BOM的魅力所在——把"版本管理"这件事从分散变成集中,从手动变成自动。
三、BOM的两种使用方式
在Maven中,使用BOM主要有两种方式。它们各有特点,适用于不同的场景。
3.1 方式一:dependencyManagement+import(推荐)
这是最常用、也是最灵活的方式。通过在<dependencyManagement>中以<scope>import</scope>的形式将BOM导入到当前项目中。
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2023.0.1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>这里有两个特殊的标签需要注意:
<type>pom</type>—— 告诉Maven这不是一个普通的jar依赖,而是一个POM类型的文件<scope>import</scope>—— 这是关键!它的意思是"把这个POM里定义的dependencyManagement内容导入到我的项目中来"
你可以把它理解为"复制粘贴":Maven会把BOM中dependencyManagement里定义的所有依赖版本,原封不动地"粘贴"到你当前项目的dependencyManagement中。这样一来,你在<dependencies>中声明这些依赖时,就不需要再写版本号了。
这种方式最大的好处是:你可以同时导入多个BOM。比如你的项目既用了Spring Boot,又用了Spring Cloud,还有公司内部的组件库,三个BOM可以并存,互不干扰。
3.2 方式二:通过parent继承
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> </parent>这种方式相信大家都很熟悉了,几乎每个Spring Boot项目都是这么开头的。它的本质是通过Maven的父POM继承机制来获取版本管理能力。
和import方式不同的是,parent继承不仅会继承版本管理,还会把父POM中定义的插件配置、编译参数、资源过滤规则等一并继承过来。这就好比import方式只是借了一份菜单,而parent方式是直接拜师学艺,师傅的全套手艺都传给你了。
但它有一个致命的限制:Maven的单继承机制决定了一个项目只能有一个parent。如果你的公司已经有了自己的父POM,那就没办法再继承spring-boot-starter-parent了。这时候就只能用方式一的import了。
3.3 两种方式对比
特性 | import方式 | parent继承方式 |
数量限制 | ✅ 可导入多个BOM | ❌ 只能有一个parent |
继承内容 | 仅版本管理 | 版本管理 + 插件 + 属性等 |
灵活性 | 高,可自由组合 | 较低,受单继承限制 |
适用场景 | 多框架混合使用、企业级项目 | 单一框架的标准项目 |
版本覆盖 | 需在import之前声明 | 可通过properties覆盖 |
我的建议是:在大多数企业项目中,优先使用import方式。它更灵活,不占用宝贵的parent位置,而且可以同时引入多个BOM。只有在纯粹的Spring Boot单体项目中,使用parent继承才更方便一些。
四、如何自定义BOM?
了解了BOM的用法之后,你可能会想:那些开源框架有现成的BOM可以用,但我们公司内部的组件怎么办?答案是——自己造一个!
在企业开发中,创建自己的BOM来统一管理内部组件版本,是一种非常常见也非常推荐的做法。下面我们一步步来实现。
4.1 创建BOM项目
首先,创建一个全新的Maven项目,这个项目不需要写任何Java代码,它的唯一职责就是管理版本号。pom.xml内容如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.mycompany</groupId> <artifactId>mycompany-bom</artifactId> <version>1.0.0</version> <!-- 注意:packaging必须是pom,这是BOM的标志 --> <packaging>pom</packaging> <name>MyCompany BOM</name> <description>统一管理公司内部组件版本</description> <!-- 通过properties集中定义版本号,方便维护 --> <properties> <mycompany.common.version>2.1.0</mycompany.common.version> <mycompany.security.version>1.5.0</mycompany.security.version> <mycompany.logging.version>1.3.0</mycompany.logging.version> <hutool.version>5.8.25</hutool.version> <guava.version>33.0.0-jre</guava.version> </properties> <dependencyManagement> <dependencies> <!-- ========== 公司内部组件 ========== --> <dependency> <groupId>com.mycompany</groupId> <artifactId>mycompany-common-core</artifactId> <version>${mycompany.common.version}</version> </dependency> <dependency> <groupId>com.mycompany</groupId> <artifactId>mycompany-common-redis</artifactId> <version>${mycompany.common.version}</version> </dependency> <dependency> <groupId>com.mycompany</groupId> <artifactId>mycompany-security-starter</artifactId> <version>${mycompany.security.version}</version> </dependency> <dependency> <groupId>com.mycompany</groupId> <artifactId>mycompany-logging-starter</artifactId> <version>${mycompany.logging.version}</version> </dependency> <!-- ========== 常用第三方组件 ========== --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>${guava.version}</version> </dependency> </dependencies> </dependencyManagement> </project>这里有几个要点值得说明:
第一,<packaging>pom</packaging>是必须的。它告诉Maven这个项目不会产出jar包,它就是一个纯粹的POM项目,专门用来做依赖管理。
第二,版本号建议用<properties>来管理。你可能注意到了,我没有直接在<version>标签里写死版本号,而是用属性变量来引用。这样做的好处是:所有版本号都集中在<properties>里,一目了然,改起来也方便。而且,使用方还可以通过覆盖属性的方式来自定义某个特定组件的版本。
第三,建议把内部组件和第三方组件分开管理。用注释分隔开,结构清晰,后续维护的时候一眼就能找到要改的地方。
4.2 发布BOM到私服
BOM写好之后,需要发布到公司的Maven私服(比如Nexus或Artifactory),这样其他项目才能引用到它:
mvn clean deploy这一步和发布普通的jar包没什么区别,Maven会把这个POM文件上传到私服的对应仓库中。
4.3 在业务项目中使用
发布完成后,业务项目就可以通过import的方式引入这个BOM了:
<dependencyManagement> <dependencies> <!-- 引入公司BOM --> <dependency> <groupId>com.mycompany</groupId> <artifactId>mycompany-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- 直接使用,无需版本号,是不是很清爽? --> <dependency> <groupId>com.mycompany</groupId> <artifactId>mycompany-common-core</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> </dependencies>从此以后,公司所有项目只要引入这一个BOM,内部组件和常用第三方库的版本就自动统一了。新人入职不用再纠结"这个库该用哪个版本",老项目升级也只需要改BOM的版本号就行。
五、多个BOM共存与版本优先级
在实际的企业项目中,只用一个BOM几乎是不可能的。你的项目可能同时需要Spring Boot的BOM、Spring Cloud的BOM、还有公司内部的BOM。这时候就涉及到一个关键问题:如果多个BOM中定义了同一个依赖的不同版本,Maven到底听谁的?
先来看一个典型的多BOM配置:
<dependencyManagement> <dependencies> <!-- BOM 1: Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>3.2.5</version> <type>pom</type> <scope>import</scope> </dependency> <!-- BOM 2: Spring Cloud --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2023.0.1</version> <type>pom</type> <scope>import</scope> </dependency> <!-- BOM 3: 公司内部BOM --> <dependency> <groupId>com.mycompany</groupId> <artifactId>mycompany-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>5.1 版本优先级规则
Maven对于版本冲突的解决遵循一个很明确的优先级规则,从高到低依次是:
1️⃣ 当前项目 <dependencyManagement> 中直接声明的版本(非import的) 2️⃣ 先声明的BOM中的版本(声明顺序靠前的优先) 3️⃣ 后声明的BOM中的版本简单来说就是:"直接声明 > 先导入的BOM > 后导入的BOM"。
举个例子:假设Spring Boot的BOM里定义了jackson-databind的版本是2.15.4,而你的公司BOM里定义的是2.16.1。由于Spring Boot的BOM写在前面,Maven最终会使用2.15.4。
这个规则其实很好理解——Maven认为你写在前面的BOM优先级更高,因为那通常是你更"信任"的版本来源。
5.2 版本覆盖示例
那如果我就是想用2.16.1版本的jackson-databind呢?有两种办法:
办法一:调整BOM的声明顺序,把包含你想要版本的BOM放到前面。但这种方式可能会影响其他依赖的版本解析,不太推荐。
办法二(推荐):在import之前显式声明该依赖的版本。
<dependencyManagement> <dependencies> <!-- ✅ 先声明要覆盖的版本,这个优先级最高 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.16.1</version> </dependency> <!-- 再导入BOM,BOM中的jackson-databind版本会被上面的覆盖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>3.2.5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>⚠️这里有一个很多人踩过的坑:覆盖版本的声明必须写在BOM的import语句之前,写在后面是无效的。因为Maven在解析dependencyManagement时,对于同一个依赖,只认第一次出现的版本定义。
六、常见的开源BOM盘点
了解了BOM的原理和用法之后,我们来看看Java生态中有哪些常用的BOM。这些BOM都是由各大开源社区维护的,经过了大量项目的验证,可以放心使用:
BOM | artifactId | 说明 |
Spring Boot |
| Spring Boot全家桶版本管理,覆盖了几百个常用依赖 |
Spring Cloud |
| 微服务组件版本管理,和Spring Boot版本有对应关系 |
Spring Cloud Alibaba |
| 阿里巴巴微服务组件(Nacos、Sentinel等) |
JUnit 5 |
| JUnit 5测试框架全家桶 |
Jackson |
| JSON处理库,确保core/databind/annotations版本一致 |
Netty |
| 网络通信框架,子模块众多,BOM管理非常必要 |
Log4j2 |
| 日志框架版本统一 |
AWS SDK |
| AWS开发工具包,服务模块极多,必须用BOM管理 |
其中,Spring Boot的BOM可以说是最"大而全"的,它不仅管理了Spring自家的组件版本,还帮你管理了大量常用第三方库的版本,比如Jackson、Logback、HikariCP、Tomcat等等。这也是为什么用了Spring Boot之后,很多依赖都不需要写版本号的原因。
七、最佳实践总结
经过这么多年的项目实践,我总结了一些关于BOM使用的经验,分享给大家。
✅ 推荐做法
1. 企业项目必建BOM。不管公司大小,只要有超过两个项目共享组件,就应该建立统一的BOM。这是依赖治理的第一步,也是最重要的一步。
2. 优先使用import方式而非parent继承。import方式更灵活,不占用parent位置,支持多BOM共存。除非你的项目确实需要继承父POM的插件配置,否则import是更好的选择。
3. 版本号一定要用properties管理。把所有版本号集中定义在<properties>中,不要直接硬编码在<version>标签里。这样不仅便于查找和修改,还方便下游项目通过覆盖属性来自定义版本。
4. BOM应该是一个独立的项目。不要把BOM的POM和业务代码混在一起。它应该有自己独立的Git仓库、独立的版本号、独立的发布流程。
5. BOM版本号要遵循语义化版本规范(SemVer)。主版本号表示不兼容的变更,次版本号表示新增功能,修订号表示Bug修复。这样使用方看到版本号就能大致判断升级的风险。
6. 维护一份版本兼容矩阵文档。记录BOM各版本与Spring Boot、JDK等基础设施的兼容关系,方便团队成员查阅。
❌ 避免的做法
1. 不要在BOM中使用<dependencies>。BOM里只应该有<dependencyManagement>。如果你在BOM里写了<dependencies>,那所有引入这个BOM的项目都会被强制引入这些依赖,这不是BOM该干的事。
2. 不要在BOM中包含业务逻辑代码。BOM就是一个纯粹的版本管理工具,不要给它加戏。
3. 不要频繁发布SNAPSHOT版本到生产环境。BOM的稳定性直接影响所有下游项目,发布前一定要充分测试。
4. 不要在子模块中随意覆盖BOM定义的版本。除非你有充分的理由(比如某个组件确实需要特定版本来修复一个严重Bug),否则应该尊重BOM的版本定义。随意覆盖版本会破坏BOM统一管理的初衷。
八、排查依赖版本问题的实用命令
即使用了BOM,在实际开发中还是难免会遇到依赖版本相关的问题。这时候以下几个Maven命令就是你的好帮手:
# 查看完整的依赖树——这是排查依赖问题的第一步 # 它会展示项目所有依赖的层级关系,包括传递依赖 mvn dependency:tree # 查看有效POM——展开所有继承和import后的最终POM # 当你不确定某个版本到底从哪来的时候,看这个最直观 mvn help:effective-pom # 分析依赖问题——找出未声明但使用了的依赖,以及声明了但没使用的依赖 mvn dependency:analyze # 查看某个具体依赖的版本来源——精确定位某个依赖的版本是怎么解析出来的 # 比如你想知道jackson-databind到底用的哪个版本、从哪个BOM来的 mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind其中mvn dependency:tree是我用得最多的命令,几乎每次遇到NoSuchMethodError、ClassNotFoundException这类运行时错误,第一反应就是跑一下这个命令看看依赖树。很多时候问题就出在某个传递依赖的版本不对。
九、总结
最后,我们用一张表来回顾一下本文的核心内容:
维度 | 说明 |
是什么 | BOM是一种特殊的POM,专门用于集中定义一组依赖的版本号 |
为什么用 | 统一版本、避免冲突、简化配置、降低维护成本 |
怎么用 | 推荐 |
怎么建 | 创建 |
优先级 | 直接声明 > 先import的BOM > 后import的BOM |
一句话总结:BOM是Maven依赖管理的"中央版本控制台",用好它能让你的项目依赖管理从混乱走向有序。无论是使用开源框架提供的BOM,还是在企业内部自建BOM,它都是Java项目工程化实践中不可或缺的一环。
如果你的团队还没有用上BOM,不妨从今天开始,先把项目中最常用的那些依赖整理成一个BOM。相信我,当你体验过"改一个版本号,全公司项目自动统一"的快感之后,你就再也回不去了。😄
📌如果这篇文章对你有帮助,欢迎点赞收藏!有问题欢迎评论区交流讨论~