目标:理解 Maven 插件体系、多模块聚合、继承、Reactor 构建顺序与质量插件,能够开发一个可运行的自定义 Maven 插件。
目录
- [Maven 插件体系](#Maven 插件体系)
- 常用核心插件
- 自定义插件开发
- 多模块工程设计
- [Reactor 机制](#Reactor 机制)
- 版本管理与打包策略
- 代码质量集成
- [实战 Demo:maven-demo-plugin](#实战 Demo:maven-demo-plugin)
- 专家面试题
1. Maven 插件体系
Maven 本身只定义生命周期和项目模型,真正执行编译、测试、打包、部署的是插件。
text
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:repackage
Goal 与 Phase 映射
| 概念 | 含义 | 示例 |
|---|---|---|
| Plugin | 插件,一组构建能力 | maven-compiler-plugin |
| Goal | 插件中的一个目标 | compile、test、repackage |
| Phase | 生命周期阶段 | compile、test、package |
| Execution | 把 Goal 绑定到 Phase 的配置 | default-compile |
手动执行 Goal:
bash
mvn dependency:tree
mvn help:effective-pom
绑定 Goal 到生命周期:
xml
<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 统一插件版本:
xml
<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
xml
<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 代码
java
@Mojo(name = "version-check", defaultPhase = LifecyclePhase.VALIDATE, threadSafe = true)
public class VersionCheckMojo extends AbstractMojo {
@Parameter(property = "demo.requiredJavaVersion", defaultValue = "17")
private int requiredJavaVersion;
@Parameter(property = "java.version", readonly = true)
private String actualJavaVersion;
@Override
public void execute() throws MojoExecutionException {
getLog().info("Checking Java version.");
}
}
注解说明
| 注解 | 作用 |
|---|---|
@Mojo |
声明插件目标名称、默认阶段、线程安全 |
@Parameter |
从 POM、命令行、系统属性注入参数 |
defaultPhase |
插件 Goal 默认绑定的生命周期阶段 |
threadSafe |
是否支持 Maven 并行构建 |
4. 多模块工程设计
Maven 多模块有两个容易混淆的概念:聚合和继承。
聚合 Aggregator
父工程通过 <modules> 声明子模块:
xml
<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:
xml
<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 依赖关系:
text
maven-demo-api
↓
maven-demo-core
↓
maven-demo-web
maven-demo-plugin 独立构建,用于演示插件开发
maven-demo-bom 是版本清单模块
常用局部构建命令:
bash
# 只构建 web 以及它依赖的模块
mvn -pl maven-demo-web -am package
# 从 core 继续构建下游模块
mvn -pl maven-demo-core -amd package
# 跳过测试
mvn -pl maven-demo-web -am package -DskipTests
# 并行构建
mvn -T 1C clean package
参数说明:
| 参数 | 含义 |
|---|---|
-pl / --projects |
指定构建模块 |
-am / --also-make |
同时构建该模块依赖的上游模块 |
-amd / --also-make-dependents |
同时构建依赖该模块的下游模块 |
-rf / --resume-from |
从失败模块继续构建 |
6. 版本管理与打包策略
versions-maven-plugin
查看依赖可升级版本:
bash
mvn versions:display-dependency-updates
mvn versions:display-plugin-updates
批量修改项目版本:
bash
mvn versions:set -DnewVersion=1.1.0-SNAPSHOT
mvn versions:commit
Fat JAR
Fat JAR 把应用和依赖打进一个 JAR,适合命令行工具或非 Spring Boot 应用。常用 maven-shade-plugin:
xml
<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 版本是否满足要求。
构建插件
bash
cd maven-demo
mvn -pl maven-demo-plugin clean install
执行插件
bash
mvn com.example.maven.demo:demo-maven-plugin:1.0.0-SNAPSHOT:version-check -Ddemo.requiredJavaVersion=17
预期输出:
text
[INFO] Checking Java version. required=17, actual=22.0.1
[INFO] BUILD SUCCESS
如果要求 Java 99:
bash
mvn com.example.maven.demo:demo-maven-plugin:1.0.0-SNAPSHOT:version-check -Ddemo.requiredJavaVersion=99
预期失败:
text
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 插件前缀解析机制
当执行:
bash
mvn dependency:tree
Maven 并不是直接知道 dependency 是什么插件,而是根据插件组和元数据解析:
text
dependency
-> maven-dependency-plugin
-> org.apache.maven.plugins:maven-dependency-plugin
自定义插件在未发布插件前缀元数据前,推荐使用完整坐标:
bash
mvn com.example.maven.demo:demo-maven-plugin:1.0.0-SNAPSHOT:version-check
这样最稳定,也最适合文档和 CI。
10.2 插件描述符
Maven 插件必须包含插件描述符:
text
META-INF/maven/plugin.xml
它记录:
- 插件有哪些 Goal。
- Goal 对应哪个 Mojo 类。
- 参数名称、类型、默认值。
- 是否线程安全。
- 默认生命周期阶段。
本 Demo 中由 maven-plugin-plugin:descriptor 生成。
验证:
bash
cd maven-demo
mvn -pl maven-demo-plugin package
jar tf maven-demo-plugin/target/demo-maven-plugin-1.0.0-SNAPSHOT.jar | grep plugin.xml
10.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 构建故障恢复
大型多模块项目构建失败后,不必每次从头开始。
bash
mvn clean package
# 假设 maven-demo-web 失败
mvn -rf :maven-demo-web package
如果失败模块依赖的上游模块也修改了,应使用:
bash
mvn -pl maven-demo-web -am package
参数经验:
| 场景 | 命令 |
|---|---|
| 只测某模块 | mvn -pl module test |
| 模块依赖上游也要构建 | mvn -pl module -am test |
| 改了底层模块,要构建下游 | mvn -pl module -amd test |
| 从失败模块继续 | mvn -rf :module package |
10.6 多模块边界设计
模块不是越多越好。拆模块要有明确边界。
| 拆分理由 | 是否推荐 |
|---|---|
| API 与实现分离 | 推荐 |
| 不同部署单元 | 推荐 |
| 不同发布节奏 | 推荐 |
| 只是按包名机械拆分 | 不推荐 |
| 为了看起来复杂 | 不推荐 |
本 Demo:
text
api -> 对外契约
core -> 业务逻辑
cli -> 命令行入口和 Shade 打包
web -> HTTP 入口和 Spring Boot 打包
plugin -> 构建扩展
bom -> 版本清单
10.7 Shade 打包实战
新增 maven-demo-cli 演示 Fat JAR。
构建:
bash
cd maven-demo
mvn -pl maven-demo-cli -am package
运行:
bash
java -jar maven-demo-cli/target/maven-demo-cli-1.0.0-SNAPSHOT.jar Maven
预期:
text
Hello, Maven
environment=local
version=1.0.0-SNAPSHOT
Shade 常见用途:
- 命令行工具。
- Spark/Flink 作业。
- 独立运行的批处理任务。
- 需要 relocate 避免依赖冲突的 SDK。
Spring Boot Web 应用通常不需要 Shade,因为 spring-boot-maven-plugin 已经会生成可执行 JAR。
10.8 relocate 解决依赖冲突
当你开发 SDK,内部依赖了某个容易冲突的库,可以用 Shade relocate:
xml
<relocations>
<relocation>
<pattern>com.google.common</pattern>
<shadedPattern>com.example.shaded.guava</shadedPattern>
</relocation>
</relocations>
风险:
- 反射引用可能失效。
- SPI 文件需要合并。
- 日志和配置文件可能需要 transformer。
所以 relocate 不是常规项目的默认选择,而是 SDK 或平台组件的冲突隔离手段。
10.9 质量插件实践
本 Demo 增加了 quality Profile:
bash
cd maven-demo
mvn -Pquality verify
质量门禁建议分层:
| 层级 | 工具 | 目标 |
|---|---|---|
| 基础 | 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 绕过去。