Maven实战(六)- 开发自定义Maven插件

Maven实战(六)- 开发自定义Maven插件

1.开发Maven插件的一般步骤

为了能让读者对编写Maven插件的方法和过程有一个总体的认识,下面先简要介绍一下编写Maven插件的主要步骤。

  • 创建一个maven-plugin项目: 插件本身也是Maven项目,特殊的地方在于它的packaging必须是maven-plugin,用户可以使用maven-archetype-plugin快速创建一个Maven插件项目。
  • 为插件编写目标: 每个插件都必须包含一个或者多个目标,Maven称之为Mojo(与POJO对应,后者指Plain OldJava Object,这里指MavenOldJavaObject)。编写插件的时候必须提供一个或者多个继承自AbstractMojo的类。
  • 为目标提供配置点: 大部分Maven插件及其目标都是可配置的,因此在编写Mojo的时候需要注意提供可配置的参数。
  • 编写代码实现目标行为: 根据实际的需要实现Mojo。
  • 错误处理及日志: 当Mojo发生异常时,根据情况控制Maven的运行状态,在代码中编写必要的日志以便为用户提供足够的信息。
  • **测试插件:**编写自动化的测试代码测试行为,然后再实际运行插件以验证其行为。

接下来将会依此步骤,构建一个统计文件数量的插件。

本人的archetype插件版本:org.apache.maven.plugins:maven-archetype-plugin:3.2.1

2.快速构建一个Maven插件项目

使用命令mvn archetype:generate,创建一个Maven插件的骨架项目,如下图所示。

在这里插入图片描述

选择3.org.apache.maven.archetypes:maven-archetype-plugin (An archetype which contains a sample Maven plugin.)

在这里插入图片描述

最后输入Maven插件的坐标信息,一个Maven插件项目就创建好了,如下图所示。

在这里插入图片描述

创建好的项目就在当前目录下。

在这里插入图片描述

3.Maven插件的骨架项目

打开上述过程创建的maven-count-files项目,并打开项目的pom.xml,可以看见如下部分内容。

<groupId>com.ahao.plugin</groupId>
<artifactId>maven-count-files</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>maven-plugin</packaging>

<name>maven-count-files Maven Plugin</name>

<!-- FIXME change it to the project's website -->
<url>http://maven.apache.org</url>

<properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
  <dependency>
    <groupId>org.apache.maven</groupId>
    <artifactId>maven-plugin-api</artifactId>
    <version>2.0</version>
  </dependency>
  <dependency>
    <groupId>org.apache.maven.plugin-tools</groupId>
    <artifactId>maven-plugin-annotations</artifactId>
    <version>3.2</version>
    <scope>provided</scope>
  </dependency>
  <dependency>
    <groupId>org.codehaus.plexus</groupId>
    <artifactId>plexus-utils</artifactId>
    <version>3.0.8</version>
  </dependency>
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.8.2</version>
    <scope>test</scope>
  </dependency>
</dependencies>
  • <packaging>maven-plugin</packaging>:它的packaging必须为maven-plugin,这种特殊的打包类型能控制Maven为其在生
    命周期阶段绑定插件处理相关的目标,例如在compile阶段,Maven需要为插件项目构建个特殊插件描述符文件。
  • maven-plugin-api:提供了一组接口和类,用于定义和实现插件的执行逻辑。通过这些接口和类,开发人员可以创建自定义的 Maven 插件,以满足特定项目或需求的要求。
  • maven-plugin-annotations:用于开发 Maven 插件的扩展库,它提供了一组注解,以简化插件开发和配置的过程。
  • plexus-utils:一个开源的 Java 类库,提供了许多常用的工具类和方法,用于简化开发过程中的常见任务。它可以帮助开发人员处理文件、字符串、集合、日期等常见操作。

4.开发插件内容

插件项目创建好之后,下一步是为插件编写目标。使用Archetype生成的插件项目包含了一个名为MyMojo的Java文件,我们将其删除,然后自己创建一个CountMojo。

