第三章:Maven高级篇 — 插件开发与多模块工程

目标:理解 Maven 插件体系、多模块聚合、继承、Reactor 构建顺序与质量插件,能够开发一个可运行的自定义 Maven 插件。


目录

  1. [Maven 插件体系](#Maven 插件体系)
  2. 常用核心插件
  3. 自定义插件开发
  4. 多模块工程设计
  5. [Reactor 机制](#Reactor 机制)
  6. 版本管理与打包策略
  7. 代码质量集成
  8. [实战 Demo:maven-demo-plugin](#实战 Demo:maven-demo-plugin)
  9. 专家面试题

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 插件中的一个目标 compiletestrepackage
Phase 生命周期阶段 compiletestpackage
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 源码 releasesourcetarget
maven-surefire-plugin 运行单元测试 includes、并发、跳过测试
maven-failsafe-plugin 运行集成测试 integration-testverify
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

插件开发关键点

  1. 插件模块必须使用 packaging=maven-plugin
  2. maven-plugin-plugin:descriptor 会生成插件描述符。
  3. 插件参数通过 @Parameter(property = "...") 支持命令行传入。
  4. 插件逻辑应尽量拆成普通 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

它记录:

  1. 插件有哪些 Goal。
  2. Goal 对应哪个 Mojo 类。
  3. 参数名称、类型、默认值。
  4. 是否线程安全。
  5. 默认生命周期阶段。

本 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 常见用途:

  1. 命令行工具。
  2. Spark/Flink 作业。
  3. 独立运行的批处理任务。
  4. 需要 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>

风险:

  1. 反射引用可能失效。
  2. SPI 文件需要合并。
  3. 日志和配置文件可能需要 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 绕过去。

相关推荐
秋92 小时前
TiDB 数据库全链路实战指南:从下载部署到 Java 高并发调优
java·数据库·tidb
JAVA面经实录9173 小时前
Java开发工程基础完整手册(企业实战完整版)
java·开发语言·git·ci/cd·svn·github·intellij idea
李艺为3 小时前
Fake Device Test作假屏幕分辨率分析
android·java
无敌的黑星星3 小时前
Spring @Transactional 注解全解析
java·数据库·oracle
xiaogg36783 小时前
spring oauth2 单点登录
java·vue.js·spring
c++之路3 小时前
C++ STL
java·开发语言·c++
白晨并不是很能熬夜3 小时前
【RPC】第 4 篇:服务发现 — Zookeeper + 缓存容错
java·后端·程序人生·缓存·zookeeper·rpc·服务发现
EvenBoy3 小时前
IDEA中使用CodeX
java·ide·intellij-idea
日取其半万世不竭3 小时前
Minecraft Java版社区服搭建教程(Windows版)
java·开发语言·windows