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 构建过程中,如果某个插件目标是可继承的,则对子项目的继承行为如下:
- 如果子项目中没有定义同名的插件目标,并且父项目中的插件目标是可继承的,则子项目将继承父项目的插件目标。
- 如果子项目中定义了同名的插件目标,则子项目中的插件目标将覆盖父项目中的同名插件目标。
-
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实战》(许晓斌著)