@Mojo(name = "count", defaultPhase = LifecyclePhase.PROCESS_SOURCES)
public class CountMojo extends AbstractMojo {

    @Parameter(defaultValue = "${project.basedir}")
    private File baseDir;

    @Parameter(defaultValue = "${project.build.sourceDirectory}")
    private File sourceDirectory;

    @Parameter(defaultValue = "${project.build.testSourceDirectory}")
    private File testSourceDirectory;

    @Parameter(defaultValue = "${project.build.resources}")
    private List<Resource> resources;

    @Parameter(defaultValue = "${project.build.testResources}")
    private List<Resource> testResources;

    @Parameter(defaultValue = "${project.build.directory}/classes")
    private File outputDirectory;

    @Parameter(defaultValue = "${project.build.directory}/test-classes")
    private File testOutputDirectory;

    @Parameter(defaultValue = "${project.version}")
    private String version;

    @Parameter()
    private String[] includes;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
      	// 仅打印信息,此处不做统计处理。有关功能读者可自行实现。
        Log log = getLog();
        log.info("----------------------------------------[ maven-count-files ]----------------------------------------------------");
        log.info("项目根路径 = " + baseDir);
        log.info("源码目录 = " + sourceDirectory);
        log.info("测试源码目录 = " + testSourceDirectory);
        log.info("资源目录 = " + resources);
        log.info("测试资源目录 = " + testResources);
        log.info("项目版本号 = " + version);
        log.info("源码编译输出目录 = " + outputDirectory);
        log.info("测试源码编译输出目录 = " + testOutputDirectory);
        log.info("用户配置点 = " + Arrays.toString(includes));
      	// 跑出异常后,会显示BUILD ERROR
      // throw new MojoExecutionException("直接跑出异常");
    }

}

首先,每个插件目标类,或者说Mojo,都必须继承AbstractMojo并实现execute()方法,只有这样Maven才能识别该插件目标,并执行execute()方法中的行为。其次,需要使用org.apache.maven.plugins.annotations.Mojo注解,用来声明该插件的目标名称。任何一个Mojo都必须使用该注解声明明自己的目标名称,有了目标定义之后,我们才能在项目中配置该插件目标,或者在命令行调用之。

创建一个Mojo所必要的工作就是这三项:继承AbstractMojo、实现execute()方法、提供@Mojo注解声明目标。

编写好插件目标类之后,通过mvn clean install命令安装该插件至本地仓库,提供给其他项目使用。

在这里插入图片描述

打开一个本地项目(同一个Maven配置),并在其pom.xml加入如下配置。

<plugins>
    <plugin>
        <groupId>com.ahao.plugin</groupId>
        <artifactId>maven-count-files</artifactId>
        <version>1.0.0-SNAPSHOT</version>
        <configuration>
            <includes>
                <include>java</include>
                <include>yml</include>
                <include>yaml</include>
                <include>properties</include>
            </includes>
        </configuration>
    </plugin>
</plugins>

终端通过mvn com.ahao.plugin:maven-count-files:1.0.0-SNAPSHOT:count命令执行其目标。

在这里插入图片描述

如果觉得命令太长,可以打开settings.xml文件,将该插件的groupId加入到pluginGroup中。

<pluginGroups>
    <!-- pluginGroup
     | Specifies a further group identifier to use for plugin lookup.
    <pluginGroup>com.your.plugins</pluginGroup>
    -->
    <pluginGroup>com.ahao.plugin</pluginGroup>
</pluginGroups>

现在命令行就可以简化为mvn maven-count-files:count

至此,一个简单的Maven插件就开发完成了,下面将详细介绍Maven中相关注解的使用方式。

5.@Mojo注解

@Documented
@Retention( RetentionPolicy.CLASS )
@Target( ElementType.TYPE )
@Inherited
public @interface Mojo
{
    /**
     * goal name (required).
     * @return the goal name
     */
    String name();

    /**
     * default phase to bind your mojo.
     * @return the default phase
     */
    LifecyclePhase defaultPhase() default LifecyclePhase.NONE;

