Maven中BOM(Bill of Materials)的使用详解


目录

一、什么是BOM?

二、为什么需要BOM?

[2.1 没有BOM时的痛点](#2.1 没有BOM时的痛点)

[2.2 使用BOM后的效果](#2.2 使用BOM后的效果)

三、BOM的两种使用方式

[3.1 方式一:dependencyManagement + import(推荐)](#3.1 方式一:dependencyManagement + import(推荐))

[3.2 方式二:通过 parent 继承](#3.2 方式二:通过 parent 继承)

[3.3 两种方式对比](#3.3 两种方式对比)

四、如何自定义BOM?

[4.1 创建BOM项目](#4.1 创建BOM项目)

[4.2 发布BOM到私服](#4.2 发布BOM到私服)

[4.3 在业务项目中使用](#4.3 在业务项目中使用)

五、多个BOM共存与版本优先级

[5.1 版本优先级规则](#5.1 版本优先级规则)

[5.2 版本覆盖示例](#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 大概长这样:

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>

乍一看好像也没什么问题,但仔细想想:

|---------|----------------------------------------------------------------------------|
| 问题 | 描述 |
| 版本散落各处 | 同样是 3.2.5,你得在四个地方重复写,这还只是四个依赖,真实项目可能有几十个 |
| 升级成本高 | 哪天要升级到 3.3.0,你得一个一个找、一个一个改,漏掉一个就可能出问题 |
| 版本不一致风险 | 团队里多人协作,张三改了这个模块的版本,李四改了那个模块的版本,最后合到一起就炸了 |
| 传递依赖冲突 | 不同版本的Starter底层可能依赖了不同版本的Spring Framework,一旦混用就会出现各种诡异的 NoSuchMethodError |

这些问题在小项目里可能还不明显,但一旦项目规模上来了,依赖管理就会变成一场噩梦。

2.2 使用BOM后的效果

现在我们用BOM来改造一下:

XML 复制代码
<!-- 在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的那个地方。下面所有的依赖声明都干干净净,只有 groupIdartifactId。将来要升级版本?改一个地方就够了,所有依赖自动跟着变。

这就是BOM的魅力所在------把"版本管理"这件事从分散变成集中,从手动变成自动。


三、BOM的两种使用方式

在Maven中,使用BOM主要有两种方式。它们各有特点,适用于不同的场景。

3.1 方式一:dependencyManagement + import(推荐)

这是最常用、也是最灵活的方式。通过在 <dependencyManagement> 中以 <scope>import</scope> 的形式将BOM导入到当前项目中。

XML 复制代码
<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 继承

XML 复制代码
<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 复制代码
<?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了:

XML 复制代码
<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配置:

XML 复制代码
<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之前显式声明该依赖的版本。

XML 复制代码
<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-dependencies | Spring Boot全家桶版本管理,覆盖了几百个常用依赖 |
| Spring Cloud | spring-cloud-dependencies | 微服务组件版本管理,和Spring Boot版本有对应关系 |
| Spring Cloud Alibaba | spring-cloud-alibaba-dependencies | 阿里巴巴微服务组件(Nacos、Sentinel等) |
| JUnit 5 | junit-bom | JUnit 5测试框架全家桶 |
| Jackson | jackson-bom | JSON处理库,确保core/databind/annotations版本一致 |
| Netty | netty-bom | 网络通信框架,子模块众多,BOM管理非常必要 |
| Log4j2 | log4j-bom | 日志框架版本统一 |
| AWS SDK | bom (software.amazon.awssdk) | 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命令就是你的好帮手:

bash 复制代码
# 查看完整的依赖树------这是排查依赖问题的第一步
# 它会展示项目所有依赖的层级关系,包括传递依赖
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 是我用得最多的命令,几乎每次遇到 NoSuchMethodErrorClassNotFoundException 这类运行时错误,第一反应就是跑一下这个命令看看依赖树。很多时候问题就出在某个传递依赖的版本不对。


九、总结

最后,我们用一张表来回顾一下本文的核心内容:

|----------|---------------------------------------------------------|
| 维度 | 说明 |
| 是什么 | BOM是一种特殊的POM,专门用于集中定义一组依赖的版本号 |
| 为什么用 | 统一版本、避免冲突、简化配置、降低维护成本 |
| 怎么用 | 推荐 dependencyManagement + import,也可以用 parent 继承 |
| 怎么建 | 创建 packaging=pom 的独立项目,在 dependencyManagement 中定义版本 |
| 优先级 | 直接声明 > 先import的BOM > 后import的BOM |

一句话总结: BOM是Maven依赖管理的"中央版本控制台",用好它能让你的项目依赖管理从混乱走向有序。无论是使用开源框架提供的BOM,还是在企业内部自建BOM,它都是Java项目工程化实践中不可或缺的一环。

如果你的团队还没有用上BOM,不妨从今天开始,先把项目中最常用的那些依赖整理成一个BOM。相信我,当你体验过"改一个版本号,全公司项目自动统一"的快感之后,你就再也回不去了。😄


📌 如果这篇文章对你有帮助,欢迎点赞收藏!有问题欢迎评论区交流讨论~

相关推荐
XS0301062 小时前
Java 基础(七)多态
java·开发语言
不知名的老吴2 小时前
一文读懂:单例模式的经典案例分析
java·开发语言·单例模式
yaoxin5211232 小时前
388. Java IO API - 处理事件
java·服务器·数据库
JAVA学习通2 小时前
AI 工作流编排系统的任务拆分、重试与观测:2026年工程实践深度解析
java·人工智能·spring
凤山老林2 小时前
27-Java final 关键字
java·开发语言
少许极端2 小时前
算法奇妙屋(四十九)-贡献法
java·算法·leetcode·贡献法
卷毛的技术笔记2 小时前
从零到一:深入浅出分布式锁原理与Spring Boot实战(Redis + ZooKeeper)
java·spring boot·redis·分布式·后端·面试·java-zookeeper
天空属于哈夫克32 小时前
行为审计与全链路追踪:私域自动化执行的安全性设计
java·运维·微服务
skilllite作者2 小时前
SkillLite 技术演进笔记:Workspace、沙箱与进化
java·开发语言·前端·笔记·安全·agentskills