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

    • 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章 仓库管理深入

    • 仓库组与路由

自定义插件开发

本章是 Maven 进阶教程的扩展能力章节。前面章节掌握了 Maven 的标准生命周期、依赖管理和 CI/CD 集成,本章回答的问题是:当标准插件无法满足团队特有需求时,如何自己动手开发 Maven 插件。理解 Mojo 机制和插件项目结构,是打通"使用 Maven"到"驾驭 Maven"的最后一环。


核心机制

Maven 的强大不仅在于它自带的标准插件,更在于它允许任何人编写自定义插件来扩展构建能力。自定义插件的本质是:在 Maven 生命周期的特定阶段,插入你自己的 Java 逻辑。

为什么需要自定义插件?

标准插件(compiler、surefire、jar 等)覆盖了大多数通用场景,但企业级开发中总有一些"特有工序"无法被现成插件满足:

场景标准插件能否解决自定义插件的价值
生成项目特有的 API 文档格式否在 process-sources 阶段自动生成
检查代码中是否包含公司禁止的方法调用否在 compile 阶段做静态规则扫描
将构建产物按公司规范重命名并上传内部系统否在 package 阶段自动执行
生成前端资源版本号并注入 application.yml否在 process-resources 阶段替换占位符

自定义插件让这些"特有工序"成为构建流程的一等公民,而不是靠开发者手动执行某个独立脚本。

Mojo:Maven 插件的基本单元

Mojo(Maven Old Java Object)是 Maven 插件的核心概念。每个 Mojo 是一个 Java 类,对应插件的一个目标(goal)。

开发一个 Mojo 只需要三步:

  1. 继承 AbstractMojo:Maven 提供的基类,封装了日志、参数注入等基础设施
  2. 标注 @Mojo 注解:声明这个 Mojo 的名称、默认绑定生命周期阶段、所需依赖解析范围
  3. 实现 execute() 方法:编写你的构建逻辑

插件项目结构

自定义插件本身也是一个 Maven 项目,但有两个特殊之处:

  • packaging 必须是 maven-plugin:告诉 Maven 这不是普通应用,而是插件
  • 依赖 maven-plugin-api:提供 AbstractMojo、@Mojo 等 API

生活类比:餐厅的"定制酱料包"

想象飞翔科技的连锁餐厅:

  • 标准插件:总部统一配送的番茄酱、沙拉酱(compiler、surefire 等),所有分店直接用。
  • 自定义插件:某地区分店发现当地顾客偏爱麻辣口味,总部允许分店自己调配"麻辣酱包"(自定义插件),但要求这个酱包必须用总部的标准包装规格(Mojo 规范)和配送渠道(Maven 仓库)。调配好的酱包可以上架中央厨房(私服),其他分店也能订购使用。

Maven 的插件体系让"标准化"和"个性化"并存:生命周期阶段是固定的"上菜顺序",但每个阶段用什么"酱料"(插件目标),既可以用总部的,也可以用自己的。


图示

上图展示了自定义插件的完整生命周期:先开发、后安装、再引用。插件项目和业务项目是两个独立的 Maven 项目,插件通过本地仓库(或私服)作为中介被业务项目消费。这种设计让插件可以像普通依赖一样被版本管理和团队共享。


完整示例

场景

飞翔科技的 order-service 项目每次打包后,运维李眉需要手动检查 JAR 包里的 META-INF/MANIFEST.MF 是否包含正确的版本号和构建时间。CTO 大翔要求架构师白歌把这个检查自动化:每次 package 阶段完成后,自动校验清单文件,缺少版本号就构建失败。白歌决定开发一个名为 manifest-check-plugin 的自定义插件,后端小崔负责实现,前端黄俪准备学习后在她的前端构建工具链中推广类似思路。

插件项目结构

小崔创建了插件项目 manifest-check-plugin:

manifest-check-plugin/
├── pom.xml
└── src/
    └── main/
        └── java/
            └── com/
                └── feixiang/
                    └── maven/
                        └── ManifestCheckMojo.java

