最近有个同事跑过来问我:项目里用 Redis 当消息中间件,pom 里到底该写哪个版本的spring-boot-starter-data-redis。我说不用写,父工程管着。他又问,那换成 Kafka 呢,kafka-clients得自己挑版本吧。这个问题特别典型——Spring Boot 引用各种中间件时,版本确定这事,看着玄,实际有一套非常明确的规则和排查方法。把握好这套规则,你就不会在"版本号写不写"和"写多少"之间反复横跳了。
这篇文章我不打算只讲结论,而是把背后的依赖仲裁机制、信息源、选型套路和冲突排查过程完整梳理一遍,最后给出一套可以直接抄的工程规范。无论你是刚接触 Spring Boot 的新人,还是被依赖冲突折磨过的老手,应该都能在这里找到对应的答案。
1. 依赖仲裁的底层逻辑:版本究竟是谁定的
1.1 先理解"BOM"和"starter"的分工
很多人有一个误解:以为spring-boot-starter-web这种依赖里直接写死了所有子依赖的版本。实际上不是。
starter 只是一个聚合依赖,它把某个场景需要的 jar 通过<dependencies>集中引用。而以spring-boot-starter-parent为父工程的 Spring Boot 项目,版本管理的真正核心是spring-boot-dependencies这个 BOM。BOM(Bill of Materials)本质上是一个只声明<dependencyManagement>、不实际引入依赖的 POM。它里面维护了一张超长的清单,把 Spring 生态以及与之配套的常用第三方库(比如 Netty、Jackson、Kafka、Lettuce、Undertow 等)的版本全部管理起来。
所以你的项目里写:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> </parent> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>这个依赖能不加版本号,是因为父 POM 通过spring-boot-dependencies替你把spring-boot-starter-data-redis的版本以及它内部引用的lettuce-core版本全部定好了。你只要确定一件事:Spring Boot 本身用哪个版本。其他跟着走就行。
如果项目不能使用父 POM(比如公司有统一的 parent),也可以用 import 方式引入 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>这样依然能享受到同样的版本管理能力,这就是为什么 Spring 官方一直强调"优先使用 BOM 管理的版本"。
1.2 版本覆盖规则:什么时候写着版本号反而会坏事
Maven 的仲裁规则里有一个关键点:dependencyManagement中声明的版本,只对子依赖"没有声明版本"的情况生效。一旦你在<dependency>里显式写了<version>,优先采用你写的版本。
这个机制会带来一个常见事故:你为了修某个 bug,给某个库显式指定了一个新版本,结果这个库和 Spring Boot BOM 里其他依赖不兼容。比如手动把jackson-databind从 2.15 改成 2.17,表面上没问题,但 Spring 的RestTemplate、ObjectMapper的自动配置在某些方法签名变化时可能直接启动失败。
所以我的建议是:凡是被spring-boot-dependencies覆盖的依赖,先不要手动写版本。确实需要覆盖时,优先使用父 POM 暴露的属性,而不是直接给依赖写<version>。比如 Spring Boot 2.7 中 Kafka 客户端版本可以通过<kafka.version>属性覆盖:
<properties> <kafka.version>3.4.1</kafka.version> </properties>因为 BOM 内部的版本号大量使用${xxx.version}占位符,你只要覆盖属性,BOM 里的引用会同步更新。直接写<version>也可行,但会破坏"版本统一管理"这个核心机制,而且后面排查依赖关系时会多一层干扰。
2. 确定版本的信息源:四个可信渠道与我的使用顺序
2.1 第一个渠道:Spring Boot 官方依赖版本表
Spring Boot 官方文档里专门有一张 "Dependency Versions" 表,对应每一个 Spring Boot 版本,列出了所有被 BOM 管理的依赖和对应版本号。这张表是我判断"某个中间件客户端在 Spring Boot 里默认用的是哪个版本"的第一信息源。
举例,Spring Boot 3.2.x 对应的kafka-clients是 3.6.x,lettuce-core是 6.3.x,activemq-client是 5.18.x。你不需要背,用到的时候去对应版本的官方文档附录查一下即可。查不到的,可以直接看 Maven 仓库中spring-boot-dependencies-3.2.5.pom的内容。
2.2 第二个渠道:中间件官方的兼容性矩阵
Spring Boot 的 BOM 决定了客户端版本,但客户端版本能不能连上你的中间件服务端,这是另一回事。比如 Kafka 的场景:kafka-clients从 2.x 到 3.x,broker 的兼容性策略一般是"客户端可以连接与自己版本相差多个小版本的 broker",但某些新协议特性在老 broker 上不可用。Kafka 官方文档里就有 KIP 和兼容性说明,Flink 官方也有每个 Flink 版本对应的连接器版本矩阵。
这类信息在中间件官网的 "Compatibility" 或 "Supported Versions" 页面,比在代码仓库里猜靠谱得多。
2.3 第三个渠道:Spring Initializr 与现有工程样板
说实话,Spring Initializr 是最好用的"样板间"。你在 start.spring.io 上按目标 Spring Boot 版本生成一个包含所需中间件 starter 的项目,它帮你选好的版本组合就是经过官方验证的组合。比如生成一个包含spring-boot-starter-data-redis的项目,再看它生成的 pom,就能知道这个组合下lettuce-core被 BOM 管到什么版本。
这个方式特别适合"不知道该不该升版本"的时候——先看 Initializr 给什么组合,再决定要不要覆盖。
2.4 第四个渠道:第三方库自身的 Release Notes
Spring Boot BOM 只管"官方认为能形成稳定组合"的库。实际项目中很多中间件客户端并不在 BOM 里,比如 fastjson、jjwt、Flink 的 connector、PostgreSQL 驱动等。这些组件的版本确定方式是看它们自己的 Release Notes 和官方文档。
以 fastjson 为例,早期 1.2.x 一堆安全漏洞,后来 1.2.83 是 1.x 系列的收尾版本,再往后官方重点维护的是 fastjson2(包名是com.alibaba.fastjson2)。这种库不能只看"Maven 上最新版",要根据安全公告和兼容性要求选。再说 jjwt,0.9.1 和 0.11.x 的 API 差别巨大,升级不是改版本号那么简单,还要改代码。
2.5 信息源的使用顺序
我自己在项目中的定版本顺序是这样的:
| 场景 | 信息源 | 注意事项 |
|---|---|---|
| 依赖在 Spring Boot BOM 内 | 官方 Dependency Versions 表 | 默认不写版本,必要时用 properties 覆盖 |
| 依赖有官方兼容矩阵(Kafka / Flink / ActiveMQ) | 中间件官方文档 | 先确认服务端版本再确认客户端版本 |
| 依赖不在 BOM 内 | Release Notes / 安全公告 | 选稳定版,不要一味追新 |
| 公司有多项目统一要求 | 内部 BOM / 架构组规范 | 个人项目也建议对齐,避免维护混乱 |
这张顺序表的核心原则是:能用 BOM 管的别自己拍板,BOM 管不了的时候再看中间件官方怎么说,最后才考虑第三方 Release Notes。
3. 实战选型:几类常见中间件的版本定法
3.1 Kafka:版本跟着 spring-kafka 走,还是跟着 broker 走
Spring Boot 项目整合 Kafka 时,通常引入spring-kafka(Spring 官方维护)。你写:
<dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> </dependency>这个依赖会帮你把kafka-clients带进来,版本由 BOM 管理。那"版本跟着谁走"的矛盾出现在这里:broker 可能是运维侧部署的 Kafka 2.8,而 BOM 带来的kafka-clients可能是 3.6。能不能连?
可以。Kafka 客户端对 broker 的向后兼容性做得好,官方给的支持矩阵很宽。但要注意:客户端用的协议版本可能太高,broker 不支持时会在日志里提示,某些新特性(比如事务、新的分区分配策略)会失败。
实操中我建议这样:先确认线上 broker 版本,再看 BOM 里kafka-clients版本是否在 Kafka 官方兼容范围内。如果不在,就把 BOM 里的kafka-clients用属性覆盖到合适的版本;如果只是想用 Spring 的KafkaTemplate等封装 API,一般不用刻意覆盖,因为 Spring Kafka 本身对客户端版本的宽容度也不差。
3.2 Redis 作为消息中间件:客户端版本与服务端协议
Redis 本身不是消息中间件,但很多人拿它的 Stream、List 当轻量消息队列用,所以"redis做中间件"的场景很常见。Spring Boot 项目里用的是spring-boot-starter-data-redis,默认客户端是 Lettuce。
版本怎么定?分两层看:一是 Lettuce 的版本由 BOM 管理,不用自己操心;二是保障 Lettuce 和 Redis 服务端的兼容,这个主要看 Redis 服务端版本。Redis 6.x 和 Redis 7.x 的功能集有差异,Lettuce 对新特性的支持取决于版本。如果你用的 Redis 服务端和 Lettuce 匹配,基本不用改版本。
这里有一个值得注意的坑:Lettuce 底层依赖 Netty,Netty 版本在 Spring Boot BOM 里也有管理。如果项目里其他中间件(比如 gRPC、Elasticsearch client)把 Netty 版本冲掉了,启动时就会冒出一堆奇怪的连接错误。这种问题光看中间件版本是找不到答案的,得查依赖树。
3.3 Flink 与 Spring Boot:双体系下的版本取舍
"springboot整合flink"是很多实时计算项目会遇到的情况。Flink 体系和 Spring Boot 体系在版本管理上是两套独立的体系:Flink 用自己的发行版和自己的依赖管理,Spring Boot 的 BOM 没有管理 Flink 相关依赖。
所以你引入 Flink 依赖时会遇到这个问题:
<dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-streaming-java</artifactId> <version>1.18.1</version> </dependency>Flink 版本谁来定?看你们实时计算集群的 Flink 版本。如果集群是 Flink 1.18,本地的依赖版本最好对齐。否则本地 Debug 跑通、提交到集群反而失败,这是最常见的坑。
Flink 和 Spring Boot 搭配时的另一个坑是依赖冲突。Flink 自带一些较老的第三方库(比如commons-cli、curator等),和 Spring Boot 项目里其他依赖容易冲突。我在实践中的做法是:把 Flink 相关依赖尽量标记为provided,让最终提交的 job 由 Flink 集群环境提供依赖,避免打进 Spring Boot 的 fat jar 里。这样既能保证版本统一,又能减少一大半冲突。
3.4 ActiveMQ 与 Artemis:starter 已经替你选好了核心版本
springboot整合activemq 时,官方 starter 是spring-boot-starter-activemq。这个 starter 会引入activemq-client和spring-jms。如果你用的是 ActiveMQ Artemis,对应引入spring-boot-starter-artemis。
这两个 starter 的区别不只是名字不同,底层客户端也不同。ActiveMQ "Classic" 5.x 用的是activemq-client,Artemis 用的是artemis-jms-client。版本怎么定?项目在没有特殊需求时直接引用 starter 即可,BOM 会帮你锁定核心库版本。
但要注意,spring-boot-starter-activemq默认带的连接方式是vm://这种嵌入式 broker,而生产环境一般要用tcp://地址连外部 broker。这种情况版本确定性就更清晰了:starter 提供的客户端要兼容你外部 broker 的版本,一般建议 ActiveMQ Classic 5.15+ 对应较新的客户端。如果线上 broker 是很老的 5.8,那就需要用属性覆盖客户端版本,否则协议可能会有问题。
3.5 不在 BOM 里的库:fastjson、jjwt、PostgreSQL 驱动的定版本方法
Spring Boot BOM 覆盖面虽广,但 fastjson、jjwt、PostgreSQL 驱动这些确实不在管理范围里。
fastjson 的定版本不能只看功能,要看安全。1.x 系列从 1.2.24 开始陆续爆反序列化漏洞,最终官方也建议升级到 1.2.83,或者直接迁移 fastjson2。个人项目如果只是解析 JSON,我更建议用 Jackson(Spring Boot 内置管理版本);如果团队历史代码已经重度依赖 fastjson,至少要用 1.2.83 以上的版本,并且关注后续安全公告。
jjwt 的问题在 API 断裂。0.9.1 用的是io.jsonwebtoken.Jwts和依赖javax.xml.bind的方式;0.11.x 换成了 builder API 风格,并且要求 JDK 8+。选版本前先看清项目代码是哪种写法,升级成本不是改一行版本号,而是改一坨代码。所以我一般建议:新项目直接用 0.11.x 或 0.12.x,老项目短期没精力改代码就先保持,但要意识到这是技术债。
PostgreSQL 驱动的版本要看数据库服务端版本。驱动版本一般向下兼容:JDBC 42.x 能连 PostgreSQL 9.x 到 16.x,但如果要用某些新特性(比如认证方式、pg_stat_statements的字段),还是要匹配足够新驱动。同时驱动版本需要与 JDK 版本兼容,这点和 Spring Boot 版本本身没有直接关系。
4. 一次依赖冲突的完整排查链路:从报错到定位再到修复
4.1 现象:启动时 NoSuchMethodError
讲一个我真实遇到过的场景。某项目引入了一个非 Spring 官方 starter 的第三方依赖包,同时项目里已经引入了 Spring Boot 的 Web 和 Actuator。启动时直接抛:
java.lang.NoSuchMethodError: 'void com.fasterxml.jackson.databind.ObjectMapper.<init>()'这个报错的本质是:某个依赖在运行时加载到的jackson-databind,和它编译时依赖的jackson-databind不是同一个版本。新版本里ObjectMapper的构造函数或某些 API 变了,旧的代码调用不存在的构造器,于是 JVM 在类加载方法解析阶段直接抛 NoSuchMethodError。
遇到这类报错,第一反应不能是"猜哪个依赖有问题",而应该立刻走依赖树排查链路。
4.2 用依赖树定位冲突源头
Maven 项目里最常用的命令:
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind这个命令会把项目里所有引入jackson-databind的路径打出来。-Dincludes能过滤出你关心的依赖,避免被几百行输出淹没。
输出大致长这样:
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:3.2.5:compile [INFO] | \- com.fasterxml.jackson.core:jackson-databind:jar:2.15.3:compile [INFO] \- com.example:xxx-client:jar:2.1.0:compile [INFO] \- com.fasterxml.jackson.core:jackson-databind:jar:2.17.0:compile这里出现了两个jackson-databind版本。Maven 的仲裁规则是"路径最近优先":xxx-client自己引了 2.17.0,距离当前项目更近,于是最终实际生效的是 2.17.0。Spring Boot 3.2.5 BOM 本来想管成 2.15.3,结果被这个更近的声明压过去了。
这也解释了前面说的:BOM 并不是强约束,遇到有依赖显式写版本且路径更近时,BOM 的版本会被覆盖。
4.3 仲裁规则与排除策略
搞清楚冲突源头后,要决定"保留哪个版本"。原则是:优先保留与 Spring Boot BOM 一致的版本,而不是保留最新版本。Spring Boot 的自动配置是围绕 BOM 版本测试过的,所以你更希望把 2.17.0 排掉,而不是把 2.15.3 升到 2.17.0。
排除操作有几种方式。最简单粗暴的是在依赖里排除:
<dependency> <groupId>com.example</groupId> <artifactId>xxx-client</artifactId> <version>2.1.0</version> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </exclusion> </exclusions> </dependency>排除之后,项目里不会再从这个路径引入 2.17.0。由于 Spring Boot BOM 已经管理了jackson-databind,最终生效版本回到 2.15.3。
还有一种方式是使用dependencyManagement强制指定版本:
<dependencyManagement> <dependencies> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.3</version> </dependency> </dependencies> </dependencyManagement>注意,dependencyManagement并不直接影响依赖树的"路径就近原则",它只是提供一个版本约束。如果某个依赖直接声明了 2.17.0 且路径更近,这个约束可能依然压不过它。大多数情况下还是要靠exclusion把冗余路径摘干净。
这里要提醒的是:不要因为在 pom 里看到冲突就顺手把别人的依赖整个排除。先确认那个传递依赖是不是真的不需要。有些库的传递依赖是运行时必需的,盲排除会导致 NoClassDefFoundError。
4.4 修复后的验证与"防复发"
修改完 pom,重新执行依赖树命令确认清爽了:
mvn clean test mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind看到只剩一个版本且符合预期,再启动应用验证。光看依赖树还不够,我见过依赖树没冲突但运行时报错的,那通常是 classpath 里有多个 jar 同时携带同名类,或者 fat jar 打包时重复打入了不同版本。所以修复后至少要跑一遍核心功能用例,不要只确认构建成功。
防复发的话,我建议在 CI 阶段加一步dependency:tree校验,把"项目里不能出现同一个依赖的两个版本"作为检查项。
5. 把版本策略固化成工程规范,而不是靠记忆力
5.1 项目级 BOM 模块的搭建思路
如果你维护的不止一个项目,我强烈建议做一个项目级 BOM 模块,比如company-dependencies。这个模块本身是一个只有pom.xml的空工程,里面用<dependencyManagement>统一管理项目组用到的所有中间件版本。
好处有三个:第一,不同项目引用的中间件版本对齐;第二,新项目开箱即用地继承既有版本决策;第三,中间件升级只需改 BOM 一处。
项目级 BOM 和 Spring Boot BOM 共同存在时,把项目级 BOM 放在 Spring Boot BOM 之前 import,再用显式声明的覆盖规则控制特殊依赖。这样既保留了 Spring Boot 官方组合稳定性,又能按团队需要做裁剪。
5.2 用 enforcer 插件在构建期拦截风险
Maven Enforcer Plugin 在版本控制里作用非常明显。我常用的规则有三个:
requireUpperBoundDeps:要求所有依赖解析到的版本不低于指定上界,能有效避免依赖树里混入过低版本。bannedDependencies:禁止某些有安全隐患或团队不认可的库,比如可以禁止直接依赖 fastjson 1.x。dependencyConvergence:强制依赖收敛到同一个版本,出现版本分歧时直接构建失败,逼着开发人员当场解决而不是带着隐患上线。
在项目的 pom 里加入 enforcer 后,CI 阶段就会自动执行检查。我的经验是,这类规则最好在项目起步阶段就加,中途加入会有一大堆历史债务需要一次性消化,但长痛不如短痛。
5.3 升级 Spring Boot 时的版本体检清单
升级 Spring Boot 大版本(比如 2.7 到 3.2)是一次系统性工程,因为基础变化巨大:javax.*改成jakarta.*,大量自动配置属性变更,BOM 里所有依赖版本集体上移。这时候"确定中间件版本"的工作量会膨胀到几十个依赖。
我建议按下面这个清单逐项过:
- 读官方 Release Notes 中 "Dependency Upgrades" 小节,看哪些库的版本被整体抬升。
- 检查自己的项目里,哪些依赖属于 BOM 管理,哪些不在 BOM 管理。
- 对于 BOM 管理的依赖,手动验证新版本是否有 API 变更影响到自定义代码。
- 对于 BOM 不管理的依赖,逐一到中间件官方文档确认兼容性。
- 跑全量回归,重点场景包括序列化、连接池、消息收发。
这套体检不是一天能完成的,但每完成一项,线上的隐患就少一块。
5.4 我的个人版本决策顺序
最后说下我自己的习惯,算是多年排除依赖问题后沉淀下来的流程,供你参考:
先确认 Spring Boot 版本,然后所有 BOM 内的依赖默认跟随;需要覆盖时优先用 properties。BOM 外的依赖,先确认中间件服务端版本,再去官方兼容矩阵里找对应的客户端版本;有安全公告的库优先跟随安全版本。版本冲突时,先恢复 BOM 一致性,再处理业务需要。整个过程最后都会落到 dependency:tree 的验证上,确认没有两个同类依赖共存才算完。
版本这个东西,靠记忆是靠不住的。你真正需要的是机制:用 BOM 做底座,用兼容矩阵做校准,用依赖树做验证,用 enforcer 做防线。把这套机制搭起来之后,中间件版本从"每次都要查一遍"变成"按流程走一遍",心就踏实了。