    /**
     * the required dependency resolution scope.
     * @return 
     */
    ResolutionScope requiresDependencyResolution() default ResolutionScope.NONE;

    /**
     * the required dependency collection scope.
     * @return 
     */
    ResolutionScope requiresDependencyCollection() default ResolutionScope.NONE;

    /**
     * your Mojo instantiation strategy. (Only <code>per-lookup</code> and <code>singleton</code> are supported)
     * @return the instantiation strategy
     */
    InstantiationStrategy instantiationStrategy() default InstantiationStrategy.PER_LOOKUP;

    /**
     * The original spelling of the instantiationStrategy attribute.
     * @see #instantiationStrategy()
     * @return the instantiation strategy
     */
    @Deprecated
    InstanciationStrategy instanciationStrategy() default InstanciationStrategy.PER_LOOKUP;

    /**
     * execution strategy: <code>once-per-session</code> or <code>always</code>.
     * @return <code>once-per-session</code> or <code>always</code>
     */
    String executionStrategy() default "once-per-session";

    /**
     * does your mojo requires a project to be executed?
     * @return
     */
    boolean requiresProject() default true;

    /**
     * does your mojo requires a reporting context to be executed?
     * @return
     */
    boolean requiresReports() default false;

    /**
     * if the Mojo uses the Maven project and its child modules.
     * @return
     */
    boolean aggregator() default false;

    /**
     * can this Mojo be invoked directly only?
     * @return
     */
    boolean requiresDirectInvocation() default false;

    /**
     * does this Mojo need to be online to be executed?
     * @return
     */
    boolean requiresOnline() default false;

    boolean inheritByDefault() default true;

    /**
     * own configurator class.
     * @return
     */
    String configurator() default "";

    /**
     * is your mojo thread safe (since Maven 3.x)?
     * @return
     */
    boolean threadSafe() default false;
}

以上是@Mojo的源码内容,下面介绍每个属性:

  • name: 这是唯一必须声明的属性,当用户使用命令行调用插件,或者者在POM中配置插件的时候,都需要使用该目标名称。

  • defaultPhase: 默认将该目标绑定至Default生命周期的某个阶段,这样在配置使用该插件目标的时候就不需要声明phase。

  • requiresDependencyResolution: 表示在运行该Mojo之前必须解析所有指定范围的依赖。例如,maven-surefir-plugin
    插件的test目标中requiresDependencyResolution=test,表示在执行测试之前所有测试范围的依赖必须得到解析。

  • requiresDependencyCollection: 同requiresDependencyResolution。

  • instantiationStrategy: Mojo实例化策略。

  • executionStrategy: 执行策略。

  • requiresProject: 表示该目标是否必须在一个Maven项目中运行,默认为true。大部分插件目标都需要依赖一个项目才能执行,但有一些例外。例如maven-help-plugin的system目标,它用来显示系统属性和环境变量信息,不需要实际项目。

  • requiresReports: 表示是否要求项目报告已经生成,默认值是false。

  • aggregator: 当Mojo在多模块项目上运行时,使用该标注表示该目标只会在顶层模块运行。例如maven-javadoc-plugin的aggregator-jar使用了aggregator=true,它不会为多模块项目的每个模块生成Javadoc,而是在顶层项目生成一个已经聚合的Javadloc文档。

  • requiresDirectInvocation: 当值为true的时候,该目标就只能通过命令行直接调用,如果试图在POM中将其绑定到生命周期阶段,Maven就会报错,默认值为false。

  • requiresOnline: 表示是否要求Maven必须是在线状态,默认值是false。

  • inheritByDefault: 插件目标是否可继承。默认为true

    在 Maven 构建过程中,如果某个插件目标是可继承的,则对子项目的继承行为如下:

    1. 如果子项目中没有定义同名的插件目标,并且父项目中的插件目标是可继承的,则子项目将继承父项目的插件目标。
    2. 如果子项目中定义了同名的插件目标,则子项目中的插件目标将覆盖父项目中的同名插件目标。
  • configurator: 指定配置器。

  • threadSafe: Mojo类是否是线程安全的。默认为false

6.@Parameter注解