插件的 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>

    <groupId>com.feixiang.maven</groupId>
    <artifactId>manifest-check-plugin</artifactId>
    <version>1.0.0</version>
    <!-- 关键:packaging 必须是 maven-plugin -->
    <packaging>maven-plugin</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- 插件开发核心 API -->
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-plugin-api</artifactId>
            <version>3.9.6</version>
            <scope>provided</scope>
        </dependency>
        <!-- 注解支持 -->
        <dependency>
            <groupId>org.apache.maven.plugin-tools</groupId>
            <artifactId>maven-plugin-annotations</artifactId>
            <version>3.9.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- 插件描述符生成插件,必须配置 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-plugin-plugin</artifactId>
                <version>3.9.0</version>
            </plugin>
        </plugins>
    </build>
</project>

Mojo 实现代码

package com.feixiang.maven;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.annotations.LifecyclePhase;
import org.apache.maven.plugin.annotations.Mojo;
import org.apache.maven.plugin.annotations.Parameter;
import org.apache.maven.project.MavenProject;

import java.io.File;
import java.io.IOException;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

/**
 * 校验 JAR 包 MANIFEST.MF 中是否包含必需的条目。
 */
@Mojo(
    name = "check",                           // 目标名称:mvn com.feixiang.maven:manifest-check-plugin:1.0.0:check
    defaultPhase = LifecyclePhase.VERIFY,     // 默认绑定到 verify 阶段
    requiresProject = true                    // 必须在 Maven 项目内执行
)
public class ManifestCheckMojo extends AbstractMojo {

    /**
     * 自动注入当前 Maven 项目对象,无需手动创建。
     */
    @Parameter(defaultValue = "${project}", readonly = true)
    private MavenProject project;

    /**
     * 用户可配置:要求清单中必须包含的条目名称。
     */
    @Parameter(property = "manifest.requiredEntries", required = true)
    private String[] requiredEntries;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        File jarFile = new File(project.getBuild().getDirectory(),
                                project.getBuild().getFinalName() + ".jar");

        if (!jarFile.exists()) {
            throw new MojoFailureException("JAR 文件不存在: " + jarFile.getAbsolutePath());
        }

        getLog().info("正在校验清单文件: " + jarFile.getAbsolutePath());

        try (JarFile jar = new JarFile(jarFile)) {
            Manifest manifest = jar.getManifest();
            if (manifest == null) {
                throw new MojoFailureException("JAR 中未找到 MANIFEST.MF");
            }

            for (String entry : requiredEntries) {
                String value = manifest.getMainAttributes().getValue(entry);
                if (value == null || value.trim().isEmpty()) {
                    throw new MojoFailureException(
                        "清单文件缺少必需条目: " + entry
                    );
                }
                getLog().info("  ✓ " + entry + " = " + value);
            }

            getLog().info("清单文件校验通过。");

        } catch (IOException e) {
            throw new MojoExecutionException("读取 JAR 失败", e);
        }
    }
}

插件打包与安装

小崔在插件项目根目录执行:

mvn clean install

这会将插件安装到本地仓库 ~/.m2/repository/com/feixiang/maven/manifest-check-plugin/1.0.0/,供其他项目引用。

业务项目使用插件

白歌在 order-service 的 pom.xml 中引入并配置该插件:

<project>
    ...
    <build>
        <plugins>
            <!-- 自定义清单校验插件 -->
            <plugin>
                <groupId>com.feixiang.maven</groupId>
                <artifactId>manifest-check-plugin</artifactId>
                <version>1.0.0</version>
                <executions>
                    <execution>
                        <!-- 绑定到 package 阶段之后(实际由插件默认的 VERIFY 阶段决定) -->
                        <goals>
                            <goal>check</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <!-- 要求清单中必须包含这两个条目 -->
                    <requiredEntries>
                        <requiredEntry>Implementation-Version</requiredEntry>
                        <requiredEntry>Build-Time</requiredEntry>
                    </requiredEntries>
                </configuration>
            </plugin>

            <!-- 配置 jar 插件,确保清单文件包含所需条目 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Implementation-Version>${project.version}</Implementation-Version>
                            <Build-Time>${maven.build.timestamp}</Build-Time>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <properties>
        <maven.build.timestamp.format>yyyy-MM-dd HH:mm:ss</maven.build.timestamp.format>
    </properties>
