
写 SpringBoot 项目的人十个里有九个都在 pom.xml 里复制过 plugin 配置但真正搞明白这些插件在干什么的恐怕不到三分之一。我见过太多这样的场景明明只是加了一个打包插件结果mvn package突然报repackage failed明明项目在本地跑得好好的换台电脑就报non-resolvable parent pom还有人把spring-boot-maven-plugin加进去之后发现打出来的 jar 依然没有主类最后只能靠 IDE 里的配置强行启动。这篇文章就是对 SpringBoot Maven 项目 pom 中 plugin 插件用法的一次系统整理。我不打算把所有 Maven 插件都罗列一遍那样没有意义我挑 SpringBoot 项目里真正高频出现、真正影响构建结果的插件逐个拆解它们的作用、关键参数、生命周期绑定方式以及我在实际项目中踩过的坑和完整的排查链路。适合刚把 SpringBoot 跑起来的新手也适合一直靠复制粘贴配置、从没系统捋过插件逻辑的 Java 开发。全文围绕一个核心思路插件不是越多越好而是每一个都要知道它为什么存在。下面直接进入正题。1. 先理解 Maven 插件在 SpringBoot 项目中的定位1.1 插件和依赖不是一回事很多初学者会把dependency和plugin搞混。简单来说dependency 是给项目代码提供原料的也就是 jar 包里的类库编译时、运行时都会被引用而 plugin 是给 Maven 构建流程本身提供动作的它告诉 Maven 在编译、测试、打包、部署这些环节里额外执行什么操作。举个例子spring-boot-maven-plugin的repackagegoal 会在package阶段把普通 jar 改造成可执行的 fat jar这个动作和你的业务代码没有直接关系但它决定了最终产物能不能用java -jar跑起来。再比如maven-compiler-plugin决定源码用哪个 JDK 版本编译、编译参数是什么这些东西都不进入最终 jar 的依赖列表却直接影响构建成败。理解了这个区别你就能明白为什么 pom 文件里会有两片完全不同的 XML 区域dependencies和buildplugins。它们各自服务不同的目的不要混在一起看。1.2 插件 goal 与生命周期阶段的绑定关系Maven 插件通常包含多个 goal一个 goal 就是插件能执行的一个具体任务。比如maven-dependency-plugin有analyze、tree、copy-dependencies等 goal。这些 goal 有两种执行方式一种是在命令行直接调用比如mvn dependency:tree另一种是绑定到生命周期阶段比如mvn package时自动触发。SpringBoot 项目整个构建过程大致经过这样几个阶段compile-test-package-install-deploy。每个阶段背后其实是一个又一个绑定了的插件 goal。你执行mvn clean packageMaven 会先找到 clean 生命周期把 target 目录清空然后进入默认生命周期依次执行编译、测试、打包相关的插件 goal。这条知识是排查所有奇奇怪怪构建问题的基础。比如你发现测试跑了两遍很可能是因为 surefire 插件被显式配置绑定到了test阶段同时还被放在build里默认执行了一次你发现打包时间特别长十有八九是某个插件在package阶段做了太多额外工作比如重复执行了依赖分析。1.3 pluginManagement 和 plugins 的差异SpringBoot 官方 parent pom 里大量使用了pluginManagement它只做版本锁定和公共配置并不会真正执行插件。只有当你把插件写进plugins时插件才会被激活并参与构建。这就是为什么很多人只写一段plugin不带版本号也能正常工作因为 SpringBoot parent 已经在 pluginManagement 里帮你管好了版本。如果你在某个模块里手动指定了另一个版本那就是覆盖了 parent 的默认版本这一步如果没有明确理由很容易引发兼容性问题。我见过一个项目开发为了修复一个 bug把 spring-boot-maven-plugin 从 2.7.x 改成了 3.0.x结果整个项目还跑在 JDK 8 上打包直接失败。所以在动版本号之前务必先搞清楚 parent 里锁的版本是什么。2. SpringBoot 项目里高频出现的插件逐个拆解2.1 spring-boot-maven-plugin打包成可执行 jar 的核心这是 SpringBoot 项目中最重要的插件绝大多数 pom 里都有它。基础配置极简plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin不加任何配置的情况下它会在package阶段执行repackagegoal把maven-jar-plugin生成的普通 jar 改造成 SpringBoot 风格的可执行 jar。这个改造过程的细节我会在下一章专门展开这里先记住它的核心职责是改造产物不是编译代码。它的另外两个常用 goal 也值得了解build-info生成META-INF/build-info.properties文件记录构建时间和版本配合 Spring Boot Actuator 的/actuator/info接口可以直接展示项目构建信息。run支持mvn spring-boot:run直接启动应用不用先打包再 java -jar。开发阶段用这个命令其实比在 IDE 里点启动按钮更接近真实环境。如果你需要排除某些依赖不打进 fat jar可以用exclude配置。典型场景是打包时把内置 Tomcat 排除改用外部容器部署或者排除一些只在编译期用到的库。这个功能在排查 jar 体积异常时非常有用。2.2 maven-compiler-plugin控制编译参数SpringBoot parent 已经预配了 maven-compiler-plugin默认情况下它会读取pom.xml里的java.version或maven.compiler.source/target属性。但如果你需要开启额外的编译参数就必须显式声明它。我经常建议团队在 pom 里把编译插件的关键参数写明确至少包括plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId configuration source11/source target11/target parameterstrue/parameters /configuration /pluginparameters这个参数很容易被忽略但它很重要。Spring 框架在运行时需要反射获取参数名比如RequestParam绑定、ConfigurationProperties的构造函数绑定等。如果编译时没有-parameters参数参数名会被编译成arg0、arg1会导致某些配置无法按预期注入。SpringBoot parent 默认已经开启了这项配置但如果你在子模块里覆盖了 compiler 插件一定要记得把parameters带回来。2.3 maven-surefire-plugin控制测试执行surefire 负责运行单元测试默认绑定在test阶段。常见的需求是跳过测试打包很多人直接写plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId configuration skipTeststrue/skipTests /configuration /plugin注意skipTeststrue/skipTests表示跳过测试执行但依然编译测试代码如果你写testFailureIgnoretrue/testFailureIgnore则是测试失败也不中断构建。这两个参数用途完全不同。还有一种更轻量的做法在命令行加-DskipTests完全不用改 pom 文件适合临时跳过。我个人的建议是别把 skipTests 写死在 pom 里。CI 环境里想跑测试跑不了开发环境跳过了却没人记得改回来。用命令行-DskipTests是一次性的才是更安全的选择。2.4 maven-resources-plugin处理资源文件与变量占位符这个插件负责把src/main/resources下的文件复制到target/classes。它平时很安静但一旦涉及资源过滤就特别容易出现神秘问题。典型场景在application.yml里用version、artifactId这类 Maven 占位符希望在打包时被替换成真实的项目坐标。这需要开启资源过滤plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-resources-plugin/artifactId configuration delimiters delimiter/delimiter /delimiters useDefaultDelimitersfalse/useDefaultDelimiters /configuration /plugin这里有个大坑默认情况下 Maven 资源过滤只处理target/classes里的资源如果你同时在src/main/java目录下放了 XML、SQL 等文件它们默认不会被 resources 插件处理需要在buildresources里额外加一段配置。很多人遇到编译过了但运行时报找不到配置文件的问题根源就在这。2.5 maven-jar-plugin与 repackage 协作的底料maven-jar-plugin 生成最基础的 jar而 spring-boot-maven-plugin 的 repackage 是在这个 jar 的基础上做二次加工。理解这一点对排查问题非常关键——比如你只配置了archivemanifestmainClass在 maven-jar-plugin 里期望它能生成可执行 jar那是对它有过高期待。除非你加了 classpath 依赖列表否则普通 jar 的 Manifest 只有一行Manifest-Version根本不会写 Main-Class。maven-jar-plugin 在 SpringBoot 项目中真正常见的用途是用来指定Main-Class或者给 jar 添加自定义 Manifest 属性。但如果你已经用了 spring-boot-maven-plugin就不用再手动指定repackage 会自动找SpringBootApplication注解所在的主类。两个插件的职责要分清楚。3. repackage 的底层机制可执行 Jar 是怎么变出来的3.1 普通 jar 与 SpringBoot fat jar 的结构差异普通 Java 项目打出来的 jar 只包含项目自身的 class 和资源所有第三方依赖都散落在本地仓库里运行时必须用-cp或类路径装配才能加载。这也是为什么普通 jar 不能直接java -jar会报no main manifest attribute或者ClassNotFoundException。SpringBoot 的 repackage 改变了 jar 的内部结构。打开一个 SpringBoot 可执行 jar你会看到这样的目录BOOT-INF/ classes/ 项目自己的 class 和资源 lib/ 所有依赖 jar META-INF/ MANIFEST.MF org/springframework/boot/loader/ -- 启动器相关类BOOT-INF/lib下躺着全部第三方依赖META-INF/MANIFEST.MF里的Main-Class指向org.springframework.boot.loader.JarLauncher而你的业务主类被记录在Start-Class属性中。启动时JarLauncher先创建一套嵌套 jar 的类加载器再从BOOT-INF/classes和BOOT-INF/lib加载类最后反射调用你的main方法。这个过程就是 repackage 的魔法也是排查所有启动类问题的核心背景。3.2 现场一no main manifest attribute这个报错最常见的出现方式是你执行java -jar xxx.jar得到no main manifest attribute, in xxx.jar。如果确认用了 spring-boot-maven-plugin先别怀疑插件没生效优先检查这两件事mvn package执行完后jar 是不是被 repackage 改写过。执行jar tf target/xxx.jar看有没有BOOT-INF目录。MANIFEST.MF里是不是有Start-Class。如果 jar 里没有BOOT-INF说明这个 jar 是 maven-jar-plugin 直接生成的产物spring-boot-maven-plugin 并没有参与。最典型的原因是把 spring-boot-maven-plugin 写到了pluginManagement里而没有真正放到plugins中或者执行的是mvn install而 install 阶段用的旧 jar 是从本地仓库里拿的。如果 MANIFEST 里缺Start-Class通常是主类没有正常扫描到。可以手动指定plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration mainClasscom.example.demo.DemoApplication/mainClass /configuration /plugin3.3 现场二repackage 与普通 jar 同时需要的 classifier有些项目既需要可执行 jar部署用也需要原始瘦 jar给其他模块依赖用。问题来了repackage 会把原始 jar 覆盖掉导致依赖方拉到的也是 fat jar体积大且可能出现嵌套依赖冲突。解决办法是用classifier给 init 产物一个独立名字plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId executions execution goals goalrepackage/goal /goals configuration classifierexec/classifier /configuration /execution /executions /plugin这样会生成xxx-exec.jar可执行版和xxx.jar原始版两边互不干扰。我在多模块项目里一直用这个方案父模块的 api 包给子模块依赖独立部署模块用 exec 包从未再因依赖引到了 fat jar而踩坑。3.4 现场三构建出的 jar 打不开或者反编译发现结构不对有热搜词提到怎么将 SpringBoot jar 反编译成项目我的经验是先别急着用 GUI 工具先用命令行看结构。jar tf xxx.jar | head -50是最快的诊断手段。如果只想提取某个类看源码unzip -p xxx.jar BOOT-INF/classes/com/example/Foo.class | javap -c -p就能满足大部分需求。需要完整反编译时我习惯用 jd-gui 或 CFR直接把BOOT-INF/classes下的 class 拖进去比反编译整个 fat jar 稳定得多。排查反编译结果异常时核心关注点依然是结构有没有BOOT-INF依赖在不在BOOT-INF/lib如果依赖跑到了 jar 之外那说明 resources 插件没把它们收进来而不是反编译工具有问题。4. 与仓库解析、版本冲突相关的插件痛点排查4.1 non-resolvable parent pom 完整排查链路这是 Maven 项目最经典的报错热搜词里被反复提及。形式通常是Non-resolvable parent POM for com.yntravelsky:wzgl:3.6.3: Could not find artifact ... and parent.relativePath points at wrong local POM我建议按下面的链路一步步排查不要一开始就删本地仓库。第一步看报错信息中提到的 parent 坐标。如果你的 parent 是 SpringBoot 官方 parent那问题几乎都出在仓库网络如果是你自己公司/项目的内部 parent先看relativePath是否指向了正确路径。parent.relativePath默认是../pom.xml如果你把 parent 和子模块放在了不同仓库目录这里就会出问题。第二步检查本地仓库~/.m2/repository里有没有对应目录和_remote.repositories、lastUpdated文件。如果只有*.lastUpdated文件没有 jar说明下载失败了通常是网络或仓库地址问题。第三步检查settings.xml的 mirror 配置。国内项目最常见的做法是使用阿里云仓库镜像mirror idaliyun/id mirrorOf*/mirrorOf urlhttps://maven.aliyun.com/repository/public/url /mirror注意mirrorOf*/mirrorOf表示所有请求都走这个镜像如果你的项目还需要访问某个私有仓库就要改成mirrorOfcentral/mirrorOf或者写入多个 mirror 并调整顺序。第四步确认没有网络代理拦截。公司内网环境经常会遇到 HTTPS 证书问题Maven 会报sun.security.validator.ValidatorException这种情况往往要加-Dmaven.wagon.http.ssl.insecuretrue临时绕过但这不是长久之计最好是在 settings.xml 里配置正确证书。4.2 SpringBoot 版本与插件版本的兼容性边界热搜词里有一条springboot版本太高这个现象在插件层面尤其明显。Spring Boot 2.x 时代的许多插件版本都停留在某个区间直接套到 Spring Boot 3.x 项目里就会出问题。典型的例子spring-boot-maven-plugin必须和 SpringBoot 版本匹配跨大版本使用会报Unsupported class file major version或者 repackage 行为异常。如果项目从 Spring Boot 2.x 升级到 3.xjavax变成jakarta很多依赖插件的内部逻辑也会受影响。比如自动配置相关的 SPI 文件路径变了老的maven-compiler-plugin版本可能无法正确处理新的模块化配置。我的经验是升级 SpringBoot 大版本时把所有 Maven 插件版本列一张对照表逐个核对官方文档的兼容性矩阵。不要只改 parent 版本就完事最稳的是直接用 SpringBoot parent 里 pluginManagement 锁定的版本删除所有手写的插件版本号。4.3 插件之间的互相干扰另一个容易翻车的点是插件执行顺序。Maven 同一个阶段可以绑定多个插件 goal执行顺序取决于声明顺序和execution的phase。比如你同时配置了maven-dependency-plugin的copy-dependencies把依赖拷贝到lib/目录和 spring-boot-maven-plugin 的 repackage如果顺序不对repackage 可能把拷贝出来的lib目录也当成资源处理产生重复依赖。再比如 lombok 配合 maven-compiler-pluginLombok 是以注解处理器形式参与编译的如果你显式指定了maven-compiler-plugin的annotationProcessorPaths但没把 lombok 加进去就会得到一堆找不到 getter/setter的编译错误。SpringBoot parent 默认的编译插件并没有预设 annotationProcessorPaths但一旦有人为了优化编译速度显式配置了这个标签就必须把 Lombok 也列进去annotationProcessorPaths path groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version${lombok.version}/version /path /annotationProcessorPaths这种问题往往报错信息很迷惑不是在 annotation processing 阶段直接报错而是等到运行期反射调用时才暴露。排查思路是先看编译日志里是否出现 processor 字样再确认 annotationProcessorPaths 是否完整。5. 进阶用法容器镜像、依赖安全与版本管理插件5.1 用 jib-maven-plugin 构建 Docker 镜像SpringBoot 项目的部署早已离不开容器传统做法是先mvn package再写 Dockerfile再把 jar 拷进去执行docker build。这套流程繁琐还容易因为基础镜像缓存问题浪费大量时间。Google 的jib-maven-plugin直接跳过 Dockerfile在 Maven 构建阶段就把项目打包成镜像推送到仓库plugin groupIdcom.google.cloud.tools/groupId artifactIdjib-maven-plugin/artifactId version3.4.0/version configuration to imageregistry.example.com/demo/app/image /to /configuration /plugin执行mvn compile jib:build即可。jib 最大的优势是不依赖本地 Docker daemon也不需要在目标机器上安装 Docker适合在 CI 里直接构建镜像。它还做了分层缓存修改业务代码时依赖层不会重新上传构建速度比 Dockerfile 方案快很多。如果你还是习惯传统方案dockerfile-maven-plugin也值得了解但它要求 Docker daemon 可用并且配置文件维护成本高一些。两个方案我都在生产项目里用过团队规模小、CI 环境干净的用 jib 最省心。5.2 versions-maven-plugin 与 maven-dependency-plugin给 pom 做体检versions-maven-plugin是排查依赖版本的最佳帮手。两条高频命令mvn versions:display-dependency-updates mvn versions:display-plugin-updates第一条列出所有依赖有没有新版本第二条列插件的新版本。注意插件的新版本并不代表一定兼容使用前要看 release notes。我一般只关注同一大版本内的更新跨大版本升级会单独建立分支验证。maven-dependency-plugin的analyze目标可以分析出未使用的声明依赖和未声明的使用依赖。前者会让 pom 变得臃肿后者更危险——项目里直接用到了某个库的类但它没有被声明只是通过传递依赖进来的万一上游哪天移除了这个传递依赖项目会突然编译不过。analyze 可以帮你把这类隐患提前暴露出来。5.3 多模块项目里插件的继承与隔离当你把项目拆成多个 module 后plugin 的配置继承关系就要特别注意。父 pom 里buildplugins的插件会被每个子模块继承并执行如果你只想统一版本而不想强制执行就要放到pluginManagement里。典型场景父工程定义了maven-surefire-plugin的严格测试覆盖率配置但某个子模块只是纯 API 定义不需要跑测试。如果写在plugins里这个子模块也会执行白白增加构建时间。正确做法是父 pom 用pluginManagement锁定版本和公共参数子模块按需在plugins里声明并可以用skipTests关闭。另外多模块项目常遇到子模块之间相互依赖的场景B 模块依赖 A 模块B 构建时会先从本地仓库找 A 的 jar。如果 A 只执行了package没有执行install本地仓库里就没有 A 的 jarB 就会报解析失败。这个问题的根源不是插件配置而是 Maven 的 reactor 机制同一词一次构建中如果 A 和 B 都是在 reactor 里的Maven 会直接把 A 的 target/classes 给 B 用但如果单独构建 B就需要 A 已经 deploy 或 install 到仓库里。6. 一套可以直接抄的 SpringBoot 多插件 pom 骨架6.1 基础完整模板结合前面所有内容我给出一个 SpringBoot 3.x 单模块项目的基础插件模板去掉多余配置每段都带注释build plugins !-- 编译参数确保参数名保留开启编译优化 -- plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId configuration source17/source target17/target parameterstrue/parameters /configuration /plugin !-- 测试插件不写死跳过CI 里用 -DskipTests 控制 -- plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId /plugin !-- SpringBoot 打包repackage 生成可执行 jar -- plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId executions execution goals goalrepackage/goal /goals /execution /executions /plugin /plugins /build这个模板已经能覆盖绝大多数 SpringBoot 应用的构建需求编译、测试、打包、启动。如果你需要同时生成原始 jar 和可执行 jar按 3.3 节的classifier配置加上即可需要镜像就再加 jib 插件不需要的不要加。6.2 维护 pom 插件的几个实践建议第一每个插件都要写注释说明用途。pom 文件是团队共同维护的两年后回来看没人记得当初为什么加一个奇怪的插件。哪怕一行!-- 生成 build-info供监控系统展示版本 --都能节省后人大量排查时间。第二少用父 pom 里的plugins多用pluginManagement。前者是强制所有人执行后者是给一个默认值。在团队协作中强制执行的插件很容易成为谁都不敢删的历史包袱而 pluginManagement 给了子模块足够的自由度。第三定期做插件清理。用mvn help:effective-pom查看最终生效的完整 pom你会发现很多以为自己配了的插件其实已经被覆盖也会发现一些不知道为什么就存在的插件。我每年会做一次这样的体检把无效配置删掉构建时间通常能缩短 10% 到 20%。第四遇到不认识的插件先查后试。mvn help:describe -Dpluginorg.springframework.boot:spring-boot-maven-plugin -Ddetail能直接输出这个插件的所有 goal 和参数说明比去网上复制一段来路不明的配置靠谱得多。我在实际项目里最大的体会是Maven 插件的坑大多不是因为插件本身复杂而是因为我们习惯性复制粘贴不去理解它绑定了哪个生命周期、做了什么操作、和上下游插件如何配合。把本文里这些底层逻辑捋一遍之后再遇到 no main manifest attribute、non-resolvable parent pom、repackage 失败这类问题你大概率一眼就能看出问题出在哪个环节而不是像以前一样删 target、清本地仓库、重启电脑三连。希望这篇整理对你也有同样的帮助。