乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 多模块项目深入

    • Reactor构建顺序
    • 继承与聚合组合实践
    • 模块间依赖
  • 第2章 插件体系深入

    • 插件目标goal
    • execution与自定义绑定
    • pluginManagement
  • 第3章 Profile高级应用

    • Profile激活机制
    • Profile与Spring Profile区别
  • 第4章 部署与分发

    • distributionManagement
    • mvn deploy
    • settings.xml认证配置
  • 第5章 CI/CD集成

    • Maven与持续集成
  • 第6章 自定义插件开发

    • 自定义插件开发
  • 第7章 高级依赖管理

    • 快照版本机制
    • 依赖分析工具
  • 第8章 仓库管理深入

    • 仓库组与路由

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 插件被触发了两次:

  1. 在 test-compile 阶段执行 prepare-agent,为测试进程附加覆盖率探针
  2. 在 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 模块需要在构建流程中完成以下任务:

  1. 编译前:用 maven-antrun-plugin 打印构建时间戳(用于审计日志)
  2. 测试阶段:用 jacoco-maven-plugin 收集单元测试覆盖率
  3. 打包后:用 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-timestamp execution 在 compile 阶段触发,与 maven-compiler-plugin 的默认绑定同时发生,日志中可以看到它在编译之后立即执行
  • prepare-agent execution 在 test-compile 阶段触发,为测试 JVM 附加 JaCoCo 探针,这是覆盖率收集的前提
  • copy-artifact execution 在 package 阶段触发,JAR 打包完成后立即复制到共享目录
  • generate-report execution 在 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 插件执行模型的最后一环。

上一页
插件目标goal
下一页
pluginManagement