</project>

执行效果

当小崔在 order-service 目录执行:

mvn clean package

Maven 会按生命周期顺序执行:

  1. compile —— 编译代码
  2. test —— 运行单元测试
  3. package —— 打包 JAR,maven-jar-plugin 将版本号和构建时间写入清单
  4. verify —— 触发 manifest-check-plugin:check,校验清单条目

如果清单文件缺少 Implementation-Version 或 Build-Time,构建会在 verify 阶段失败,李眉再也不会拿到"信息不全"的 JAR 包。

变化分析:

  • 大翔的质量要求被编码进了构建流程,不再依赖人工检查
  • 黄俪理解了"构建阶段插入自定义逻辑"的思路后,在她的前端项目中用类似思路写了 npm 自定义脚本
  • 插件版本 1.0.0 被安装到本地仓库后,团队其他项目只需在 pom.xml 里加几行配置就能复用

易错点与常见问题

误区一:packaging 写成 jar 或 pom

错误做法:插件项目的 pom.xml 里写 <packaging>jar</packaging>。

问题:Maven 不会为这个项目生成插件描述符(plugin.xml),导致业务项目引用时找不到 Mojo 定义,执行 mvn 命令时报错 Could not find goal。

纠正:自定义插件项目的 packaging 必须是 maven-plugin。这是 Maven 识别"这是一个插件项目"的唯一标志,触发 maven-plugin-plugin 生成必要的元数据。

误区二:execute() 里吞掉异常

错误做法:

@Override
public void execute() {
    try {
        // ... 业务逻辑
    } catch (Exception e) {
        getLog().error("出错了: " + e.getMessage());
        // 没有抛出异常,构建继续
    }
}

问题:插件内部出错但构建显示"成功",导致有问题的产物被安装或部署到生产环境。李眉拿到 JAR 后才发现里面的清单文件是错的。

纠正:自定义插件应该区分两种异常:

  • MojoExecutionException:插件自身执行出错(如 IO 异常、配置错误),表示"插件坏了"
  • MojoFailureException:业务规则检查不通过(如清单缺少条目),表示"被检查的对象不符合要求"

两者都会让 Maven 构建失败,但语义不同,日志信息也更清晰。

误区三:插件版本硬编码在业务项目里

错误做法:每个业务项目的 pom.xml 里直接写死插件版本号:

<plugin>
    <groupId>com.feixiang.maven</groupId>
    <artifactId>manifest-check-plugin</artifactId>
    <version>1.0.0</version>
</plugin>

问题:当小崔升级插件到 1.1.0(比如增加了新的校验规则)时,需要逐个修改十几个业务项目的 pom.xml,容易遗漏。

纠正:在公司父 POM(feixiang-parent)的 <pluginManagement> 中统一管理插件版本:

<!-- feixiang-parent/pom.xml -->
<pluginManagement>
    <plugins>
        <plugin>
            <groupId>com.feixiang.maven</groupId>
            <artifactId>manifest-check-plugin</artifactId>
            <version>1.1.0</version>
        </plugin>
    </plugins>
</pluginManagement>

业务项目只需声明 groupId 和 artifactId,版本由父 POM 统一控制:

<!-- order-service/pom.xml -->
<plugin>
    <groupId>com.feixiang.maven</groupId>
    <artifactId>manifest-check-plugin</artifactId>
</plugin>

小结

自定义 Maven 插件是扩展构建能力的终极手段。开发插件只需掌握三个核心要素:maven-plugin 打包类型、@Mojo 注解声明、以及 execute() 方法实现业务逻辑。插件开发完成后,通过 mvn install 进入本地仓库(或 mvn deploy 进入私服),即可被任何业务项目通过 pom.xml 引用并绑定到生命周期阶段。自定义插件让团队的特有构建需求成为一等公民,摆脱"手动脚本+口头约定"的脆弱协作模式。

本章与全局的关系:本章回答了"如何扩展 Maven 的能力边界"。至此,Maven 进阶教程覆盖了多模块构建、依赖高级管理、CI/CD 集成和自定义插件开发,形成了从"使用标准能力"到"驾驭和扩展平台"的完整进阶路径。