ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot + Maven 的 pom.xml 插件配置详解:打包、编译、测试一次讲透

SpringBoot + Maven 的 pom.xml 插件配置详解:打包、编译、测试一次讲透 搞 Maven 项目的这些年我见过太多同学把pom.xml当成“依赖清单”以为里面只放dependency就够了看到plugin标签就一脸懵甚至直接全部删掉。直到mvn clean package打出来的 jar 运行报错、单元测试静默跳过、SpringBoot 项目打成普通 jar 导致启动失败才回头来翻插件配置。这篇就把 SpringBoot Maven 项目里pom.xml中 plugin 的用法一次说透每个插件干什么、为什么需要、配置怎么改、踩过哪些坑。适合刚接触 SpringBoot 构建的新手也给用 Maven 一段时间但一直没系统整理过插件配置的同学做一次查漏补缺。1. 整体设计与思路拆解pom 里插件系统到底在解决什么问题1.1 dependency 与 plugin两套体系别混着看很多人最开始分不清dependency和plugin这太正常了因为两者在pom.xml里都是xxx开头的一坨 XML。但它们的定位完全不同。dependency是编译期、运行期要引用的类库。比如spring-boot-starter-web它把 SpringMVC、内嵌 Tomcat、Jackson 等一堆 jar 拉进依赖树你的代码里import org.springframework.web.bind.annotation.RestController才能编译通过。可以理解成“做饭的食材”。plugin是Maven 构建生命周期里干活的工具。Maven 本身只定义了clean、compile、test、package、install这些阶段的生命周期但它不知道“打成一个 SpringBoot 可执行 fat jar”这种操作该怎么做这活儿得交给spring-boot-maven-plugin去干。可以理解成“厨具和加工工序”。SpringBoot 的自动装配则依赖前者spring-boot-starter系列的 jar 里带着META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件2.7 之前是spring.factoriesSpringApplication 启动时扫描这些文件完成自动装配。这个机制和插件没有直接关系但容易和新手形成认知冲突既然是 SpringBoot 项目为什么还要配 Maven 插件答案很简单——依赖决定“你有哪些能力”插件决定“构建时帮你把这些能力怎么组织好”。两者少一个都会出事。1.2 SpringBoot 项目为什么瘦不了这几类插件放到一个典型的 SpringBoot 项目里pom.xml通常要配四类插件职责各不相同。首先是打包类插件。spring-boot-maven-plugin几乎是必配的它把项目打成一个包含所有依赖和三方 jar 的 fat jar保证java -jar app.jar能直接跑。没有它SpringBoot 项目只会被打成普通 jar能编译能测试但运行时就报“no main manifest attribute”。其次是编译类插件。maven-compiler-plugin用来控制源码级别和编码比如指定source和target为 Java 17否则默认用的还是 Maven 所在 JVM 的版本项目里的新语法可能编译不过或者编译出的 class 字节码版本在老环境跑不了。你在热词里看到的“springboot版本太高”往往不是 SpringBoot 本身的问题而是编译器版本没跟上SpringBoot 3.x 强制要求 Java 17但编译插件没升级报了一堆不兼容错误。第三是测试类插件。maven-surefire-plugin负责跑单元测试。很多人没配过它因为 SpringBoot 父级spring-boot-starter-parent已经默认配好了一个版本。但当你想调整测试报告路径、指定跑哪些测试类、或排查测试“明明写了一个 test 但构建时没跑”的时候就必须显式配它。Maven 默认只认src/test/java下*Test.java、Test*.java、*Tests.java等命名规范的类你写了个DBConnectionCheck.java结尾不带 Test无论写多少Test方法surefire 都不会执行。第四类是辅助类插件比如maven-dependency-plugin分析/拷贝依赖、maven-jar-plugin打普通 jar、maven-assembly-plugin打自定义结构包、build-helper-maven-plugin添加额外源码目录。这类插件不是每个项目必需但在多模块、需要定制发布包、需要把配置文件抽出 jar 外部的场景里非常常见。这四类插件加在一起才构成一条完整的构建链路compile - test - package - install。想清楚每一环干什么后面加配置就顺手很多。2. 核心插件细节解析与实操要点2.1 spring-boot-maven-plugin 的 repackage 原理与配置细节spring-boot-maven-plugin最核心的目标是把项目构建成 SpringBoot 可执行 jar。它内部有个叫repackage的 goal会把 Mavenpackage阶段产出的原始 jar 拿过来重打包把项目自己的 class 文件放进BOOT-INF/classes/依赖的三方 jar 放进BOOT-INF/lib/再生成一个启动用的傻瓜类org.springframework.boot.loader.launch.JarLauncher3.2 之后是org.springframework.boot.loader.launch.JarLauncher老版本结构略有不同。这样java -jar才能找到真正的Main-Class和依赖位置。写配置时最常见的模板长这样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 /plugin如果项目继承了spring-boot-starter-parent这个插件版本号会自动管理连version都不用写。但很多 Maven 项目是自定义父 pom不继承 SpringBoot 父级那就必须显式指定版本比如version3.2.5/version写 3.1.x 或 3.2.x 都行目的是和 SpringBoot 大版本对齐。这里有个容易踩坑的地方mainClass要不要写。如果你的项目里只有一个SpringBootApplication启动类插件会自动发现不配也能重打包。但多模块或启动类不止一个时必须显式指定否则它会报“Unable to find a suitable main class”。配置文件里只写mainClass还不够还得放在configuration里的mainClass节点别放错层级。另一个细节是排除不需要打进去的依赖。比如热部署依赖spring-boot-devtools平时开发时需要它自动重启但线上发布时我们不希望把 devtools 打进去configuration excludes exclude groupIdorg.springframework.boot/groupId artifactIdspring-boot-devtools/artifactId /exclude /excludes /configuration这个配置应写在插件配置里而不是去删 dependency。开发的时候依赖还在repackage 时它被剔掉效果刚好符合预期。2.2 compiler 与 surefire容易被忽略却天天在用的两个插件maven-compiler-plugin的配置几乎是所有项目的“地基”。SpringBoot 3.x 项目要求 Java 17如果本机 Maven 默认跑在 JDK 8 上不显式指定编译参数就会报无效的目标发行版: 17或者一堆语法错误。我推荐至少配置这三项plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.13.0/version configuration source17/source target17/target encodingUTF-8/encoding parameterstrue/parameters /configuration /pluginparameterstrue/parameters是个隐藏好东西。开启之后编译出的 class 文件会保留方法参数名方便在 SpringMVC 的RequestParam、MyBatis 的Param等场景下做反射获取参数名。SpringBoot 官方文档里也要求 IDE 设置-parameters但很多项目是通过编译器插件这一步补上的效果一样。maven-surefire-plugin虽然 SpringBoot 父级已经带了一个版本但当你需要“只跑某些测试”、“跳过测试但保留编译”或 is 单独控制测试报告目录的时候就得显式配plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId version3.2.5/version configuration includes include**/*Test.java/include include**/*Tests.java/include /includes skipTestsfalse/skipTests /configuration /plugin值得多说一句的是skipTests和maven.test.skip的区别。skipTeststrue会跳过测试执行但会先编译测试代码如果连测试代码都不想编译用-Dmaven.test.skiptrue。实际项目里 CI 上常跑mvn clean install -DskipTests加快构建但建议本地至少保留一轮完整测试。surefire 和 test 阶段的执行顺序是很多“测试没跑全”问题的根源这一节篇幅有限但价值很高。2.3 maven-jar、dependency、assembly 等辅助插件怎么选先分清这三个插件maven-jar-plugin打普通 jar默认把编译后的 class 和src/main/resources下的资源打在一起。它是 Maven 默认打包行为的实现者。maven-dependency-plugin能输出依赖树也能把 jar 拷贝到指定目录常用于发布时把可执行 jar 和依赖 jar 分离部署。maven-assembly-plugin可以做“自定义打包”比如打一个包含 bin 脚本、config 目录、lib 目录、README 的发布 zip/tar.gz。如果你只是想“打一个 SpringBoot 可执行 jar 然后别人拷走就能跑”那spring-boot-maven-plugin完全够不需要 assembly。如果你是要做“部署包分发”比如上线时要把配置文件放外面方便运维改端口和数据源那推荐结构是可执行 jar 放lib/配置文件放config/启动脚本放bin/。这种情况下spring-boot-maven-plugin负责打可执行 jarmaven-dependency-plugin负责把依赖拷进lib/再用maven-assembly-plugin拼整体目录结构。maven-dependency-plugin常用的一条命令是mvn dependency:tree它能看全量依赖排查“哪个 jar 的哪个版本被高优先级依赖覆盖”特别好用。用它定位NoClassDefFoundError导致的三方 jar 冲突比打开 IDE 里一堆 Maven 节点看半天快多了。配合optionaltrue/optional使用的时候注意optional 的依赖不会向下游传递dependency:tree里也默认不展开 optional 依赖经常有同学排查半天找不到某个标了 optional 的包原因就在这。这个细节后面单独说。2.4 optional 标签与依赖管理跟 plugin 配套的边界知识热词里出现了“pom optional”这虽然不是 plugin 本身但它和 Maven 插件、依赖解析强相关。optionaltrue/optional写在dependency里表示“我这个项目的依赖不传递给使用我这个 jar 的下游项目”。比如你写了一个公共工具包common-cache内部用 Redis 实现缓存于是引入了spring-boot-starter-data-redis并标记 optional下游项目引入common-cache后不会被动引入 Redis 依赖由业务方自己决定要不要用。这个机制能避免依赖污染。但 optional 有一个反直觉的坑在单模块项目里optional 依赖当前项目自己用完全没问题Maven 会正常加进实际构建过程可一旦这个项目被打包成插件或库被别人依赖optional 的依赖就“消失”了。我在一个老项目里见过有人把spring-boot-starter-parent里的某个组件依赖标成 optional导致下游引这个模块时启动直接ClassNotFoundException排查两小时才发现是 optional 闹的鬼。和 optional 配套的另一个操作是exclusion。optional 控制“主动不传递”exclusion 控制“主动剔除某个特定依赖”。很多时候一个依赖被多方引入版本冲突被迫用 exclusion 剔除其中一个。整理插件配置时我习惯随手在 pom 的依赖区加注释哪个 optional 是为了什么哪个 exclusion 是为了解决哪个冲突。没有注释的 pom三个月后自己都看不懂。3. 实操过程与核心环节实现3.1 一份可直接修改的 SpringBoot 多环境 pom 配置下面给一份我平时项目里常用的核心配置它覆盖了编译、测试、打包和资源过滤。不是让你全盘照抄而是对着它逐行理解后再改名字和版本。properties java.version17/java.version spring-boot.version3.2.5/spring-boot.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies build finalNamedemo-app/finalName plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.13.0/version configuration source${java.version}/source target${java.version}/target encodingUTF-8/encoding parameterstrue/parameters /configuration /plugin plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId version${spring-boot.version}/version configuration mainClasscom.example.demo.DemoApplication/mainClass /configuration executions execution goals goalrepackage/goal /goals /execution /executions /plugin /plugins /build这段配置里值得注意的点有两个。第一我没有显式配maven-surefire-plugin因为spring-boot-starter-parent如果没被继承就需要自己确定 surefire 版本不实际上这里因为我没有继承spring-boot-starter-parent所以 Maven 默认会用超级 pom 里的旧版 surefire它也能跑但对于 JUnit 5 的支持可能不全。想要稳一点就按 2.2 节里的样例把 surefire 3.x 显式加上。比如 JUnit 5 的测试类在旧 surefire 下不会自动被识别几乎每个 SpringBoot 项目初次没有父 pom 时都会遇到“测试没跑”的问题。第二finalName决定了mvn package后 jar 的名字比如demo-app.jar。很多人喜欢在命令后加时间戳或 git commit可以用maven-jar-plugin里的archive配置或者直接用 CI 脚本重命名比在 pom 里硬编码动态参数更省心。3.2 maven 命令行验证插件是否按预期工作配置写完之后不能光靠 IDEA 里点一个绿色按钮就完事。我强烈建议整套配置过一遍命令行因为 CI 环境用的就是命令IDEA 里跑得通不代表终端跑得通。先跑mvn clean package看输出有没有BUILD SUCCESS。接着验证 jar 内容jar tf target/demo-app.jar | head -50如果你看到BOOT-INF/classes/和BOOT-INF/lib/目录说明spring-boot-maven-plugin的 repackage 生效了。如果只看到com/example/demo/...的 class 平铺结构说明打的是普通 jar启动肯定会挂。再跑一下依赖树mvn dependency:tree看是不是有重复 jar 或版本冲突。里面的omitted for conflict with ...意思是冲突时版本被覆盖一般以解决方式为准但如果项目出现了运行时NoSuchMethodError多半是某个依赖被覆盖成了低版本。最后跑mvn clean install把构建产物安装到本地仓库默认~/.m2/repository这样本地其他模块可以引用。这里有个和热词“maven命令行 clean install”相关的实操点clean和install组合时调用的生命周期是 complete即 clean 先清理然后把compile - test - package - install全跑一遍。如果你的 pom 里配了maven-surefire-plugin且skipTests是 false那 install 时单测也会被跑时间较长属正常现象。3.3 本地环境与 IDEA 侧 Maven 配置要点热词里连续出现了“maven安装配置”、“maven配置文件”、“maven配置阿里云仓库”、“idea设置plugin中插件仓库地址”。这些表面上是安装配置类问题实际上和 pom 插件能不能拉下来强相关。Maven 的配置文件在~/.m2/settings.xml用户级和 Maven 安装目录下的conf/settings.xml全局级。用户级优先于全局级。没有.m2目录时Maven 首次执行命令会自动创建一份默认配置。但你不在settings.xml里配置镜像仓库的话Maven 会直连中央仓库repo.maven.apache.org国内下载 100MB 的 SpringBoot 依赖慢到怀疑人生。配置阿里云仓库的片段写这里可以直接放进settings.xmlmirrors mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror /mirrorsmirrorOf写central是只代理中央仓库写*会代理所有仓库包括你自定义的私服私服场景下不建议。插件仓库地址默认也是中央仓库所以镜像配置生效后plugins 也能从阿里云拉取。IDEA 里如果不生效去Settings - Build, Execution, Deployment - Build Tools - Maven里确认 Maven home path 指向你配置过的那个 Maven 目录User settings file指向~/.m2/settings.xml。很多人改了settings.xml没生效一看路径IDEA 用的是它内置的 Maven 和默认配置路径相当于白改。关于“maven配置阿里云仓库”除了镜像还有一个选择是直接给项目 pom 增加repositories和pluginRepositories但这种方式对多人协作不太友好因为每个项目都要改。镜像配置在本地机器上写一次全局生效更适合团队统一。4. 常见问题与排查技巧实录4.1 打包后能生成 jar 却运行报错no main manifest attribute这个报错见过的次数太多了。java -jar xxx.jar时 JVM 要在MANIFEST.MF里找Main-Class找不到就说“no main manifest attribute”。本质原因就是spring-boot-maven-plugin的 repackage 没有生效或者根本没配这个插件。排查步骤可以按这个顺序来解压 jar查看META-INF/MANIFEST.MF看里面有没有Main-Class和Start-Class。Start-Class是 SpringBoot 自己的启动类Main-Class是JarLauncher。如果MANIFEST.MF里只有普通Main-Class: com.example.demo.DemoApplication没有Start-Class说明这个 jar 是maven-jar-plugin打的普通 jar而不是 SpringBoot 插件重打包后的 jar。回 pom 看spring-boot-maven-plugin是否配置了executions而且goal是repackage。注意多模块场景父模块也引入了spring-boot-maven-plugin子模块如果没加同配置子模块打包可能不会重打包。通常把该插件只配置在真正需要打成可执行 jar 的模块里。还有一个隐藏原因如果你在 pom 里显式配置了maven-jar-plugin的archive指定了mainClass会覆盖 SpringBoot 插件生成的结果。SpringBoot fat jar 的启动原理依赖 JarLauncher不要手动指定mainClass否则也会出现“jar 能打开但启动就报错”的情况。4.2 SpringBoot 版本升级后插件版本连带的坑热词里有“springboot版本太高”这个现象在升级到 SpringBoot 3.x 后尤其常见。SpringBoot 3.x 基于 Java 17如果你的maven-compiler-plugin还是 3.5 之类的上古版本编译会直接报错。本身 spring-boot-starter-parent 2.x 时代默认插件版本可能只到 3.8直接升级到 3.x 父 pom 后插件版本会自动跟着变但如果你的项目没继承父 pom就全得自己升级。具体升级时注意这几个连带项maven-compiler-plugin至少 3.10推荐 3.11 以上或 3.13。maven-surefire-plugin至少 3.0否则 JUnit 5 兼容性有问题测试不报错但显示 0 tests。SpringBoot 3.0 的 fat jar 结构从org/springframework/boot/loader改成了org/springframework/boot/loader/launch如果你用MANIFEST.MF里的Main-Class写死旧值也会启动失败。涉及 Java 17 的反射访问原有的--add-opensJVM 参数可能需要在发布脚本里重新加。我建议升级 SpringBoot 版本时先在一个分支上跑全量mvn clean package关注输出里的 deprecated 提示和插件版本插件是否自身升级全绿了再动业务代码。4.3 单元测试跑不起来 / 找不到测试类很多 IDEA 里能手跑的测试命令行走一遍就是 0 tests。不是 IDEA 坏了是 surefire 的默认 includes 模式没覆盖到你的测试类名。默认只认*/Test.java、Test*.java、*Tests.java、*TestCase.java等少数模式。你命名成UserServiceValidate.java里面写了 5 个Test命令行走起来一个也不会跑。解决办法两个要么把类名改成符合规范要么在 surefire 配置里显式加 includes。我推荐前者因为团队里其他成员看到UserServiceValidate也不会第一眼认为它是个测试类而UserServiceTest一目了然。前提是你的maven-surefire-plugin版本要跟 JUnit 5 匹配JUnit Jupiter 的测试类和旧版 surefire 不兼容需要 2.22.0 以上版本才支持。另一个问题是 “测试已经跑了但项目启动时 bean 报错”。很多 SpringBoot 单测加载SpringBootTest整个上下文动作很重。测试失败时优先看控制台是否给出了完整 bean 创建异常的 Caused by别只看一堆BeanCreationException就急着删测试类。一般通过exclude某些 AutoConfiguration 或者加MockBean可以解决这时配置和插件无关但如果你在 surefire 里设置了forkCount0/forkCount之类的参数排除干扰后再排查会更干净。4.4 反编译检查 jar 内容验证插件有没有白配置热词里有“springboot jar反编译成项目”。这里不是让你真的有逆向需求而是说明一种很实用的验证手段打包完成后你可以用工具看一下 jar 里的内容确认插件配置确实生效。方法一命令行解压看结构jar tf target/demo-app.jar方法二用反编译工具 IDA / jd-gui / IDEA FernFlower 看图。IDEA 中直接打开 target 下的 jar展开BOOT-INF/classes能直接看到你项目的 class 文件展开BOOT-INF/lib能确认依赖的 jar 是否齐全打开META-INF/MANIFEST.MF确认Main-Class和Start-Class这是检查 fat jar 最直观的方式。在实际排查过程中我经常通过这个方式确认“某个配置文件到底有没有进 jar”和“某依赖是不是被排除了”。有一个小技巧在BOOT-INF/classes里找到application.yml看内容是不是你预期的 profile 版本。如果是多环境构建此时能直接确认 resource 过滤是否生效。4.5 常见故障排查速查表把上面这些问题整理成一张表贴到团队 wiki 或自己笔记里都方便。现象可能原因快速解决java -jar报 no main manifest attribute缺少 spring-boot-maven-plugin 或 repackage 未执行解压 jar 看 MANIFEST补插件配置编译报无效的目标发行版compiler 插件 source/target 与 JDK 不一致升级 compiler 插件并指定 17单测 0 testssurefire 版本旧或测试类命名不符合规范升级 surefire按*Test.java命名依赖冲突出现 NoSuchMethodError高版本 jar 被低版本覆盖mvn dependency:tree定位并 exclusion本地改 settings.xml 后 Maven 仓库仍没变化IDEA 使用内置 Maven 或默认 settings 路径IDEA 中重新指定 Maven home 和 settings 文件插件报 plugin tree failed to load插件仓库不通或本地缓存损坏清~/.m2/repository对应插件目录重拉SpringBoot 打包后启动类找不到多模块中 mainClass 未指定配置mainClass表格里“plugin tree failed to load”是之前见过的一个怪问题本质是本地仓库的_remote.repositories元数据损坏或者某个插件 jar 下到一半断了。解决方式很简单删掉~/.m2/repository下对应插件的整个目录重新执行 Maven 构建。别上来就把整个.m2删了那等于把所有依赖重新下一遍时间是灾难。5. 写在最后的实操体会整理插件配置这件事没有想象中那么难但也不能只看不练。我个人最深刻的体会是pom 里的 plugin 配置一定要“敢删、敢加、敢验证”。什么都不配也能跑起来但有计划地配好之后构建链路会稳很多。最后分享两个小技巧。第一个多模块项目尽量把公共插件配置放到父 pom 的pluginManagement里子模块只在需要时声明使用这样不会出现每个模块都写一遍插件版本、升级时改到崩溃的情况。第二个经常被忽略的maven-help-plugin值得一配命令行跑mvn help:effective-pom就能看到所有继承和默认插件的真实状态排查插件版本问题比猜快得多。SpringBoot 和 Maven 这套组合用了这么多年本质上就是把规则写清楚、让构建可复现。插件配置这一环值得每一位做 Java 开发的同学花一下午时间好好过一遍。
返回列表