
这几年维护过的 Java 后端项目里SpringBoot 占了绝大多数。每个 SpringBoot Maven 项目的 pom.xml 里plugins和plugin这块配置我几乎每次都要动一遍。很多人对依赖很熟但对 plugin 的理解还停留在“打包的时候复制一段配置进去就行”。真到了项目裂开构建报错、本地跑不起来、测试被跳过、版本冲突找不到原因的时候才发现插件这块绕不过去。所以这次专门把 SpringBoot Maven 项目 pom 中对 plugin 插件的常见用法完整地理一理从插件机制讲到具体配置再到排查思路尽量让一份 pom 里的插件部分能当作可复用的配置文件来抄。这篇文章适合正在搭建 SpringBoot 项目、被构建配置折磨的初学者也适合想把自己的 pom 插件段整理得更干净的有经验开发者。内容按 Maven 插件机制、SpringBoot 高频插件、辅助插件、排错实战、最终配置模板分类展开每段都配了实际能直接用的配置片段和参数解释。先把话说在前面Maven 插件这块90% 的坑不是插件本身多难而是不清楚某个 goal 到底在哪个阶段被触发、某个参数到底传给谁了。1. 先搞懂插件在 Maven 里扮演什么角色1.1 插件和依赖是两回事我看到过很多人的 pom 里把dependencies和buildplugins混着理解觉得都是“引一个 jar 进来”。这个想法会带来很多隐患。依赖是项目运行时的“原料”解决的是代码里能不能 import、类路径上有没有这个类的问题。而插件是构建工具的“功能模块”解决的是 Maven 在执行编译、测试、打包、部署这些动作时具体怎么做的问题。比如 maven-compiler-plugin 负责把 Java 源码编译成 classspring-boot-maven-plugin 负责把打出来的 jar 变成能java -jar直接运行的可执行包。一个比较直观的类比依赖是菜市场买回来的菜和肉插件是厨房里的锅碗瓢灶。菜买了但不一定会做饭锅有了但没有菜也做不成。pom 里这两块区域维护起来互不影响但构建结果两个都影响。插件本身的“体积”一般很小因为 Maven 核心只负责构建调度真正的编译、测试、打包都交给插件完成。插件也是一个 jar也有 groupId、artifactId、version只是它会在执行特定阶段时被加载到构建环境里。所以插件的版本管理同样要认真对待尤其当你用的是老 Maven 版本时默认绑定的某些插件版本可能很旧和你的 JDK 版本、SpringBoot 版本都对不上。1.2 核心概念goal 与 execution这是理解 plugin 插件用法绕不开的两个词也是很多人配置报错时看不懂日志的根源。每个插件都由多个“动作”组成这些动作叫 goal。比如 maven-dependency-plugin 里有dependency:copy-dependencies、dependency:analyze、dependency:tree等 goal。mvn dependency:tree这行命令的本质就是调用了 maven-dependency-plugin 的 tree 这个 goal。而 execution 是把某个 goal 绑定到 Maven 生命周期的某个阶段。Maven 对每个构建动作都有预定义阶段顺序比如 clean、compile、test、package、install、deploy。默认情况下很多通用插件的 goal 已经绑定到固定阶段了。比如 maven-compiler-plugin 的 compile goal 默认绑定在 compile 阶段不需要你在 pom 里额外配置一个execution。但像 spring-boot-maven-plugin 的 repackage goal默认绑定在 package 阶段而且 Spring Boot 官方文档里默认示例会专门写一段execution就是为了明确告诉 Maven 在 package 阶段执行 repackage。看插件是否真的执行了用mvn clean package跑一遍时观察日志里有没有对应的[INFO] --- xxx-maven-plugin:version:goal (id) project ---输出。如果日志里没有这一行说明它没被绑定到当前生命周期里执行通常就是execution没配对。1.3 插件配置的结构长什么样一个完整的插件配置块一般长这个样子build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId version2.7.18/version executions execution idrepackage/id goals goalrepackage/goal /goals /execution /executions configuration mainClasscom.example.demo.DemoApplication/mainClass classifierexec/classifier /configuration /plugin /plugins /buildconfiguration是给插件的参数区。不同插件、不同 goal 能接受的参数不同最靠谱的方式是查对应插件的官方文档而不是靠记忆。你配置了一个不存在的参数Maven 大多不会报错而是静默忽略这是最容易埋雷的地方之一。另外要明确一点version能省则省的前提是你的父 pom 或项目本身引入了 Spring Boot 依赖管理它会帮你统一管理 spring-boot-maven-plugin 的版本。但非 Spring Boot 官方管理的第三方插件比如一些自定义的、内部的插件最好明确写版本否则不同机器上构建出的结果可能完全不一样。2. SpringBoot 项目里点名率最高的三个插件2.1 spring-boot-maven-plugin让项目能“直接双击跑”的关键这个插件是 SpringBoot 项目的“原配插件”。它主要提供了多个 goal最常用的有三个repackage、run、build-info。repackage 的核心作用是把mvn package打出来的普通 jar 再加工一次重写成可执行 jar。SpringBoot 可执行 jar 的目录结构和普通 jar 不一样两者差异很大。食物链路里普通 jar 就是一盒菜料repackage 之后才是一份能下锅出菜的套餐里面包含了内嵌的 Tomcat、所有依赖 jar、启动类信息和META-INF/MANIFEST.MF里的Main-Class与Start-Class。如果你不配 spring-boot-maven-plugin、只靠 maven-jar-plugin 打普通 jar那么打出来的 jar 用java -jar运行大概率会报“没有主清单属性”。所以 SpringBoot 项目的 pom 里没有它整个打包链路就塌了一半。默认情况下repackage goal 必须在 package 阶段后执行官方推荐写法plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration mainClasscom.example.demo.DemoApplication/mainClass /configuration executions execution goals goalrepackage/goal /goals /execution /executions /pluginmainClass默认会自动从项目依赖里找Start-Class或主类但有些多入口项目会找不到主类手动指定最稳妥。如果你希望同时保留原始 jar 和可执行 jar在configuration里加classifierexec/classifier即可这样会生成一个项目名-exec.jar原始 jar 不会被覆盖。做法很简单但这个细节救过我好几次尤其是当项目被另一个模块依赖时依赖不能去依赖可执行 fat jar否则类路径会出问题。对于本地开发还可以直接mvn spring-boot:run启动项目这个 goal 会编译完成后直接运行主类。配合 spring-boot-devtools 时它能做到代码变更热重启。但要注意生产环境别用 devtools影响性能和稳定性这是常见坑。另外最近总有人问“SpringBoot 的 jar 怎么反编译还原成项目”。如果你用 spring-boot-maven-plugin 打了可执行 jar里面的结构是BOOT-INF/classes下放项目类文件BOOT-INF/lib下放依赖还原项目时把BOOT-INF/classes里的包结构提取出来再通过 pom 里的依赖列表反查版本是能恢复出源码结构的。但如果 jar 是普通 jar里面的 class 散落在根路径下还原难度就会大很多。所以插件配置是否正确直接影响后续排查和逆向整理的效率。2.2 maven-compiler-plugin把 Java 编译版本锁死很多刚接触 SpringBoot 的人会忽略这个插件因为编译器不带它也能连编译。真正的问题出现在多环境、多 JDK 版本的协作场景里。maven-compiler-plugin 默认绑定在 compile 阶段负责把 Java 源码编译成字节码。它最核心的配置是编译版本。常见三种写法plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.11.0/version configuration release11/release parameterstrue/parameters /configuration /pluginsource和target是老写法分别表示源码语言版本和目标 class 版本。有一个坑只设置 target 为 8编译器依然可能使用高版本 JDK 的 API导致编译出来的代码在低版本 JDK 上抛NoClassDefFoundError或UnsupportedClassVersionError。更稳的写法是使用release11/release它会同时锁定 source、target 和 API 的检查级别JDK 9 以上推荐用它。比如我在 JDK 17 环境里编译一份目标为 11 的代码写release11/release编译结果在 JRE 11 上能直接运行也不会误用 JDK 17 的新 API。配置里还有个parameterstrue/parameters它让编译器在 class 文件里保留方法参数名这个参数对 Spring MVC 的接口参数名解析是必要的否则某些 IDE 或反射场景可能拿不到参数名。再有就是 Lombok 这类注解处理器。推荐在 maven-compiler-plugin 里显式配置annotationProcessorPaths确保构建时使用精准版本不用把 Lombok 打进运行时依赖。现在很多团队对产物体积敏感这招能省不少心。2.3 maven-surefire-plugin测试到底跑不跑文件说了算surefire 是 Maven 默认的测试执行插件。SpringBoot 项目里只要写了测试类它就参与构建默认绑定 test 阶段。它最常见的操作是控制测试的执行。我常用的几个场景分别是本地构建不跑测试但又要编译测试类mvn clean package -DskipTests连测试编译都跳过mvn clean package -Dmaven.test.skiptrue只跑某个指定测试类mvn test -DtestUserServiceTest只跑某个方法mvn test -DtestUserServiceTest#testLogin区别要拎清楚-DskipTests会编译测试类但不执行-Dmaven.test.skiptrue是编译和执行全部跳过。CI 里想快速验证主代码能否打包用-DskipTests更合理。在 pom 里也可以写死一些默认规则比如只执行*Test.java结尾的测试类plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId version3.0.0-M7/version configuration skipTests${skipTests}/skipTests includes include**/*Test.java/include include**/*Tests.java/include /includes excludes exclude**/*AbstractIntegrationTest.java/exclude /excludes /configuration /plugin这个skipTests${skipTests}/skipTests写法很有意思它把 pom 里的默认值变成一个可从命令行覆盖的属性。构建时执行mvn test -DskipTestsfalse就能强制开启测试。很多团队把某些慢速集成测试放到*IT.java命名再用 surefire 排除、用 failsafe 插件执行这里不展开但思路一致用插件配置精确控制测试范围。3. 构建过程里常用但容易被忽略的插件3.1 maven-resources-plugin环境占位符与资源过滤SpringBoot 项目里经常有多个环境配置application-dev.properties、application-prod.properties通过spring.profiles.active选择。但有时你想在构建时根据 profile 自动替换配置里的${db.url}这类占位符这就要用 maven-resources-plugin 的资源过滤能力。资源过滤是默认关闭的开启方式是在build里写resourcesbuild resources resource directorysrc/main/resources/directory filteringtrue/filtering /resource /resources /build开启之后资源文件里形如${project.version}、${spring.profiles.active}的占位符在构建时会被替换成 project 的属性值或 Maven profile 里定义的属性值。它默认绑定在 process-resources 阶段一般不需要手动配execution。但这里有一个需要提醒的地方如果你的src/main/resources里有证书、图片、字体、.so文件这类二进制资源开全局 filtering 可能会导致二进制文件损坏因为它会按字符方式扫描并替换文本占位符。我见过有人把公司公众号二维码图片放 resources 下构建后图片直接打不开。解决方法是配置 nonFilteredFileExtensionsplugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-resources-plugin/artifactId version3.3.1/version configuration nonFilteredFileExtensions nonFilteredFileExtensionpng/nonFilteredFileExtension nonFilteredFileExtensionico/nonFilteredFileExtension nonFilteredFileExtensionpem/nonFilteredFileExtension nonFilteredFileExtensionso/nonFilteredFileExtension /nonFilteredFileExtensions /configuration /plugin简单说文本类配置放心过滤二进制扩展名全加上排除。这算是我实操中遇到过的最隐蔽的“坏文件”来源。3.2 maven-dependency-plugin把依赖收集到服务器目录如果你的项目需要最终发布一个包含全部依赖 jar 的目录而不是单个可执行 jarspring-boot-maven-plugin 可能帮你不上忙因为 fat jar 里的依赖在BOOT-INF/lib内。但你做的是一个多模块项目要把每个服务部署在指定目录手动复制依赖是件痛苦事maven-dependency-plugin 的copy-dependencies就是干这个的。我通常给它绑定到 package 阶段让每次构建都自动把依赖拷贝到target/lib目录配合一个外层脚本启动plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-dependency-plugin/artifactId version3.6.0/version executions execution idcopy-dependencies/id phasepackage/phase goals goalcopy-dependencies/goal /goals configuration outputDirectory${project.build.directory}/lib/outputDirectory includeScoperuntime/includeScope excludeScopeprovided/excludeScope /configuration /execution /executions /pluginincludeScope和excludeScope是两个很实用的过滤条件runtime表示编译时不需要但运行时必需的依赖比如驱动provided表示容器或 JDK 已自带比如 servlet-api。如果你把 scope 配反了拷贝出来的目录会多出一堆运行时根本用不到的 jar把部署包体积撑大。同样常用的是dependency:analyze它用来分析项目里“声明了但没用到”的依赖以及“用到了但没声明”的依赖。注意它不是百分百准确但对清理 pom 很有帮助。3.3 maven-enforcer-plugin从源头拦住版本问题“springboot版本太高”这个搜索词可以说是每个后端群里的高频话题。SpringBoot 2.7 和 SpringBoot 3.0 之间 Java 版本要求截然不同SpringBoot 3 要求 Java 17 起步如果团队里有同事还在用 JDK 8合并到主分支后在 CI 上就会出现莫名其妙的构建失败。如果能在构建开始前直接拦下来会省去很多沟通成本。maven-enforcer-plugin 专门做这件事。它能在构建最开始检查环境不满足条件就直接 FAIL。我用过的一个相对完整的配置plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-enforcer-plugin/artifactId version3.3.0/version executions execution idenforce-versions/id goals goalenforce/goal /goals configuration rules requireJavaVersion version[11,12)/version /requireJavaVersion requireMavenVersion version[3.6.3,)/version /requireMavenVersion bannedDependencies excludes excludecommons-collections:commons-collections:*/exclude /excludes /bannedDependencies /rules failFasttrue/failFast /configuration /execution /executions /pluginrequireJavaVersion的版本区间写法符合 Maven 版本范围规则比如[11,12)表示大于等于 11 且小于 12即锁定 JDK 11。很多技术栈升级时团队就是靠这个插件统一在入口做拦截比在一堆文档里列注意事项有效得多。如果你不想让某个老库出现在依赖里用bannedDependencies直接禁止比 code review 时人工揪依赖可靠。3.4 两个能提升构建信息可见度的插件第一个是 maven-assembly-plugin它可以把项目主 jar、依赖 jar、配置文件、shell 脚本、Dockerfile 等打包成一个自定义结构的 zip 或 tar.gz。这个在传统运维部署模式里很常见。它的配置相对复杂核心是描述assemblyDescriptor自定义格式偏多这里就不贴全套代码了只提一句如果你需要打包成“部署包”它是正解。第二个是 git-commit-id-plugin。它能把当前 Git 分支、commit 版本、构建时间等信息写进构建产物里。用它的好处是线上查问题时打开/actuator/info接口就能看到这个版本是哪个 commit 构建出来的。有一说一线上问题排查时这个信息价值极大比在标题里写v1.0.1靠谱太多。配置很简单绑好 goal 后生成一个git.properties文件放进 classpath 即可。4. 插件相关报错与排查实战4.1 插件下载不下来、仓库配置失效怎么办插件没有下载到本地的话构建日志会出现类似Could not resolve plugin org.apache.maven.plugins:maven-compiler-plugin:3.1的报错。原因通常是本地 Maven 仓库.m2/repository里没有缓存且配置的远程仓库无法访问。国内访问 Maven 中央仓库很慢我在自己的settings.xml里配了阿里云镜像仓库这么多年用下来很稳。镜像配置放在settings.xml的mirrors里如果这个文件没找到注意 Maven 默认会读Maven安装目录/conf/settings.xml最好不要直接改这个全局文件而是复制一份到用户目录下的.m2/settings.xml。很多人的.m2目录没有 settings.xml导致自定义的镜像配置完全不生效这个点非常隐蔽。另一个容易踩的坑IDEA 里项目的 Maven 设置指定了错误仓库地址或者你在 IDEA 里手动改了 plugin 仓库地址却和命令行构建用的配置不一致。排查思路是先确认 IDEA 的 Maven home path、user settings file、local repository 三个路径是否和命令行一致。如果构建工具反复提示“plugin tree failed to load”这类网络错误多半是插件下载环节的 HTTPS 证书或代理问题优先检查镜像地址能否在浏览器里直接访问。4.2 版本冲突与依赖树分析插件版本冲突和依赖版本冲突是两类不同问题。依赖冲突靠dependencyManagement对齐插件冲突则需要关注pluginManagement和插件自身版本。排查插件相关问题时mvn help:effective-pom非常有价值。它能输出当前项目经过父 pom、配置文件、profile 合并后的最终 pom插件版本到底是谁定的一目了然。比如某个项目的父 pom 是 Spring Boot 依赖管理它会自动维护一组插件版本但当你 IDE 里显示的构建配置和父 pom 不一致时用 effective-pom 一眼就能看出来。如果构建日志里有 “Could not find artifact xxx in xxx” 这类提示意思是插件 jar 没找到。优先用mvn dependency:get -Dartifact插件坐标 -DremoteRepositories远程仓库地址手动拉取测试确认仓库地址可用后再来看 settings 和 pom 的 pluginRepository 配置。插件下载偶尔也会出现本地.lastUpdated文件污染导致一直拉不到新版本这个问题我遇到过几次解决方法是删除.m2/repository里对应插件目录下的.lastUpdated文件再重新构建。4.3 repackage 之后原始 jar 被覆盖的问题spring-boot-maven-plugin 执行 repackage 时默认会用 fat jar 覆盖target目录下的原始 jar。如果你的项目作为模块被别的项目依赖那内部依赖访问到的就是 fat jar可能引起类路径灾难。前面提到过classifierexec/classifier能解决这里具体说下方案plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId executions execution goals goalrepackage/goal /goals /execution /executions configuration classifierexec/classifier /configuration /plugin这样构建结果会生成mydemo-0.0.1-SNAPSHOT.jar作为普通可依赖 jar以及mydemo-0.0.1-SNAPSHOT-exec.jar作为可执行包。引用这个模块的其他服务依赖普通 jar部署脚本里则启动 exec jar。两者各司其职。不建议手动给所有模块都加这个配置只给被其他模块直接依赖的服务模块加就行。SpringBoot 多模块项目里最终可执行模块不保留原始 jar 也行但要记得给公共模块单独加 maven-jar-plugin 或直接依赖管理。4.4 常见构建报错速查表下面这些报错基本覆盖了插件配置里最常见的坑可以直接对照查询报错现象可能原因处理建议Failed to execute goal ... maven-compiler-plugin ...Java 版本不对、source/target 冲突、release值高于当前 JDK检查 JDK 版本合理设置release或统一 source/targetNo main manifest attribute in ...没配置 spring-boot-maven-plugin 的 repackage goal给该模块加 spring-boot-maven-plugin 并绑定 package 阶段Could not resolve plugin ...插件版本不存在、镜像仓库不可达、本地.lastUpdated缓存污染检查 version、镜像配置清理本地缓存的.lastUpdated文件Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin ...测试运行环境异常、JUnit 与 surefire 版本不匹配统一 surefire 版本检查测试类命名和运行环境The requested profile ... could not be activatedprofile 名不存在或触发条件不满足检查-P参数拼写确认 profile 是否在 activeProfiles 或 settings.xml 中定义ClassNotFoundException ... org.springframework.boot.loader...可执行 jar 的 loader 层损坏、fat jar 被改过重新 repackage避免对BOOT-INF手动改动表格里的第 1 条和第 3 条我在实际项目里见过频率最高。maven-compiler-plugin 报错里很多人明明用的是 JDK 17编译目标想设 11但source8/source和target8/target与项目某些依赖的 API 级别不匹配就会出现奇怪错误。解决方案就是统一改成release11/release。这类问题查起来很费时间配置时一步到位反而最省事。5. 一份可抄的 pom 插件组合与我的使用习惯5.1 精简但完整的 SpringBoot 插件配置段下面这段配置可以直接作为 SpringBoot 项目 pom 里buildplugins的起点模板。我通常会在新建项目时直接复用再根据项目情况微调build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration classifierexec/classifier /configuration executions execution goals goalrepackage/goal /goals /execution /executions /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.11.0/version configuration release11/release parameterstrue/parameters /configuration /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId version3.0.0-M7/version configuration skipTests${skipTests}/skipTests /configuration /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-enforcer-plugin/artifactId version3.3.0/version executions execution goals goalenforce/goal /goals configuration rules requireJavaVersion version[11,12)/version /requireJavaVersion /rules /configuration /execution /executions /plugin /plugins /build这个模板里有几个小细节值得说明。spring-boot-maven-plugin 的classifierexec/classifier不是所有项目都必要但你保留它不会有坏处最多就是部署时要把 exec 后缀写进启动命令。maven-compiler-plugin 没有写version是因为 Spring Boot 父 pom 已经管理了它的版本如果你不是 Spring Boot 父 pom 管理需要手动加一个稳定版本。surefire 的${skipTests}属性默认没有值不传参数时 Maven 会当成false处理所以安全。5.2 我的几个操作习惯和我最后想说的第一每次构建都先mvn clean package -DskipTests做本地验证不要用 IDEA 里那个绿锤子打包。IDEA 的构建动作有时候不会完整走完 Maven 生命周期插件绑定阶段可能被 IDE 内部逻辑跳过最终产物和命令行构建不一致。我在公司里强调过很多次部署包一律以命令行构建结果为准。第二遇到插件版本不确定时用mvn help:describe -Dpluginorg.apache.maven.plugins:maven-compiler-plugin -Dgoalcompile -Ddetail看官方插件详细信息能直接列出这个 goal 支持的所有参数与默认值。比在浏览器里翻文档快多了。第三多模块项目的插件配置尽量下沉到父 pom 的pluginManagement里。父模块只声明版本和管理坐标子模块按需引用。一旦要升级插件版本只改父 pom 一处不会出现“A 模块用了 3.0、B 模块还在 2.7”的割据状态。我接手过一个老项目光 maven-compiler-plugin 就被各子模块写了好几个不同版本升级 Java 版本时苦不堪言。最后分享一个个人体会比较深的小技巧如果线上项目要还原或反编译排查问题一定留一份构建时的pom.xml和git.properties。spring-boot-maven-plugin 生成的 fat jar 里BOOT-INF/classes/META-INF/build-info.properties记录了构建时间和项目版本配合 git-commit-id-plugin 生成的 commit 信息基本能把一个发布物对应到具体的代码提交。这个习惯在追线上事故时帮了我很多次拿 jar 来直接反编译看不出问题拿“构建信息 依赖清单 源码分支”一对照问题一下子就能定位到是主分支还是某条特性分支打出来的。