可以使用@Parameter将Mojo类的某个字段标注为可配置的参数,即Mojo参数。例如4.开发插件内容中自定义Mojo类,其中includes属性就是一个Mojo参数。

Maven支持种类多样的Mojo参数,包括单值的boolean、int、float、String、Date、File和URL,多值的数组、Collection、Map、Properties等。

6.1.boolean(boolean和Boolean)

@Parameter()
private boolean b;

对应的配置如下。

<b>true</b>

6.2.int(int、long、short、byte及其包装类型)

@Parameter()
private int i;

对应的配置如下。

<i>100</i>

6.3.float(float、double及其包装类型)

@Parameter()
private float f;

对应的配置如下。

<f>100.12</f>

6.4.String(包括StringBuffer、char、Character)

@Parameter()
private String str;

对应的配置如下。

<str>hello</str>

6.5.Date(格式为yyyy-MM-dd HH:mm:ss.S a 或者 yyyy-MM-dd HH:mm:ss a)

@Parameter()
private Date date;

对应的配置如下。

<date>2024-08-04 10:13:00.1 AM</date>

6.6.File

@Parameter()
private File file;

对应的配置如下。

<file>/Users/a/b</file>

6.7.URL

@Parameter()
private URL url;

对应的配置如下。

<url>http://www.bai.com/</url>

6.8.数组

@Parameter()
private String[] includes;

对应的配置如下。

<includes>
    <include>java</include>
    <include>yml</include>
    <include>yaml</include>
    <include>properties</include>
</includes>

6.9.Collection(任何实现Collection接口的类)

@Parameter()
private List includes;

对应的配置如下。

<includes>
    <include>java</include>
    <include>yml</include>
    <include>yaml</include>
</includes>

6.10.Map

@Parameter()
private Map map;

对应的配置如下。

<map>
    <key1>value1</key1>
    <key2>value2</key2>
</map>

6.11.Properties

@Parameter()
private Properties properties;

对应的配置如下。

<properties>
  <property>
    <name>age</name>
    <value>18</value>
  </property>
  <property>
    <name>name</name>
    <value>jack</value>
  </property>
</properties>

以上介绍了所有支持的参数类型。@Parameter还有一些额外的其他属性,用来进一步拓展参数的使用。

6.12.其他属性

  • alias: 别名配置。当Mojo字段名称太长或者可读性不强时,用户就可以为Mojo参数使用别名。

    @Parameter(alias="u")
    private URL url;
    

    对应的配置如下。

    <u>http://www.bai.com/</u>
    
  • property: 用于获取属性。

    @Parameter(property = "example.message")
    private String message;
    

    在上面的例子中,参数 message 被标记为 @Parameter 注解,并设置了 property 属性为 “example.message”。这意味着,该参数可以通过 example.message 属性来进行设置。 在插件配置或命令行中,可以使用 -D 参数来设置该属性的值。

    例如,在命令行中执行如下命令: mvn example:example -Dexample.message="Hello, world!" 上述命令将会执行 Example插件的 example 目标,并将参数 message 的值设置为 “Hello, world!”。

  • defaultValue: 参数的默认值。可以通过${}表达式获取系统属性。

  • required: 表示该Mojo参数是必须的。如果设置required=true,但是用户没有配置该Mojo参数且其没有默认值,Maven就会报错。

  • readonly: 表示该Mojo参数是只读的。如果设置readonly=true,用户就无法对其进行配置。

7.测试Maven插件

编写Maven插件的最后一步是对其进行测试,单元测试较之于一般的Maven项目无异。手动测试Maven插件也是一种做法,读者可以将插件安装到本地仓库后,再找个项目测试该插件。本节要介绍的并非上述两种测试方法,而是如何编写自动化的集成测试代码来验证Maven插件的行为。

这里介绍一款插件maven-invoker-plugin,该插件能够用来在一组项目上执行Maven,并检查每个项目的构建是否成功,最后,它还可以执行BeanShell或者Groovy脚本来验证项目构建的输出。

