execution与自定义绑定
本章承接入门教程中"生命周期与插件绑定的基础概念"和"pluginManagement",深入讲解 Maven 的 execution 机制——即如何将插件目标(goal)显式绑定到生命周期的指定阶段。理解自定义绑定,是扩展 Maven 构建流程、集成代码检查/文档生成/资源处理等非标准任务的核心能力。
核心机制
入门教程已经讲过:Maven 的生命周期阶段(如 compile、test、package)默认绑定了特定的插件目标。例如 compile 阶段默认执行 maven-compiler-plugin:compile。这种绑定是 Maven 超级 POM 中预定义的,开发者无需配置即可使用。
但在实际项目中,默认绑定远远不够。企业级项目通常需要在构建流程中插入自定义步骤:
- 编译前自动生成代码(如从 OpenAPI 规范生成 Java 接口)
- 打包前检查代码风格(如 Checkstyle)
- 测试后生成覆盖率报告(如 JaCoCo)
- 安装前对 JAR 进行签名
这些需求无法通过默认绑定实现,必须通过 <execution> 标签进行自定义绑定。
<execution> 标签的结构
<execution> 是 <plugin> 的子元素,用于定义插件目标的一次具体执行:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>generate-report</id> <!-- 唯一标识,必填 -->
<phase>package</phase> <!-- 绑定到生命周期的哪个阶段 -->
<goals>
<goal>run</goal> <!-- 执行插件的哪个目标 -->
</goals>
<configuration>
<!-- 本次执行特有的配置 -->
</configuration>
</execution>
</executions>
</plugin>
一个 <plugin> 中可以包含多个 <execution>,每个 execution 可以:
- 绑定到不同的生命周期阶段
- 调用插件的不同目标
- 拥有独立的配置
phase + goal 的组合逻辑
自定义绑定的本质是 phase + goal 的显式配对:
| 元素 | 含义 | 示例 |
|---|---|---|
<phase> | 生命周期中的"时机" | compile、test、package、verify |
<goal> | 插件的"动作" | run、check、report、sign |
当 Maven 执行到 <phase> 指定的生命周期阶段时,会自动调用 <goal> 指定的插件目标。如果 <phase> 省略,则使用插件目标默认声明的阶段(每个插件目标在开发时都会声明一个默认绑定阶段,如 maven-jar-plugin:jar 默认绑定到 package)。
多个 execution 的独立执行
一个插件可以配置多个 execution,每个 execution 独立触发:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<id>prepare-agent</id>
<phase>test-compile</phase>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>generate-report</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
上面的配置中,JaCoCo 插件被触发了两次:
- 在
test-compile阶段执行prepare-agent,为测试进程附加覆盖率探针 - 在
verify阶段执行report,读取探针数据生成 HTML 报告
两个 execution 的 <id> 必须不同,否则 Maven 会将其视为同一个 execution 的后定义覆盖前定义。
生活类比:工厂流水线的自定义工位
想象飞翔科技的一条汽车装配流水线(Maven 生命周期):
- 默认绑定:流水线上已经固定了"喷漆工位"(compile 绑定 compiler 插件)、"质检工位"(test 绑定 surefire 插件)。这些工位是工厂标配,所有车型都走同样的流程。
- 自定义绑定(execution):CTO 大翔要求在"喷漆之后、质检之前"增加一个"贴膜工位"(自定义 execution)。这个工位不是工厂标配,是飞翔科技的特殊需求。你需要在流水线控制系统的"阶段映射表"(pom.xml)中写明:当车辆到达"喷漆完成"阶段(phase)时,启动"贴膜机器人"(goal)。
- 多个 execution:如果还需要在"质检之后"增加一个"打蜡工位",你可以再添加一个 execution,绑定到另一个 phase。两个工位互不干扰,各自在指定的时机启动。
图示
上图展示了自定义绑定在生命周期中的实际位置。默认绑定(蓝色)是 Maven 超级 POM 中预定义的,每个 Maven 项目自动拥有。自定义绑定(绿色)是开发者通过 <execution> 显式插入的,它们像"插件"一样嵌入到生命周期的指定阶段。关键观察:自定义绑定不会替换默认绑定,而是与之共存——在 test-compile 阶段,JaCoCo 的 prepare-agent 与默认的编译动作同时发生;在 verify 阶段,JaCoCo 的 report 在默认绑定之外额外执行。
完整示例
场景
飞翔科技的 feixiang-service 模块需要在构建流程中完成以下任务:
- 编译前:用
maven-antrun-plugin打印构建时间戳(用于审计日志) - 测试阶段:用
jacoco-maven-plugin收集单元测试覆盖率 - 打包后:用
maven-antrun-plugin把构建产物复制到团队的共享目录
CTO 大翔要求这些步骤必须自动化,不能依赖手动执行。
操作前的配置/项目状态
feixiang-service/pom.xml 初始状态(只有默认绑定):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.feixiang</groupId>
<artifactId>feixiang-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>feixiang-service</artifactId>
<packaging>jar</packaging>
</project>
操作步骤
架构师白歌在 feixiang-service/pom.xml 中添加自定义绑定:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.feixiang</groupId>
<artifactId>feixiang-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>feixiang-service</artifactId>
<packaging>jar</packaging>
<build>
<plugins>
<!-- Execution 1: 编译前打印时间戳 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>print-timestamp</id>
<phase>compile</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<echo message="Build started at: ${maven.build.timestamp}"/>
</target>
</configuration>
</execution>
</executions>
</plugin>
<!-- Execution 2: 测试阶段收集覆盖率 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<id>prepare-agent</id>
<phase>test-compile</phase>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>generate-report</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Execution 3: 打包后复制产物 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>copy-artifact</id>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<copy file="${project.build.directory}/${project.build.finalName}.jar"
todir="/shared/builds/feixiang-service/"/>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
后端工程师小崔执行构建:
cd feixiang-service
mvn clean verify
操作结果
构建日志节选:
[INFO] --- maven-compiler-plugin:3.11.0:compile @ feixiang-service ---
[INFO] --- maven-antrun-plugin:3.1.0:run (print-timestamp) @ feixiang-service ---
[INFO] Executing tasks
[INFO] [echo] Build started at: 2024-01-15T09:30:00Z
[INFO] Executed tasks
...
[INFO] --- jacoco-maven-plugin:0.8.11:prepare-agent (prepare-agent) @ feixiang-service ---
[INFO] argLine set to -javaagent:...jacocoagent.jar
...
[INFO] --- maven-surefire-plugin:3.1.2:test @ feixiang-service ---
[INFO] Tests run: 42, Failures: 0, Errors: 0
...
[INFO] --- maven-jar-plugin:3.3.0:jar @ feixiang-service ---
[INFO] Building jar: .../feixiang-service-1.0.0-SNAPSHOT.jar
[INFO] --- maven-antrun-plugin:3.1.0:run (copy-artifact) @ feixiang-service ---
[INFO] Executing tasks
[INFO] [copy] Copying 1 file to /shared/builds/feixiang-service
[INFO] Executed tasks
...
[INFO] --- jacoco-maven-plugin:0.8.11:report (generate-report) @ feixiang-service ---
[INFO] Loading execution data file .../jacoco.exec
[INFO] Writing report to .../site/jacoco/index.html
变化分析:
print-timestampexecution 在compile阶段触发,与maven-compiler-plugin的默认绑定同时发生,日志中可以看到它在编译之后立即执行prepare-agentexecution 在test-compile阶段触发,为测试 JVM 附加 JaCoCo 探针,这是覆盖率收集的前提copy-artifactexecution 在package阶段触发,JAR 打包完成后立即复制到共享目录generate-reportexecution 在verify阶段触发,读取测试阶段生成的jacoco.exec数据,输出 HTML 报告到target/site/jacoco/- 所有自定义步骤都自动嵌入了标准生命周期,小崔只需执行
mvn clean verify,无需记忆任何额外命令
易错点与常见问题
误区一:<execution> 的 <id> 可以省略或重复
错误认知:"execution 的 id 只是注释,写不写无所谓,多个 execution 用同样的 id 也没问题。"
纠正:<id> 是 execution 的唯一标识符,Maven 用它来判断两个 execution 是否是同一个。如果两个 execution 的 id 相同,后定义的配置会完全覆盖前定义的配置,而不是合并执行。在上面的例子中,如果两个 maven-antrun-plugin 的 execution 都叫 default,那么只有最后一个(copy-artifact)会执行,print-timestamp 会被静默覆盖。最佳实践:为每个 execution 赋予语义化的唯一 id,如 generate-code、check-style、deploy-docs。
误区二:自定义绑定会替换默认绑定
错误认知:"我在 compile 阶段绑定了 maven-antrun-plugin,那默认的 maven-compiler-plugin 是不是就不执行了?"
纠正:自定义绑定与默认绑定是叠加关系,不是替换关系。在同一个生命周期阶段,所有绑定的插件目标都会被执行,执行顺序大致按照插件在 pom.xml 中出现的顺序(但 Maven 不保证严格的先后顺序,有依赖关系的插件除外)。在 compile 阶段,maven-compiler-plugin:compile(默认)和 maven-antrun-plugin:run(自定义)都会执行。如果你想禁用默认绑定,需要使用 <plugin> 的 <executions> 中覆盖默认 execution,或借助特定插件的 skip 配置——这是高级话题,不在本章范围。
误区三:<phase> 省略时,execution 不会执行
错误认知:"我没写 <phase>,所以这个 execution 不会绑定到任何阶段,永远不会执行。"
纠正:如果 <phase> 省略,Maven 会使用插件目标自身声明的默认阶段。例如 maven-jar-plugin:jar 的默认阶段是 package,maven-clean-plugin:clean 的默认阶段是 clean。省略 <phase> 不等于"不绑定",而是"使用插件作者推荐的绑定时机"。如果你确实想控制执行时机,应该显式写 <phase>;如果你信任插件的默认设计,可以省略。
小结
<execution> 是 Maven 插件体系的扩展点,它允许开发者将任意插件目标插入到生命周期的任意阶段,实现默认绑定无法覆盖的构建需求。phase + goal 的组合是自定义绑定的核心语法,多个 execution 通过唯一 id 区分,各自独立触发。掌握 execution 机制,意味着你能将代码生成、质量检查、报告生成、产物分发等任务无缝集成到标准构建流程中。
本章与全局的关系:本章讲解了如何"激活"插件在生命周期中的执行时机。下一章"插件目标 goal"将深入讲解 goal 本身的定义、直接调用语法,以及 goal 与 phase 的本质区别——这是理解 Maven 插件执行模型的最后一环。