1. 先搞清楚这个插件到底是干嘛的
用Spring Boot做Java开发,最终交付的无非是两类东西:可执行的Fat Jar,或者依赖外部容器的War包。spring-boot-maven-plugin的核心作用就是把Maven构建产物加工成能直接运行的制品,省去一堆手工操作。
说白了,这个插件替你干了三件事:打可执行包时把依赖和启动类按Spring Boot的规则组装好;启动开发阶段的热启动和调试;生成构建信息供监控、排障使用。很多新手只把它当成一个打包工具,实际它的参数配置里藏着不少东西,用好了能让打包和部署省心很多。
我自己常跟别人说,这个插件不用专门学,但一定要捅破一层窗户纸:它的参数项不多,但参数和参数之间会互相影响,网上大部分教程只让你照抄,不解释为什么。这篇就围绕它的核心参数、配置逻辑和我在项目中踩过的坑,做一个尽量完整的拆解。
2. 版本选择和基础引入:为什么不能随便换插件版本
很多人在pom.xml里引入插件时习惯性不带版本号,让Spring Boot父POM统一管理。这个做法本身没问题,但前提是你用的是spring-boot-starter-parent作为父工程。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent>父POM里已经帮我们把spring-boot-maven-plugin的版本锁定了,直接声明插件即可:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>如果在没有继承父POM的工程里单独用这个插件,就需要自己写版本号,比如2.7.18或3.2.5等。这里给个参考:
| Spring Boot版本 | 推荐插件版本 | 注意事项 |
|---|---|---|
| 1.x | 1.5.x | 旧版参数较少,repackage是核心 |
| 2.x(2.4-2.7) | 2.4.x-2.7.x | 参数体系最稳定,支持分层镜像 |
| 3.x | 3.x | 要求JDK17+,参数基本兼容 |
我遇到过最典型的版本问题:Spring Boot 2.3用了插件2.2的配置,结果某些参数不识别,打包报错或者配置被静默忽略。排查了半天最后发现是版本不匹配。建议做法是:优先继承Spring Boot父POM,别手动去管插件版本。如果项目是多模块、父工程自定义,那就在父POM的dependencyManagement或pluginManagement里统一锁版本。
另外一个容易被忽视的点:插件本身只做“构建期加工”,跟Spring Boot版本强相关的是Spring Boot框架的类加载机制。插件版本和Boot版本不匹配,最常见的症状不是编译报错,而是打包后运行时报ClassNotFoundException或者“no main manifest attribute”,排查起来非常隐蔽。
3. repackage:最核心的打包参数,线把这几个配置吃透
3.1 layout参数:决定你的jar内部长什么样
repackage的layout参数决定最终制品如何组织内部结构,它一共有四个选项:
- JAR:默认值,可执行jar,适合Spring Boot独立应用。内部结构就是BOOT-INF/classes、BOOT-INF/lib这类标准布局。
- WAR:可执行war,适合需要部署到外部Servlet容器但又想能直接java -jar的场景。
- ZIP:对应Dependency插件的方式,比较少见,一般不用。
- NONE:不做fat jar组装,只打个普通的jar包。
举个例子,layout=NONE时,mvn package打出来的就是一个普通jar,依赖不会打进去。这个参数在什么场景下有用?如果你的服务是部署到已有Tomcat里、由外部容器管理类加载,那Fat Jar反而多余,直接NONE布局更干净。
但要注意一个坑:如果你用layout=NONE,Spring Boot的启动类不会自动进MANIFEST里的Main-Class,你打出来的jar是不能java -jar运行的。如果希望既能外部部署又能直接使用,要用WAR布局,而不是NONE。
3.2 includeSystemScope:打包时到底要不要带系统依赖
system scope的依赖在国内很多传统项目里特别常见,比如本地放了个ojdbc.jar或某个SDK,用system scope引入,打包时默认不会被装进Fat Jar。原因很简单:Spring Boot认为system依赖属于JDK或容器提供的环境。
<dependency> <groupId>com.oracle</groupId> <artifactId>ojdbc8</artifactId> <version>12.2.0.1</version> <scope>system</scope> <systemPath>${project.basedir}/lib/ojdbc8.jar</systemPath> </dependency>这种依赖如果你想让它跟随可执行Jar一起分发,就需要开启includeSystemScope,否则运行时会报ClassNotFoundException或NoClassDefFoundError。
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <includeSystemScope>true</includeSystemScope> </configuration> </plugin>不过说实话,我现在的项目里基本不推荐system scope方案了。更好的方式是把它安装到本地Maven仓库或者私有Nexus上,再正常引入。system scope会破坏构建可移植性,换台机器构建就失灵。如果不得不兼容老项目,这才用includeSystemScope。
3.3 requiresUnpack:哪些依赖必须解压后才能运行
requiresUnpack参数用于指定某些依赖打Fat Jar时不解压进BOOT-INF/lib,而是原样拆开,放到临时目录再加载。
哪些场景需要?比如依赖内部包含JNI本地库(.so/.dll),或者需要读取同目录下某个配置文件,再比如有些SDK用到了绝对路径的资源读取。这些依赖如果被压缩进内部jar,运行时资源路径对不上,就会突然报找不到文件。
我常见是配合JNA或者某些加密SDK时遇到这种需求。配置方法如下:
<configuration> <requiresUnpack> <dependency> <groupId>com.example</groupId> <artifactId>native-sdk</artifactId> </dependency> </requiresUnpack> </configuration>配置了之后,Maven在构建时会把这个依赖解压到Jar包的BOOT-INF/lib/目录下,以class目录形式存在,而不是一个嵌套jar。运行时类加载器能直接读取其中的本地库。
坦白讲,这个参数使用频率不高,但一旦遇到需要它的情况,你不知道就会卡很久。建议先把repackage的includeSystemScope、requiresUnpack、layout这三个官方文档里都有,但很容易被跳过的参数放进脑子里,遇到问题才能快速定位。
3.4 classifier:要不要保留原始jar
repackage默认会把原始jar替换为可执行jar。但多模块项目里,如果其他模块依赖了当前模块的jar,希望拿到的是不含Boot启动逻辑的普通jar,那就有问题了。比如A模块依赖B模块,B模块打的包被repackage成了可执行jar,A模块编译时如果引入这个包,启动类、内嵌Tomcat等一堆东西就会被反复打包,导致各种冲突。
解决方案是给B模块的repackage配置classifier:
<configuration> <classifier>exec</classifier> </configuration>这样打出来的原始jar会保留(不带classifier),对外的依赖引用依然指向原始jar,同时多生成一个xxx-exec.jar作为可执行包。这个技巧在我做微服务基础模块拆分时非常实用,强烈推荐。
3.5 mainClass:自动检测失败时的兜底方案
大多数情况下,Spring Boot插件能自动找到带有main方法的类。但如果你的工程里出现多个main方法(常见于多模块或测试代码较多时),插件可能选错或直接报错。这时需要手动指定:
<configuration> <mainClass>com.example.demo.DemoApplication</mainClass> </configuration>mainClass和Spring Boot的Start-Class是两个概念,很多新手容易混淆。mainClass是给Maven插件看的,告诉它哪个类有启动入口;而Start-Class是写进manifest里的,在运行时由Spring Boot的Launcher去加载。插件在repackage过程里会把mainClass的值转成Start-Class。
4. jvmArguments和Run插件:本地调试时怎么调JVM参数
4.1 spring-boot:run 的JVM参数传递方式
spring-boot:run是开发阶段非常好用的goal,但它有一个默认行为:fork。具体来说,如果你直接跑mvn spring-boot:run,插件会启动一个子进程来运行应用,而不是在Maven进程里运行。这个细节很重要,因为它决定了jvmArguments是否生效。
<configuration> <jvmArguments> -Xmx512m -XX:MaxMetaspaceSize=256m -Djava.security.egd=file:/dev/./urandom </jvmArguments> </configuration>也可以直接在命令行传入:
mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Xmx512m -XX:MaxMetaspaceSize=256m"有个很大的坑是:如果该配置指向的参数名拼错了或者fork被设置为false,jvmArguments并不会生效。原因在于fork=false模式下,应用跑在Maven进程内部,JVM早就启动完了,再传JVM参数没有任何意义。
因此我的经验是:如果你要调整JVM参数,务必保持fork默认开启。在IDEA里跑spring-boot:run也应留意配置代理或直接使用Spring Boot的启动类,而非让IDEA自己做classpath执行。
4.2 arguments:给main方法传参并不是jvmArguments
有的同学容易把arguments和jvmArguments搞混。前者对应的是程序启动时传给main方法的args参数,后者是传给JVM的启动参数。举个直观例子:
<configuration> <arguments> <argument>--server.port=8081</argument> <argument>--spring.profiles.active=dev</argument> </arguments> </configuration>这两条会以命令行参数形式进入SpringApplication.run的args里,由Spring Boot解析成配置属性。它们不需要以-D开头,写法是--key=value。
4.3 agent和远程调试参数
本地调试时最常用到的其实是JDWP远程调试参数的注入。因为IDEA启动Spring Boot时一般不用Maven插件,但命令行或CI里想调试,这时可以给jvmArguments配agsent。
<jvmArguments> -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -Xmx1024m </jvmArguments>然后启动:
mvn spring-boot:run再用IDEA的Remote配置连上去,端口5005即可。
调试完毕记得把这行删掉或注释,否则线上repackage的时候可能把调试端口带进去,这是安全大忌。
4.4 environmentVariables:给子进程设置环境变量
spring-boot:run还支持配置环境变量,这在有些需要读取环境变量做配置的场景里非常有用。
<configuration> <environmentVariables> <MY_ENV>hello</MY_ENV> <JAVA_HOME>/usr/local/jdk17</JAVA_HOME> </environmentVariables> </configuration>只对当前启动的Spring Boot进程生效,不会污染Maven进程。
从这里能看出这个插件的定位:它不只是单纯打包,其实还承担了一部分应用生命周期管理。
5. 生产环境构建里真正要关注的参数细节
5.1 build-info和Actuator配合
除了打包和运行,spring-boot-maven-plugin还有一个被低估的goal:build-info。它会在构建时生成一个META-INF/build-info.properties,里面记录构建时间、版本、Java版本、Spring Boot版本等信息。配合Actuator的info端点,可以直接从运行中的应用看到当前部署的是哪个版本。
<executions> <execution> <goals> <goal>build-info</goal> </goals> </execution> </executions>// 也可在配置类中自定义属性,不写代码的话Spring Boot会默认读取然后在application.yml里暴露info端点:
management: endpoints: web: exposure: include: health,info,metrics访问/actuator/info时,能看到build中的version和时间,这个在查线上问题时特别有用。我自己的排查习惯是:先把构建时间、Git commit、版本号看一遍,确认当前跑的是不是我以为的那个版本。
如果要纳入Git提交号,可以再配合git-commit-id-plugin或者spring-boot的git.properties自动生成机制,把git信息也暴露进info端点。
5.2 repackage的executions和goal如何绑定
如果我们只用命令行手工执行mvn package,不配置executions也可以,但默认的package生命周期并不会自动触发repackage。Spring Boot 2.3+之后,如果只声明了插件而不加execution,那么mvn package打出来的只是一个普通jar,不是能被java -jar运行的Fat Jar。
这个问题很多人踩过。正确做法是声明execution,把它绑定到package阶段:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin>这么配置之后,mvn package执行时就会自动把前面maven-jar-plugin生成的jar拿过来重新加工。注意,不是替换成自己的包,而是用原始jar做输入,重新打包出新的可执行jar,同时把原始jar附加为.original。
5.3 多模块项目里避免依赖模块被repackage
在微服务的多模块工程里,常见的父子结构是:
- parent模块:管理依赖和插件版本
- common模块:公共类、工具类,被其他模块依赖
- service-a模块:可执行应用
- service-b模块:可执行应用
如果common模块也继承了父POM的插件配置,并且配了repackage的execution,那它也会被打成可执行Fat Jar,导致依赖关系混乱。更严重的是,service-a打包时把common的可执行jar作为依赖引入,可能会把common里的类重复打包或者把common的Spring Boot启动类整进来。
我的处理方式是:父POM只声明插件和管理版本,不放execution。在需要打包成可执行应用的模块里单独放execution配置,并且给公共模块加上skip:
<configuration> <skip>true</skip> </configuration>这样既保留了模块复用性,也避免了重复加工。
5.4 完整可复制的生产配置参考
结合上面的讨论,我在这里给一套比较完整的生产配置,供你直接参考使用:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <!-- 显式指定启动类,防止多main时选错 --> <mainClass>com.example.demo.DemoApplication</mainClass> <!-- 有system依赖时打开 --> <includeSystemScope>false</includeSystemScope> <!-- 有JNI/SDK需解压时添加 --> <requiresUnpack> <dependency> <groupId>com.example</groupId> <artifactId>native-sdk</artifactId> </dependency> </requiresUnpack> <!-- 多模块时给可执行包加后缀,保留原始jar --> <classifier>exec</classifier> </configuration> <executions> <execution> <goals> <goal>repackage</goal> <goal>build-info</goal> </goals> </execution> </executions> </plugin>这个配置可以覆盖绝大多数常规项目的需求。有特殊要求时再按需调整,但核心原则是:参数能少则少,能明确就明确,不要让插件靠着隐式猜测去工作。
6. 高频问题排查与实战避坑
这里整理一些我遇到的、或者朋友遇到的问题,直接用表格列出:
| 现象 | 原因 | 解决办法 |
|---|---|---|
| java -jar时报No main manifest attribute | repackage没执行或mainClass没找到 | 检查executions是否正确绑定到package阶段,或者显式指定mainClass |
| 运行时报ClassNotFoundException,查看lib里没有本地jar | system scope依赖默认不打进Fat Jar | 开启includeSystemScope,或改用安装到仓库方式 |
| 子模块被重复打成可执行jar | 父POM统一配了repackage且没有skip | 公共模块配skip,可执行模块单独配execution |
| jvmArguments配置了但没生效 | fork设为false,或属性名拼写错误 | 保持fork默认开启,检查spring-boot.run前缀 |
| 打出的jar非常大,怀疑有问题 | Fat Jar本来就是这样的,不用慌 | 用jar tf查看内部结构确认是否包含重复类 |
| 运行时报ZipFileException / jar包损坏 | 打包过程被中断或IDEA和命令行混合构建 | 清理target后重新构建,注意多模块间的依赖顺序 |
| repackage后原jar被覆盖,找不到原始jar | 默认行为,原始jar保存为.original | 配置classifier保留一份原始jar |
| 多模块下模块间依赖到可执行jar | common模块也触发repackage | 公共模块加skip或移除repackage的execution |
6.1 第一次用spring-boot-maven-plugin最容易忽略的事
我刚开始接触这个插件时,以为pom里声明了插件就等于能打包成可执行jar,结果在服务器上java -jar直接报错。后来才明白,插件声明、goal执行、execution绑定是三个层次。简单说:
- 声明插件:只是让Maven知道有这个插件存在
- goal:是插件提供的具体能力
- execution:才决定这些能力在哪个生命周期阶段自动触发
这个认知帮我避免了很多莫名其妙的构建问题。也建议新人在学习时,遇到一个构建插件不要只问“怎么配置”,而是问三个问题:这个插件提供哪些goal?每个goal做什么?怎么让它在我需要的时候自动执行?
6.2 关于JVM参数与线程池配置的延伸思考
Spring Boot的jvmArguments只是JVM层面调参的第一步。实际线上问题往往出在线程池参数上,比如Tomcat线程池、业务线程池是否合理。如果你发现接口在高并发下响应慢、超时多,先别急着加Xmx,先看GC日志和线程池指标。
jvmArguments里常用的一组参数供参考:
-Xms1024m -Xmx1024m -XX:MaxMetaspaceSize=512m -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/app/logs/这里-XX:+HeapDumpOnOutOfMemoryError和HeapDumpPath属于那种“平时无所谓、出事救命”的参数,建议所有生产项目都加上。线程池本身的调优属于代码层面,但JVM参数影响的是整个进程的运行水位,两者需要搭配着看。
6.3 Actuator端点暴露的范围控制
结合spring boot actuator 漏洞、micrometer + spring boot actuator这两个热搜词,我再多说一句:生产环境暴露Actuator端点时,不要图方便用include: "*" 把所有端点全开。常规业务只会用到health、info、metrics、prometheus这几个,其他端点尽量关掉或通过management.endpoints.web.exposure.include精确控制。
management: endpoints: web: exposure: include: health,info,metrics,prometheus毕竟插件只负责把包打出来,运行期的安全防护还得自己上心。Actuator端点暴露越多,被探测的风险就越大,控制好暴露面是最基本的底线。
7. 最后一次构建后的运维小技巧
构建产物出来以后,我习惯在CI脚本里做两个动作:一个是把构建信息写入文件,比如使用build-info;另一个是用unzip -l核对Fat Jar内部的Main-Class和Start-Class,确保是期望的启动类。
unzip -p app.jar META-INF/MANIFEST.MF输出里能看到Main-Class、Start-Class、Spring-Boot-Version这些关键信息。如果Start-Class不是你预期的类,多半是mainClass检测或配置出了问题。
还有一个习惯:把生成的.jar文件同时保留一份原始jar或至少记录build信息,防止后续要复现问题时不知道当初打的到底是什么代码。毕竟排查一次线上问题时,最怕的就是连当前部署的产物都是从哪儿构建来的都不清楚。
8. 我对这个插件的一些个人体会
spring-boot-maven-plugin表面上看只是构建工具链里的一个小环节,但真正用熟了之后,它会帮你解决很多“部署后才发现”的问题。配置不是越全越好,恰到好处才关键。多数项目其实很少用到requiresUnpack,也很少用到classifier,但遇到多模块、老SDK、特殊部署场景时,这几个参数能救急。
我个人在实际操作中的体会是,构建工具的坑基本都不是复杂知识点,而是对参数的理解偏差或使用场景的误判。比如frok默认情况、repackage不会自动执行这些问题,都是团队里反复出现的痛点。建议把常用配置收藏起来,直接在项目里抽成公共模板,避免每个新项目都重复踩一遍坑。
如果还有余力,可以把插件的build-info和前端版本管理连起来,在发布页面上展示每个服务对应的包构建时间、Git提交和启动类。这些信息对于运维排查和数据追溯都很有帮助。[项目]spring-boot-maven-plugin的配置看似琐碎,但每一条都是用来解决真实痛点的。