BeanShell和Groovy都是基于JVM平台的脚本语言,读者可以访问 http://www.beanshell.org/ 和 http://groovy.codehaus.org/ 以了解更多的信息。本章下面的内容会用到少许的Groovy代码,不过这些代码十分简单,很容易理解。

首先在上述maven-count-files插件项目的pom.xml中配置如下插件。

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-invoker-plugin</artifactId>
  <version>1.7</version>
  <configuration>
    <debug>true</debug>
    <projectsDirectory>src/it</projectsDirectory>
    <postBuildHookScript>validate.groovy</postBuildHookScript>
    <goals>
      <goal>install</goal>
    </goals>
  </configuration>
  <executions>
    <execution>
      <id>integration-test</id>
      <goals>
        <goal>install</goal>
        <goal>run</goal>
      </goals>
    </execution>
  </executions>
</plugin>

首先projectsDirectory用来配置测试项目的目录,也就是说在src/it目录下存放要测试的Maven项目源码;其次goals表示在测试项目上要运行的Maven目标,这里的配置就表示maven-invoker-plugin会在src/it目录下的各个Maven项目中运行mvn install命令;最后的postBuildHookScript表示在测试完成后要运行的验证脚本,这里是一个groovy文件。

另外,maven-invoker-plugin的两个目标install和run被绑定到了integration-test生命周期阶段。这里的install目标用来将当前的插件构建并安装到仓库中供测试项目使用,run目标则会执行定义好的mvn命令并运行验证脚本。

然后复制需要测试的Maven项目java_project至src/it目录下,如下图所示。

在这里插入图片描述

在上述Maven项目java_project的pom.xml中使用本次用于测试的插件。

<build>
    <plugins>
        <plugin>
            <groupId>com.ahao.plugin</groupId>
            <artifactId>maven-count-files</artifactId>
            <version>1.0.0-SNAPSHOT</version>
            <executions>
                <execution>
                    <phase>verify</phase>
                    <goals>
                        <goal>count</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <includes>
                    <include>java</include>
                    <include>yml</include>
                    <include>yaml</include>
                    <include>properties</include>
                </includes>
            </configuration>
        </plugin>
    </plugins>
</build>

测试项目准备好了,现在要准备的是与该项目对应的验证脚本文件即validate.groovy,内容如下。

def file = new File(basedir, 'build.log')
def countMain = false
def countTest = false

file.eachLine {
    if (it =~ /Default locale: zh_CN, platform encoding: UTF-8/)
        countMain = true
        countTest = true
}

if (!countMain)
    throw new RuntimeException("incorrect src/main/java count info");
if (!countTest)
    throw new RuntimeException("incorrect src/test/java count info");

这段Groovy代码做的事情很简单。它首先读取app项目目录下的的build.log文件,当maven-invoker-plugin构建测试项目的时候,会把mvn输出保存到到项目下的build.log文件中。因此,可以解析该日志文件来验证测试插件maven-count-files是否输出了正确的代码行信息。

Maven会首先在测试项目java_project上运行mvn install命令,如果运行成功,则再执行validate.groovy脚本。只有脚本运行通过且没有异常,集成测试才算成功。

现在在测试插件项目maven-count-files下运行mvn clean install,就能看到如下的输出。

在这里插入图片描述

至此,所有Maven插件集成测试的步骤都完成了。
上述样例只涉及了maven-invoker-plugin的很少一部分配置点,用户还可以配置:

  • debug(boolean): 是否在构建测试项目的时候开启debug输出。
  • settingsFile(File): 执行集成测试所使用的settings.xml,默认为本机环境settings.xml。
  • localRepositoryPath(File): 执行集成测试所使用的本地仓库,默认就是本机环境仓库。
  • preBuildHookScript(String): 构建测试项目之前运行的BeanSheell或Groovy脚本
  • postBuildHookScript(String): 构建测试项目之后运行的BeanShell或Groovy脚本。

要了解更多的配置点,可以访问maven-invoker-plugin的站点:http://maven.apache.org/plugins/maven-invoker-plugin

注:内容源于《Maven实战》(许晓斌著)

  • 43
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值