Spring Boot依赖管理:parent POM与BOM的差异及选型指南
1. 两个名字背后的本质差异继承机制与导入机制接触Spring Boot一段时间的人基本都会遇到这个困惑项目里既见过spring-boot-starter-parent又见过spring-boot-dependencies看起来都是管依赖版本的到底什么关系我最早也以为这俩是同一个东西的两种叫法直到有一次在团队里改公共父POM把spring-boot-starter-parent换成了spring-boot-dependencies结果一大批子项目的插件行为全变了才真正搞清楚它们根本不是一回事。先说最本质的区别spring-boot-starter-parent是一个真正的Maven父POM你的项目通过parent标签去继承它而spring-boot-dependencies只是一个BOMBill of Materials物料清单文件通过dependencyManagement里的scopeimport/scope来导入。一个是继承关系一个是导入关系这两个词背后是完全不同的Maven语义。我用一个生活化的类比来解释继承父POM相当于你从父母那里继承财产父母有的你都有父母定义的属性、插件、依赖版本约束全部流到你这里你还能覆盖它们。导入BOM相当于你抄别人的购物清单对方买的每样东西你记下了品牌和型号但你不会继承对方的房产、车子和生活习惯。具体到Maven行为上继承spring-boot-starter-parent意味着你不仅拿到了依赖版本管理还拿到了maven-compiler-plugin、maven-surefire-plugin、maven-jar-plugin等一系列插件的预配置版本和参数project.build.sourceEncoding、project.reporting.outputEncoding等属性预设repositories和pluginRepositories仓库配置pluginManagement里的插件版本锁定resources配置默认的resource过滤规则Java版本属性java.version自Spring Boot 3.x起默认是17而spring-boot-dependencies字面意思就很直白——它只管依赖版本。它是一个巨大的dependencyManagement配置里面锁定了Spring Boot各个模块以及它依赖的第三方库比如Jackson、SLF4J、Hibernate等的版本号。你把它import进来之后声明依赖的时候可以省掉version仅此而已。编译器版本、编码格式、插件配置、仓库地址统统不管。这也解释了为什么很多公司里的公共父POM会推荐用spring-boot-dependencies而不是直接用spring-boot-starter-parent因为公司往往有自己的父POM来统一管理插件、编码和发布配置如果你的公司父POM再去继承Spring Boot的parent就会失去对自身父POM的控制权。Maven的继承是单根的一个项目只能有一个parent但可以import多个BOM。明白了这个本质区别之后后面所有行为差异都能推导出来。接下来我会逐个拆解。2. 继承parent带来的全家桶配置到底多出来的东西是什么很多人用spring-boot-starter-parent用得毫无感知以为它只是省掉了写版本号的麻烦。其实它做的事情远比版本管理多得多。我建议你找一个Maven项目执行一下mvn help:effective-pom把输出的effective-pom和你的pom.xml对比一下你会看到Spring Boot parent塞进来的大量配置。2.1 插件版本与配置的统一锁定spring-boot-starter-parent的pluginManagement部分定义了一整套插件及其版本你在子项目里只需要声明plugin而不写version就能获得一个经过Spring Boot官方验证的、彼此兼容的插件版本组合。以我手头Spring Boot 3.2.x为例parent锁定的关键插件大概有这些插件作用Spring Boot 3.2.x默认版本maven-compiler-pluginJava编译3.13.0maven-surefire-plugin单元测试执行3.2.5maven-failsafe-plugin集成测试执行3.2.5maven-jar-plugin打jar包3.4.1maven-war-plugin打war包3.4.0spring-boot-maven-pluginSpring Boot插件负责repackage跟随Spring Boot版本maven-resources-plugin资源文件处理3.3.1注意这些版本不是随便选的。比如surefire版本和JDK版本之间有兼容性关系官方在parent里锁定这些版本相当于帮你做了一轮版本兼容性测试。我自己曾经在一个子项目里手动升级了surefire到某个测试版结果mvn test阶段在JDK 21上直接抛异常回退到parent锁定的版本就一切正常。这算是我第一次对parent的隐藏价值有直观感受。2.2 Java版本、编码等基础属性预设spring-boot-starter-parent里有一堆properties其中最常用的properties java.version17/java.version project.build.sourceEncodingUTF-8/project.build.sourceEncoding project.reporting.outputEncodingUTF-8/project.reporting.outputEncoding maven.compiler.source${java.version}/maven.compiler.source maven.compiler.target${java.version}/maven.compiler.target /properties继承之后你的maven-compiler-plugin会自动使用java.version指定的版本作为source和target。Spring Boot 3.x默认是17如果你不想用17单独在properties里覆盖java.version即可比如开发环境统一JDK 11的时候虽然3.x本身要求17起步实际项目里选型时要注意。编码这块也值得多说一句很多新手项目在Windows上开发Linux上构建如果不统一UTF-8中文注释和资源文件很容易乱码。parent里预设了UTF-8等于默认帮你解决了一个隐藏的坑。如果你用的是spring-boot-dependencies这些全部要自己在公司父POM里补漏一个就可能在特定环境踩坑。2.3 资源过滤与构建信息的注入parent还配置了资源处理规则默认对application.properties、application.yml这类文件做property占位符替换。什么意思呢就是你在配置文件里写app.nameproject.artifactId app.versionproject.version构建的时候Maven会自动把...替换成实际的artifactId和version。这个功能很多团队用得非常顺手但它同样是通过spring-boot-starter-parent的resources配置实现的。如果改用BOM导入这个变量替换不会生效除非你在自己的pom里手动配置maven-resources-plugin的过滤规则。我见过有同事切到BOM之后配置文件里project.version原样打进了jar包排查了半天。2.4 单继承限制的现实困境说到这里你可能会想那既然parent这么好大家都用parent不就行了吗问题在于Maven的继承是单根的你只能有一个父POM。现实情况是很多公司内部已经有自己的company-parent里面统一了私服仓库地址repositories、distributionManagement发布插件如maven-deploy-plugin、nexus-staging-maven-plugin统一的代码质量插件如spotbugs-maven-plugin、checkstyle依赖版本的公司级约束比如强制某个日志框架版本在这种情况下子项目如果继承spring-boot-starter-parent就没法再继承company-parent了。解法通常有两种一种是让company-parent去继承spring-boot-starter-parent。这样做的代价是公司父POM被Spring Boot绑架一旦升级Spring Boot版本company-parent也要跟着改而且公司自己塞进去的插件配置如果和Spring Boot parent里冲突处理起来会比较麻烦。另一种就是今天的主角切换——用spring-boot-dependencies导入保留company-parent作为唯一父POM。这也是我最终在实际项目中采用的方案。3. 使用spring-boot-dependencies的精简之道BOM导入的正确姿势现在聊spring-boot-dependencies。它其实也是spring-boot-starter-parent内部的一部分——parent里通过import把这个BOM引入了。也就是说spring-boot-starter-parent内部就依赖了spring-boot-dependencies它的依赖版本管理能力完全来自于这个BOM。3.1 标准配置模板在你的pom.xml里正确导入方式是把它放进dependencyManagementdependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version3.2.5/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement这段配置之后你在dependencies里声明Spring Boot相关的依赖就不用再写版本号了dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency /dependencies除此之外项目里常用的第三方库比如com.fasterxml.jackson.core:jackson-databind、org.projectlombok:lombok、org.slf4j:slf4j-api同样不用写版本号。BOM里管理了数百个依赖条目社区里有一句话说得很形象只要你在mvn dependency:tree里看到的是Spring Boot官方推荐的版本组合那大概率就是BOM在起作用。3.2 手动指定版本的覆盖顺序使用BOM之后还有一个值得了解的地方dependencyManagement里的匹配顺序是按声明顺序来解析的。如果你在BOM之后又手动声明了一个版本那可以覆盖BOM里的版本。比如dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version3.2.5/version typepom/type scopeimport/scope /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.16.2/version /dependency /dependencies /dependencyManagement后声明的jackson-databind会覆盖BOM给的版本。这是Maven本身的行为逻辑与Spring Boot无关理解了这一点排版本冲突的时候思路会清晰很多——先看mvn dependency:tree再判断到底是哪一层dependencyManagement在起作用。3.3 插件的使用方式self-managed也有依赖管理需要注意的是BOM只管dependencies里的依赖不管buildplugins里的插件。这意味着你使用spring-boot-maven-plugin、maven-compiler-plugin这些插件时不再有parent帮你锁定版本必须在pluginManagement里手动声明版本。Spring Boot的repackage把普通jar包变成可执行jar包依赖的spring-boot-maven-pluginBOM里其实也管理了插件版本——准确说BOM的pluginManagement部分同样存在。但和parent不同BOM里的pluginManagement不会自动生效你需要在build节点里显式引用并配合版本。最常见的写法是build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId version3.2.5/version configuration mainClasscom.example.YourApplication/mainClass /configuration executions execution goals goalrepackage/goal /goals /execution /executions /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.13.0/version configuration source17/source target17/target /configuration /plugin /plugins /build如果你的公司父POM里已经通过pluginManagement统一配置了这些那子项目就只需要声明引用不需要写版本号。这是公司级工程与Spring Boot BOM协作的典型姿势。3.4 多个BOM并存时的注意事项BOM还有一个parent做不到的好处可以import多个。比如项目中既需要Spring Boot又需要Spring Cloud还可能需要阿里巴巴的spring-cloud-alibaba-dependencies三者可以同时importdependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version3.2.5/version typepom/type scopeimport/scope /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-dependencies/artifactId version2023.0.1/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement但是要记住一个关键原则spring-cloud-dependencies的版本针对的是对应的Spring Boot版本。如果Spring Boot和Spring Cloud版本不匹配运行时会出现各种奇怪的问题最常见的就是某些自动配置不生效、上下文启动失败。我在项目里维护了一个版本对应表Spring Cloud 2023.0.x对应Spring Boot 3.2.x区别就在这里——BOM帮你管理库版本但库之间的兼容矩阵仍然要人来操心。4. 依赖仲裁与版本管理的底层逻辑为什么BOM能让你不写版本号要真正理解spring-boot-dependencies和spring-boot-starter-parent的区别光看表面配置还不够最好理解一下Maven的依赖仲裁机制。这个内容可能有点偏原理但理解了之后排查问题的速度快一个量级。4.1 依赖版本优先级的三条基本规则Maven在解析一个依赖的版本时遵循这几条规则按优先级排列最短路径优先如果项目直接声明了某个依赖那么直接声明的版本优先于传递依赖进来的版本。先声明者优先如果两个传递依赖都带了同一个库的不同版本路径深度相同的情况下在dependencies里声明靠前的那个胜出。父POM的dependencyManagement优先如果父POM或dependencyManagement里显式声明了某个依赖的版本那么这个版本会覆盖所有的传递依赖版本无论路径长短。其中第三条就是BOM发挥作用的核心机制。spring-boot-dependencies本质上是一个超大的dependencyManagement集合是你依赖版本的最高仲裁层。你import了BOM相当于给所有的传递依赖套上了一个版本统一约束所有依赖最终都会向BOM看齐。4.2 一个实际例子Jackson版本冲突举个例子你的项目用spring-boot-starter-web它传递依赖了jackson-databind2.15.x同时另一个库又传递依赖了jackson-databind2.14.x。如果没有BOMMaven按最短路径或先声明者优先可能选中2.14.x导致运行时出现NoSuchMethodError。有了Spring Boot BOM的约束最终统一使用2.15.x具体版本跟随Spring Boot版本冲突消弭于无形。这也是为什么我一直建议团队不要轻易在dependencyManagement里覆盖Spring Boot BOM里的版本特别是Jackson、Netty这种被大量库依赖的基础组件。你改了版本不一定能跑起来——因为Spring Boot的自动配置和代码是围绕它测试过的版本编译的混搭版本很高概率出问题。4.3 parent与BOM在依赖管理上的区别总结维度spring-boot-starter-parentspring-boot-dependencies引入方式parent继承dependencyManagementimport依赖版本管理有内部import了BOM有本体的全部内容Java版本/编码属性有默认值并可覆盖无插件版本锁定有有pluginManagement但不自动生效资源过滤配置有默认开启..替换无仓库配置有额外仓库无与公司父POM的关系冲突单继承不冲突可共存多BOM共存不支持支持4.4 亲测有效的排障命令最后分享三个我在排查依赖问题时必用的命令mvn dependency:tree查看完整依赖树看每个依赖的实际版本。mvn dependency:tree -Dverbose显示依赖被忽略的原因什么规则导致的。mvn help:effective-pom查看最终的合并配置确认BOM是否真的生效、父属性覆盖情况。排查版本问题时先看effective-pom里的dependencyManagement再看dependency:tree的输出基本都能定位到哪一层在捣乱。5. 插件行为差异从parent切换到BOM后构建行为会偷偷改变的地方如果说依赖版本管理是两者的明面差异那插件行为差异就是暗面差异。很多人切到BOM之后项目还能编译、还能运行但在一些细节上表现完全不同最典型的就是前面提到的几个点。我单独拉一节出来细讲。5.1 spring-boot-maven-plugin的repackage行为用spring-boot-starter-parent的项目spring-boot-maven-plugin是自动配置在buildplugins里的默认会执行repackagegoal。也就是说mvn package打出来的jar包就是可执行的fat jar。这里面还隐含了一层作用——repackage会把原来的普通jar重命名成xxx.jar.original替换出可执行jar。切到BOM之后spring-boot-maven-plugin默认不再出现在插件列表里。如果项目里没有显式引入这个插件mvn package打出来的jar只是一个普通的、带有依赖坐标的jar包直接java -jar会报no main manifest attribute错误。所以切记BOM方案下spring-boot-maven-plugin必须自己显式声明并配置execution。5.2 编译插件和surefire的行为差异parent锁定了maven-compiler-plugin的特定版本并且预设了source/target。BOM方案下如果不手动配插件版本取决于Maven默认捆绑的版本或你自己pom里的声明source/target可能默认是Maven运行时的JDK版本。这会导致一种很隐蔽的坑开发机器JDK 17生产环境JDK 11用parent构建的产物是11字节码切到BOM之后可能变成17字节码部署到生产直接ClassFormatError。所以切BOM的时候maven-compiler-plugin的source和target必须显式写清楚或者通过maven.compiler.source属性统一管理。我在团队里推行了一个约定所有pom必须显式声明maven.compiler.release而不是source/target因为release同时约束编译源码和运行的JDK API比source/target更安全。5.3 资源过滤的切换代价前面提到parent默认开启property占位符替换。切到BOM后如果你们的配置文件里用了project.version这类写法必须自己配置maven-resources-pluginplugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-resources-plugin/artifactId version3.3.1/version configuration encodingUTF-8/encoding nonFilteredFileExtensions nonFilteredFileExtensionpng/nonFilteredFileExtension nonFilteredFileExtensionjpg/nonFilteredFileExtension /nonFilteredFileExtensions useDefaultDelimiterstrue/useDefaultDelimiters delimiters delimiter/delimiter /delimiters /configuration /plugin不配的话配置文件里project.version会被原样带进jar包启动时如果代码里对做了特殊解析还会引发其他问题。这个坑我在上线前的集成测试里踩过当时打出来的jar包解压看配置文件里面赫然是project.version排查了将近半天才确认是资源过滤没生效。5.4 properties占位符与Spring的协同补充一个容易误解的点Spring Boot本身在application.properties里也支持${..}占位符比如${java.version}。这和Maven的..过滤是两个体系前者是Spring运行时解析后者是构建期替换。项目里用BOM方案时如果想在配置文件里注入Maven变量建议约定好只使用..不要用${project.version}否则可能被Spring Boot误当成环境变量占位符处理在本地开发时跑出一个莫名其妙的空值。6. 实际项目中的选型建议与迁移踩坑记录看完了原理和差异最后落到现实你的项目应该用哪种方案以及在从parent搬到BOM的时候有哪些坑可以提前避开。这一节的内容是我自己团队迁移过程中的真实记录希望能帮你少走弯路。6.1 什么情况下继续用starter-parent如果满足下面几个条件直接用spring-boot-starter-parent是最省心的选择你是独立项目没有强制的公司父POM约束。团队对Maven的个性化配置需求很少。你希望开箱即用不想手工维护编译插件版本、编码属性、资源过滤规则。不需要同时import其他BOM比如不涉及Spring Cloud Alibaba这类体系。特别是个人项目、快速原型、微服务数量不多的小团队parent的价值在于减少决策。少写100行pom配置换来的是稳定和一致这比所谓的灵活重要得多。6.2 什么情况下必须转向spring-boot-dependencies反过来这些情况说明你需要BOM方案公司已有统一的parent且不易变更。需要同时管理Spring Boot Spring Cloud 第三方BOM比如Dubbo、Sentinel的BOM。你需要在dependencyManagement里覆盖某些依赖版本并且不希望parent的插件配置干扰你的构建。团队有专门的平台工程组维护公共父POM依赖版本统一由BOM控制。实际上现在多数中大型公司走的都是这条路公司公共company-parent负责插件、发布、编码、仓库Spring Boot的版本控制全部走spring-boot-dependenciesimport。这样职责清晰互不干扰。6.3 迁移时按这个顺序做能避开90%的坑如果决定从parent切到BOM按下面的顺序操作大概率比直接改pom顺利得多先备份原始pom最好用git打一个tag。移除parent替换为dependencyManagement里的BOM import。注意parent里可能还定义了relativePath一并处理。逐项迁移属性把parent里你需要的东西复制到自己的properties特别是java.version、project.build.sourceEncoding。显式声明maven-compiler-plugin、spring-boot-maven-plugin和生产期必要的插件版本不要依赖默认版本。检查资源过滤项目是否有..占位符有则配置maven-resources-plugin。执行mvn help:effective-pom确认编译插件版本、source/target、依赖管理里有BOM的条目。执行mvn clean package跑一遍单元测试和传递依赖检查。如果有Spring Cloud等组件确认BOM版本与Spring Boot的兼容矩阵用官方文档核对。6.4 我在迁移中踩过的坑第一个坑是surefire版本导致的测试失败。切BOM之后由于公司父POM里surefire锁定的是旧版本它在JDK 17上跑某些使用了--enable-preview的测试时直接报错而parent锁定的版本可以在同一JDK上正常跑。当时同事一度怀疑是代码问题折腾了半天才定位到插件版本差异。从那以后我养成了一个习惯任何pom改动第一件事就是看mvn help:effective-pom里插件版本是否有变化。第二个坑是spring-boot-maven-plugin的repackage和maven-surefire-plugin的执行顺序问题。BOM方案下如果你把spring-boot-maven-plugin的execution声明放在surefire前面可能会导致测试阶段就触发了repackage打出来的测试jar包行为异常。稳妥的做法是让repackage绑定在package阶段测试由surefire独立执行。这也提醒了我BOM方案下所有插件的行为都要重新审视一遍不能指望环境帮你兜底。第三个坑是公司的公共父POM里如果已经声明了某些依赖的版本比如lombok放在dependencyManagement里时会覆盖BOM的版本。如果公司锁定的lombok版本和Spring Boot预期的版本相差太远编译期不报错但运行时某个注解处理可能异常。这个就需要和平台组协商尽量让公司层面少锁版本或者锁定版本时以Spring Boot BOM为基准。6.5 最后说一句关于版本升级的体会每季度Spring Boot出新版的时候用parent的项目升级方式很简单改一个parent版本号就行。用BOM的项目多一个步骤——还要确认公司父POM没有锁住某个关键依赖的旧版本否则BOM的版本约束会被公司层级的dependencyManagement覆盖。说到底parent方案把升级动作收敛到一行BOM方案把升级动作分散到多处换来的是自定义空间。没有绝对的好坏只有是否匹配你的工程治理水平。我个人在维护多个老项目时现在的偏好是独立小项目用spring-boot-starter-parent被公司统一管理的项目用spring-boot-dependencies。每次新项目立项我第一件事就是问清楚团队的父POM策略再决定怎么引入Spring Boot。这个决定虽然小但会影响后面每一次构建、每一次升级的体验。