第三章:Maven高级篇 — 插件开发与多模块工程
目标:理解 Maven 插件体系、多模块聚合、继承、Reactor 构建顺序与质量插件,能够开发一个可运行的自定义 Maven 插件。
目录
- Maven 插件体系
- 常用核心插件
- 自定义插件开发
- 多模块工程设计
- Reactor 机制
- 版本管理与打包策略
- 代码质量集成
- 实战 Demo:maven-demo-plugin
- 专家面试题
1. Maven 插件体系
Maven 本身只定义生命周期和项目模型,真正执行编译、测试、打包、部署的是插件。
mvn package ↓ default lifecycle ↓ compile phase -> maven-compiler-plugin:compile test phase -> maven-surefire-plugin:test package phase -> maven-jar-plugin:jar 或 spring-boot-maven-plugin:repackageGoal 与 Phase 映射
| 概念 | 含义 | 示例 |
|---|---|---|
| Plugin | 插件,一组构建能力 | maven-compiler-plugin |
| Goal | 插件中的一个目标 | compile、test、repackage |
| Phase | 生命周期阶段 | compile、test、package |
| Execution | 把 Goal 绑定到 Phase 的配置 | default-compile |
手动执行 Goal:
mvn dependency:tree mvn help:effective-pom绑定 Goal 到生命周期:
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-source-plugin</artifactId><executions><execution><id>attach-sources</id><phase>verify</phase><goals><goal>jar-no-fork</goal></goals></execution></executions></plugin>2. 常用核心插件
| 插件 | 作用 | 常见配置点 |
|---|---|---|
maven-compiler-plugin | 编译 Java 源码 | release、source、target |
maven-surefire-plugin | 运行单元测试 | includes、并发、跳过测试 |
maven-failsafe-plugin | 运行集成测试 | integration-test、verify |
maven-jar-plugin | 打 JAR | manifest、classifier |
maven-source-plugin | 生成源码包 | 发布 SDK 必备 |
maven-shade-plugin | 打 Fat JAR | relocate、filters |
maven-assembly-plugin | 自定义分发包 | zip/tar、文件布局 |
maven-enforcer-plugin | 规则约束 | Java/Maven 版本、依赖收敛 |
父 POM 建议用pluginManagement统一插件版本:
<pluginManagement><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.13.0</version><configuration><release>17</release></configuration></plugin></plugins></pluginManagement>注意:pluginManagement只管理默认配置,不会自动执行插件。要执行插件,仍需在build.plugins中声明或由 Maven 默认生命周期绑定。
3. 自定义插件开发
Maven 插件本质是一个特殊 JAR,packaging=maven-plugin,里面包含一个或多个 Mojo。
插件 POM
<artifactId>demo-maven-plugin</artifactId><packaging>maven-plugin</packaging><dependencies><dependency><groupId>org.apache.maven</groupId><artifactId>maven-plugin-api</artifactId><version>3.9.8</version><scope>provided</scope></dependency><dependency><groupId>org.apache.maven.plugin-tools</groupId><artifactId>maven-plugin-annotations</artifactId><version>3.12.0</version><scope>provided</scope></dependency></dependencies>Mojo 代码
@Mojo(name="version-check",defaultPhase=LifecyclePhase.VALIDATE,threadSafe=true)publicclassVersionCheckMojoextendsAbstractMojo{@Parameter(property="demo.requiredJavaVersion",defaultValue="17")privateintrequiredJavaVersion;@Parameter(property="java.version",readonly=true)privateStringactualJavaVersion;@Overridepublicvoidexecute()throwsMojoExecutionException{getLog().info("Checking Java version.");}}注解说明
| 注解 | 作用 |
|---|---|
@Mojo | 声明插件目标名称、默认阶段、线程安全 |
@Parameter | 从 POM、命令行、系统属性注入参数 |
defaultPhase | 插件 Goal 默认绑定的生命周期阶段 |
threadSafe | 是否支持 Maven 并行构建 |
4. 多模块工程设计
Maven 多模块有两个容易混淆的概念:聚合和继承。
聚合 Aggregator
父工程通过<modules>声明子模块:
<modules><module>maven-demo-bom</module><module>maven-demo-api</module><module>maven-demo-core</module><module>maven-demo-plugin</module><module>maven-demo-web</module></modules>作用:在父目录执行一次mvn package,Maven 会构建所有模块。
继承 Inheritance
子模块通过<parent>继承父 POM:
<parent><groupId>com.example.maven.demo</groupId><artifactId>maven-demo</artifactId><version>1.0.0-SNAPSHOT</version></parent>作用:继承版本、插件管理、属性、Profile 等。
聚合和继承可以分离
企业项目里常见三种形态:
| 形态 | 用法 |
|---|---|
| 同一个父 POM 同时聚合和继承 | 中小项目最常见 |
| 只继承不聚合 | 多仓库项目共享公司 Parent |
| 只聚合不继承 | 临时批量构建多个独立模块 |
5. Reactor 机制
Reactor 是 Maven 多模块构建调度器。它会根据模块依赖关系计算构建顺序,而不只是按<modules>的文本顺序。
本 Demo 依赖关系:
maven-demo-api ↓ maven-demo-core ↓ maven-demo-web maven-demo-plugin 独立构建,用于演示插件开发 maven-demo-bom 是版本清单模块常用局部构建命令:
# 只构建 web 以及它依赖的模块mvn-plmaven-demo-web-ampackage# 从 core 继续构建下游模块mvn-plmaven-demo-core-amdpackage# 跳过测试mvn-plmaven-demo-web-ampackage-DskipTests# 并行构建mvn-T1C clean package参数说明:
| 参数 | 含义 |
|---|---|
-pl/--projects | 指定构建模块 |
-am/--also-make | 同时构建该模块依赖的上游模块 |
-amd/--also-make-dependents | 同时构建依赖该模块的下游模块 |
-rf/--resume-from | 从失败模块继续构建 |
6. 版本管理与打包策略
versions-maven-plugin
查看依赖可升级版本:
mvn versions:display-dependency-updates mvn versions:display-plugin-updates批量修改项目版本:
mvn versions:set-DnewVersion=1.1.0-SNAPSHOT mvn versions:commitFat JAR
Fat JAR 把应用和依赖打进一个 JAR,适合命令行工具或非 Spring Boot 应用。常用maven-shade-plugin:
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>3.5.3</version></plugin>Spring Boot 应用通常使用spring-boot-maven-plugin:repackage生成可执行 JAR,不需要自己用 Shade 打所有依赖。
WAR
WAR 适合部署到外部 Servlet 容器。现代 Spring Boot 项目更多采用可执行 JAR 和容器镜像。
7. 代码质量集成
企业 Maven 构建不应只停留在“能打包”,还要做质量门禁。
| 工具 | 作用 | 推荐阶段 |
|---|---|---|
| Checkstyle | 代码风格 | validate |
| PMD | 静态代码规则 | verify |
| SpotBugs | 字节码缺陷扫描 | verify |
| JaCoCo | 覆盖率 | test/verify |
| OWASP Dependency Check | 依赖漏洞 | CI 定时或发布前 |
| Enforcer | JDK/Maven/依赖规则 | validate |
本 Demo 父 POM 已配置 Enforcer,强制 Java 17+ 和 Maven 3.9+。
8. 实战 Demo:maven-demo-plugin
目标
开发一个自定义 Maven 插件version-check,用于检查当前 Java 版本是否满足要求。
构建插件
cdmaven-demo mvn-plmaven-demo-plugin cleaninstall执行插件
mvn com.example.maven.demo:demo-maven-plugin:1.0.0-SNAPSHOT:version-check-Ddemo.requiredJavaVersion=17预期输出:
[INFO] Checking Java version. required=17, actual=22.0.1 [INFO] BUILD SUCCESS如果要求 Java 99:
mvn com.example.maven.demo:demo-maven-plugin:1.0.0-SNAPSHOT:version-check-Ddemo.requiredJavaVersion=99预期失败:
Java 99+ is required, but current version is 22.0.1插件开发关键点
- 插件模块必须使用
packaging=maven-plugin。 maven-plugin-plugin:descriptor会生成插件描述符。- 插件参数通过
@Parameter(property = "...")支持命令行传入。 - 插件逻辑应尽量拆成普通 Java 类,方便单元测试。
9. 专家面试题
Q1:Maven 插件和生命周期是什么关系?
答:生命周期定义阶段顺序,插件提供具体执行能力。Phase 本身不做事,必须由一个或多个插件 Goal 绑定后才有行为。例如compile阶段通常绑定maven-compiler-plugin:compile。用户既可以执行生命周期阶段,也可以直接执行某个插件 Goal。
Q2:pluginManagement为什么配置了插件但不执行?
答:pluginManagement的设计目标是统一插件版本和默认配置,类似dependencyManagement。它不会把插件自动加入构建执行列表。真正执行插件需要放到build.plugins中,或由 Maven 默认生命周期根据 packaging 自动绑定。
Q3:Reactor 为什么能按依赖关系构建,而不是完全按 modules 顺序?
答:Maven 在多模块构建时会收集所有 Reactor 项目,读取模块间依赖关系、插件依赖和构建扩展,然后进行拓扑排序。这样即使web写在前面,只要它依赖core,Maven 也会先构建core。不过模块声明顺序仍会影响没有依赖关系的模块顺序。
Q4:自定义 Maven 插件开发时,为什么插件 API 依赖通常是 provided?
答:插件运行在 Maven 自己的插件容器中,Maven 运行时已经提供maven-plugin-api。如果把它打进插件产物,可能导致类加载冲突和版本不一致。插件开发应只把自身真正需要的第三方运行库打包进去。
10. 高级篇扩展核查:插件、多模块与构建工程化
10.1 插件前缀解析机制
当执行:
mvn dependency:treeMaven 并不是直接知道dependency是什么插件,而是根据插件组和元数据解析:
dependency -> maven-dependency-plugin -> org.apache.maven.plugins:maven-dependency-plugin自定义插件在未发布插件前缀元数据前,推荐使用完整坐标:
mvn com.example.maven.demo:demo-maven-plugin:1.0.0-SNAPSHOT:version-check这样最稳定,也最适合文档和 CI。
10.2 插件描述符
Maven 插件必须包含插件描述符:
META-INF/maven/plugin.xml它记录:
- 插件有哪些 Goal。
- Goal 对应哪个 Mojo 类。
- 参数名称、类型、默认值。
- 是否线程安全。
- 默认生命周期阶段。
本 Demo 中由maven-plugin-plugin:descriptor生成。
验证:
cdmaven-demo mvn-plmaven-demo-plugin package jar tf maven-demo-plugin/target/demo-maven-plugin-1.0.0-SNAPSHOT.jar|grepplugin.xml10.3 聚合 Mojo 与普通 Mojo
普通 Mojo 会对 Reactor 中每个模块执行一次。聚合 Mojo 通常只在根项目执行一次,用于生成聚合报告、统一检查或发布。
| 类型 | 执行范围 | 例子 |
|---|---|---|
| 普通 Mojo | 每个模块 | 当前version-check |
| 聚合 Mojo | 根项目一次 | 聚合依赖报告、聚合覆盖率 |
如果插件会扫描整个 Reactor,应该谨慎设计为聚合 Mojo,避免每个模块重复执行。
10.4 插件参数设计原则
好的插件参数应满足:
| 原则 | 示例 |
|---|---|
| 有明确默认值 | defaultValue = "17" |
| 支持命令行覆盖 | property = "demo.requiredJavaVersion" |
| 不把只读系统属性暴露为可配置项 | ${java.version}只读注入 |
| 错误信息可行动 | 告诉用户当前值和期望值 |
| 线程安全 | 不写共享可变状态 |
当前 Demo 的VersionCheckMojo体现了这些点。
10.5 Reactor 构建故障恢复
大型多模块项目构建失败后,不必每次从头开始。
mvn clean package# 假设 maven-demo-web 失败mvn-rf:maven-demo-web package如果失败模块依赖的上游模块也修改了,应使用:
mvn-plmaven-demo-web-ampackage参数经验:
| 场景 | 命令 |
|---|---|
| 只测某模块 | mvn -pl module test |
| 模块依赖上游也要构建 | mvn -pl module -am test |
| 改了底层模块,要构建下游 | mvn -pl module -amd test |
| 从失败模块继续 | mvn -rf :module package |
10.6 多模块边界设计
模块不是越多越好。拆模块要有明确边界。
| 拆分理由 | 是否推荐 |
|---|---|
| API 与实现分离 | 推荐 |
| 不同部署单元 | 推荐 |
| 不同发布节奏 | 推荐 |
| 只是按包名机械拆分 | 不推荐 |
| 为了看起来复杂 | 不推荐 |
本 Demo:
api -> 对外契约 core -> 业务逻辑 cli -> 命令行入口和 Shade 打包 web -> HTTP 入口和 Spring Boot 打包 plugin -> 构建扩展 bom -> 版本清单10.7 Shade 打包实战
新增maven-demo-cli演示 Fat JAR。
构建:
cdmaven-demo mvn-plmaven-demo-cli-ampackage运行:
java-jarmaven-demo-cli/target/maven-demo-cli-1.0.0-SNAPSHOT.jar Maven预期:
Hello, Maven environment=local version=1.0.0-SNAPSHOTShade 常见用途:
- 命令行工具。
- Spark/Flink 作业。
- 独立运行的批处理任务。
- 需要 relocate 避免依赖冲突的 SDK。
Spring Boot Web 应用通常不需要 Shade,因为spring-boot-maven-plugin已经会生成可执行 JAR。
10.8 relocate 解决依赖冲突
当你开发 SDK,内部依赖了某个容易冲突的库,可以用 Shade relocate:
<relocations><relocation><pattern>com.google.common</pattern><shadedPattern>com.example.shaded.guava</shadedPattern></relocation></relocations>风险:
- 反射引用可能失效。
- SPI 文件需要合并。
- 日志和配置文件可能需要 transformer。
所以 relocate 不是常规项目的默认选择,而是 SDK 或平台组件的冲突隔离手段。
10.9 质量插件实践
本 Demo 增加了qualityProfile:
cdmaven-demo mvn-Pqualityverify质量门禁建议分层:
| 层级 | 工具 | 目标 |
|---|---|---|
| 基础 | Checkstyle | 风格和低级错误 |
| 缺陷 | SpotBugs | 字节码缺陷 |
| 规范 | PMD | 代码规则 |
| 测试 | Surefire/Failsafe | 单元和集成测试 |
| 覆盖率 | JaCoCo | 分支和行覆盖率 |
| 安全 | OWASP/CycloneDX | 漏洞和 SBOM |
10.10 高级篇新增面试题
Q5:为什么 Maven 插件 artifactId 不建议命名为maven-xxx-plugin?
答:maven-xxx-plugin命名形式保留给 Apache Maven 官方插件。第三方插件推荐使用xxx-maven-plugin或业务语义更明确的名称。本 Demo 使用模块目录maven-demo-plugin,但插件 artifactId 使用demo-maven-plugin,避免官方保留命名警告。
Q6:Shade 和 Spring Boot repackage 的区别是什么?
答:Shade 会把依赖 class 合并进一个 JAR,必要时还能 relocate 包名;Spring Boot repackage 会把依赖以嵌套 JAR 的形式放进BOOT-INF/lib,由 Spring Boot Loader 加载。普通命令行工具适合 Shade,Spring Boot 应用适合 repackage。
Q7:多模块项目如何避免模块之间循环依赖?
答:先定义清晰方向:API 被 core 依赖,core 被 web/cli 依赖,入口模块不能反向被 core 依赖。出现循环依赖通常说明边界错误,应抽取公共契约或公共工具模块,而不是用插件或 scope 绕过去。
