编写Maven插件的一般步骤
1、创建一个maven-plugin项目:插件本身也是Maven项目,特殊的地方在于它的packaging必须是maven-plugin,用户可以使用maven-archetype-plugin快速创建一个Maven插件项目
2、为插件编写目标:每个插件都必须包含一个或者多个目标,Maven称之为Mojo(与POJO对应,后缀指Plain Old Java Object,这里指Maven Old Java Object)。编写插件的时候必须提供一个或者多个继承自AbstractMojo的类
3、为目标提供配置点:大部分Maven插件及其目标都是可配置的,因此在编写Mojo的时候需要注意提供可配置的参数
4、编写代码实现目标行为:根据实际的需要实现Mojo
5、错误处理及日志:当Mojo发生异常时,根据情况控制Maven的运行状态。在代码中编写必要的日志以便为用户提供足够的信息
6、测试插件:编写自动化的测试代码测试行为,然后再实际运行插件以验证其行为
编写一个用于代码行统计的Maven插件
使用该插件,用户可以了解到Maven项目中各个源代码目录下文件的数量,以及它们加起来共有多少代码行。
不过,强烈方队使用代码行来考核程序员,因为大家都知道,代码的数量并不能真正反映一个程序员的价值
代码行统计插件的POM
<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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.juvenxu.mvnbook</groupId> <artifactId>maven-loc-plugin</artifactId> <packaging>maven-plugin</packaging> <version>0.0.1-SNAPSHOT</version> <name>maven-loc-plugin Maven Mojo</name> <url>http://maven.apache.org</url> <properties> <maven.version>3.0</maven.version> </properties> <dependencies> <dependency> <groupId>org.apache.maven</groupId> <artifactId>maven-plugin-api</artifactId> <version>${maven.version}</version> </dependency> </dependencies> </project>
Maven插件项目的POM有两个特殊的地方:
1、它的packaging必须为maven-plugin,这种特殊的打包类型能控制Maven为其在生命周期阶段绑定插件处理相关的目标,例如在compile阶段,Maven需要为插件项目构建一个特殊插件描述符文件
2、从上述代码中可以看到一个artifactId为maven-plugin-api的依赖,该依赖中包含了插件开发所必须的类,例如稍后会看到的AbstractMojo,需要注意的是,并没有使用默认Archetype生成的maven-plugin-api版本,而是升级到了3.0,这样做的目的是与Maven的版本保持一致
CountMojo的主要代码
package com.juvenxu.mvnbook.loc; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.apache.maven.model.Resource; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; /** * Goal which counts lines of code of a project * * @goal count */ public class CountMojo extends AbstractMojo { private static final String[] INCLUDES_DEFAULT = {"java", "xml", "properties"}; /** * @parameter expression="${project.basedir}" * @required * @readonly */ private File basedir; /** * @parameter expression="${project.build.sourceDirectory}" * @required * @readonly */ private File sourceDirectory; /** * @parameter expression="${project.build.testSourceDirectory}" * @required * @readonly */ private File testSourceDirectory; /** * @parameter expression="${project.build.resources}" * @required * @readonly */ private List<Resource> resources; /** * @parameter expression="${project.build.testResources}" * @required * @readonly */ private List<Resource> testResources; /** * The file types which will be included for counting * * @parameter */ private String[] includes; public void execute() throws MojoExecutionException { if (includes == null || includes.length == 0) { includes = INCLUDES_DEFAULT; } try { countDir(sourceDirectory); countDir(testSourceDirectory); for (Resource resource : resources) { countDir(new File(resource.getDirectory())); } for (Resource resource : testResources) { countDir(new File(resource.getDirectory())); } } catch (IOException e) { throw new MojoExecutionException("Unable to count lines of code.", e); } } private void countDir(File dir) throws IOException { if (!dir.exists()) { return; } List<File> collected = new ArrayList<File>(); collectFiles(collected, dir); int lines = 0; for (File sourceFile : collected) { lines += countLine(sourceFile); } String path = dir.getAbsolutePath().substring(basedir.getAbsolutePath().length()); getLog().info(path + ": " + lines + " lines of code in " + collected.size() + " files"); } private void collectFiles(List<File> collected, File file) { if (file.isFile()) { for (String include : includes) { if (file.getName().endsWith("." + include)) { collected.add(file); break; } } } else { for (File sub : file.listFiles()) { collectFiles(collected, sub); } } } private int countLine(File file) throws IOException { BufferedReader reader = new BufferedReader(new FileReader(file)); int line = 0; try { while (reader.ready()) { reader.readLine(); line++; } } finally { reader.close(); } return line; } }
1、每个插件目标类,或者说Mojo,都必须继承AbstractMojo并实现execute()方法,只有这样maven才能识别该插件目标,并执行execute()方法中的行为
2、由于历史原因,上述的CountMojo类使用了Java 1.4风格的标注 (将标注写在注释中),这里要关注的是@goal,任何一个Mojo都必须使用该标注写明自己的目标名称
3、有了目标定义名称之后,我们才能在项目中配置该插件目标,或者在命令行调用之
4、mvn com.juvenxu.mvnbook:maven-loc-plugin:0.0.1-SNAPSHOT:count
5、还包含了basedir、sourceDirectory、testSourceDirectory等字段,它们都使用了@parameter标注,但同时关键字expression表示从系统属性读取这几个字段的值
6、${project.basedir}项目的基础目录,${project.build.sourceDirectory}主代码目录,${project.build.testSourceDirectory}测试代码目录
7、@readonly标注表示不允许用户对其进行配置,因为对于一个项目来说,这几个目录位置都是固定的
了解这些简单的配置点之后,下一步就该实现插件的具体行为了
1、从execute()方法中可以看到,如果用户没有配置includes则就是用默认的统计包含配置
2、然后再分别统计项目主代码目录、测试代码目录、主资源目录,以及测试资源目录
3、这里涉及了一个countDir()方法
简单解释CollectFiles()、countLine()、CountDir()这三个方法
1、collectFiles()方法用来递归地搜集一个目录下所有应当被统计的文件
2、countLine()方法用来统计单个文件的行数
3、countDir()则借助上述两个方法统计某个目录下共有多少文件被统计,以及这些文件共包含了多少代码航
简单解释execute()方法包含的异常处理
1、execute()方法包含了简单的异常处理,代码行统计的时候由于涉及了文件操作,因此可能会抛出IOException
2、当捕获到IOException的时候,使用MojoExecutationException对其简单包装后再抛出
3、Maven执行插件目标的时候如果遇到MojoExecutationException,就会在命令行显示"BULD ERROR" 信息
countDir()方法是最后一行使用了AbstractMojo的getLog()方法
1、该方法返回一个类似于Log4j的日志对象,可以用来将输出日志到Maven命令行
2、这里使用了info级别的日志告诉用户某个路径下有多少文件被通缉,共包含了多少代码行
下一步是为插件提供配置点
1、我们希望该插件默认统计所有的Java、XML,以及properties文件,但是允许用户配置包含哪些类型的文件
2、includes字段就是用来为用户提供该配置点的,它的类型为String数组,并且使用了@parameter参数表示用户可以在使用该插件的时候在POM中配置该字段
配置CountMojo的includes参数
<plugin> <groupId>com.juvenxu.mvnbook</groupId> <artifactId>maven-loc-plugin</artifactId> <version>0.0.1-SNAPSHOT</version> <configuration> <includes> <include>java</include> <include>sql</include> </includes> </configuration> </plugin>
配置CountMojo统计Java和SQL文件,而不是默认的Java、XML和properties
使用mvn clean install命令将该插件项目构建并安装到本地仓库
如果先命令行太长太复杂,可以将该插件的groupId添加到settings.xml中
<settings> <pluginGroups> <pluginGroup>com.juvenxu.mvnbook</pluginGroup> </pluginGroups> </settings>
现在Maven命令行就可以简化成:mvn loc:count
Mojo标注
每个Mojo都必须使用@Goal标注来注明其目标名称,否则Maven将无法识别该目标
Mojo的标注不仅限于@Goal,以下是一些可以用来控制Mojo行为的标注
n @goal <name>,这是唯一必须声明的标注,当用户使用命令行调用插件,或者在POM中配置插件的时候,都需要使用该目标名称
n @phase <phase>,默认将该目标绑定至Default声明周期的某个阶段,这样在配置使用该插件目标的时候就不需要声明phase,例如,maven-surefire-plugin的test目标就带有@phase test标注
n @requiresDependencyResolution <scope>,表示在运行该Mojo之前必须解析所有指定范围的依赖。例如,maven-surefire-plugin的test目标带有@requiresDependencyResolution test标注,表示在执行测试之前,所有测试范围的依赖必须得到解析。这里可用的依赖范围有compile、test和runtime,默认值为runtime
n @requiresProject <true/false>,表示该目标是否必须在一个Maven项目中运行,默认为true。大部分插件目标都需要依赖一个项目才能执行,但有一些例外。例如maven-help-plugin的system目标,它用来显示系统属性和环境变量信息,不需要实际项目,因此使用了@requiresProject false标注。另外maven-archetype-plugin的generate目标也是一个很好的例子
n @requiresDirectInvocation <true/false>,当值为true的时候,该目标就只能通过命令行直接调用,如果试图在POM中将其绑定到生命周期阶段,Maven就会报错,默认值为false。如果你希望编写的插件只能在命令行独立运行,就应当使用该标注
n @requiresOnline <true/false>,表示是否要求Maven必须是在线状态,默认值是false
n @requiresReport <true/false>,表示会否要求项目报告已经生成,默认值是false
n @aggregator,当Mojo在多模块项目上运行时,使用该标注表示该目标只会在顶层模块运行,例如maven-javadoc0plugin的aggregator-jar使用了@aggregator标注,它不会为多模块项目的每个模块生成javadoc,而是在顶层项目生成一个已经聚合的Javadoc文档
n @execute goal = "<goal>",在运行该目标之前先让Maven运行另外一个目标,如果是本插件的目标,则直接使用目标名称,否则使用"prefix:goal"的形式,即注明目标前缀。例如,maven-pmd-plugin是一个使用PMD来分析项目源码的工具,它包含pmd和check等目标,其中pmd用来生成报告,而check用来验证报告。由于check是依赖于pmd生成的内容的,因此可以看到它使用了标注@execute goal = “pmd”
n @execute phase = "<phase>",在运行该目标之前让Maven选运行一个并行的生命周期,到指定的阶段为止。例如maven-dependency-plugin的analyze使用了标注@execute phase=“test-compile”,因此当用户在命令行执行dependency:analyze的时候,Maven会首先执行default声明周期所有至test-compile的阶段
n @execute lifecycle = "<lifecycle>" phase = "<phase>",在运行该目标之前让Maven先运行一个自定义的生命周期,到指定的阶段为止。例如maven-surefire-report-plugin这个用来生成测试报告的插件,它有一个report目标,标注了@execute phase = "test" lifecycle = "surefire",表示运行这个自定义的urefire声明周期至test阶段。自定义生命周期的配置文件位于src/main/resources/META-INF/maven/lifecycle.xml
maven-surefire-report-plugin的自定义生命周期
<lifecycles> <lifecycle> <id>surefire</id> <phases> <phase> <id>test</id> <configuration> <testFailureIgnore>true</testFailureIgnore> </configuration> </phase> </phases> </lifecycle> </lifecycles>
Mojo参数
1、我们可以使用@parameter将Mojo的某个字段标注为可配置的参数,即Mojo参数
2、事实上几乎每个Mojo都有一个或者多个Mojo的参数,通过配置这些参数,Maven用户可以自定义插件的行为
3、Maven支持种类多样的Mojo参数,包括单值的boolean、int、float、String、Date、File和URL,多值的数组、Collection、Map、Properties等
n boolean (包含boolean和Boolean)
/**
* @parameter
*/
private boolean sampleBoolean
对应的配置如下:
<sampleBoolean>true</sampleBoolean>
n int (包含Integer、long、Long、short、Short、byte、Byte)
/**
* @parameter
*/
private int sampleInt
对应的配置如下
<sampleInt>8</sampleInt>
n float(包含Float、double、Double)
/**
* @parameter
*/
private float sampleFloat
对应的配置如下
<sampleFloat>8.8</sampleFloat>
n String(包含StringBuffer、char、Character)
/**
* @parameter
*/
private String sampleString
对应的配置如下
<sampleString>Hello World</sampleString>
n Date(格式为yyyy-MM-dd HH:mm:ss.S a或者yyyy-MM-dd HH:mm:ssa)
/**
* @parameter
*/
private Date sampleDate
对应的配置如下:
<sampleDate>2010-06-06 3:4:55.1 PM</sampleDate>
或者
<sampleDate>2010-06-06 3:14:55PM</sampleDate>
n File
/**
* @ parameter
*/
private File sampleFile
对应的配置如下:
<sampleFile>c:/tmp<./sampleFile>
n URL
/**
* @parameter
*/
private URL sampleURL
对应配置如下:
<sample=URL>http://www.juvenxu.com</sampleURL>
n 数组
/**
* @parameter
*/
private String[] includes
对应的配置如下:
<includes>
<include>java</include>
<include>sql</include>
</includes>
n Collection (任何实现Collection接口的类,如ArrayList和HashSet)
/**
* @parameter
*/
private List includes
对应的配置如下:
<includes>
<include>java</include>
<include>sql</include>
</includes>
n Map
/**
* parameter
*/
private Map sampleMap
对应的配置如下:
<sampleMap>
<key1>value1</key1>
<key2>value2</key2>
</sampleMap
n Properties
/**
* @parameter
*/
private Properties sampleProperties
对应的配置如下:
<sampleProperties>
<property>
<name>p_name_1</name>
<value>p_value_1</value>
</property>
<property>
<name>p_name_2</name>
<value>p_value_2</value>
</property>
</sampleProperties>
一个简单的@parameter标注就能让用户配置各种类型的Mojo字段,不过在此基础上,用户还能为@parameter标注提供一些额外的属性,进一步自定义Mojo参数
n @parameter alias = "<aliasName>"
使用alias,用户就可以为Mojo参数使用别名,当Mojo字段名称太长或者可读性不强时,这个别名就非常有用
/**
* @parameter alias = "uid"
*/
private String uniqueIdentity
对应的配置如下:
<uid>juven</uid>
n @parameter expression = "${aSystemProperty}"
使用系统属性表达式为Mojo参数进行赋值,这是非常有用的特性。配置@parameter的expression之后,用户可以再命令行配置该Mojo参数。例如,maven-surefire-plugin的test目标有如下源码:
/**
* @parameter expression="${maven.test.skip}"
*/
private boolean skip
用户可以在POM中配置skip参数,同时也可以直接在命令行使用-Dmaven.test.skip=true来跳过测试,如果Mojo参数没有提供expression,那就意味着该参数无法在命令行直接配置。还需要注意的是,Mojo参数的名称和expression名称不一定相同
n @parameter defaut-value = "aValue/ ${anExpression}"
如果用户没有配置该Mojo参数,就为其提供一个默认值。该值可以是一个简单字面量,如"true"、"hello"或者“1.5”,也可以是一个表达式,以方便使用POM的某个元素
例如,下面代码中的参数sampleBoolean默认值为true:
/**
* @parameter defaultValue="true"
*/
private boolean sampleBoolean
代码清单
/**
* @parameter expression="${project.build.sourceDirectory}"
* @required
* @readonly
*/
private File sourceDirectory;
表示默认使用POM元素<project><build><sourceDirectory>的值
除了@parameter标注外,还看到可以为Mojo参数使用@readonly和@required标注
n @readonly
表示该Mojo参数是只读的,如果使用了该标注,用户就无法对其进行配置。通常为应用POM元素内容的时候,我们不希望用户干涉
n @required
表示该Mojo参数是必须的,如果使用了该标注,但是用户没有配置该Mojo参数且其没有默认值,Maven就会报错
关于错误处理和日志
1、AbstractMojo实现了Mojo接口,execute()方法正是在这个接口中定的
void execute() throws MojoExecutionException, MojoFailureException;
2、这个方法可以抛出两种异常,分别是MojoExecutionException和MojoFailureException
3、如果Maven执行插件目标的时候遇到MojoFailureException,就会显示“BUILD FAILURE”的错误信息,这种异常表示Mojo在运行时发现了预期的错误。例如maven-surefire-plugin运行后若发现有失败的测试就会抛出该异常
4、如果Maven执行插件目标的时候遇到MojoExecutionException,就会显示"BUILD ERROR"的错误信息。这种异常表示Mojo在运行时发现了未预期的错误,比如代码统计插件何时会遇到IOException,我们并不知道,这个时候只能将其嵌套进MojoExecutation后再抛出
AbstractMojo提供了一个getLog()方法
该方法返回Log对象,该对象支持四种级别的日志方法,它们从低到高分别为:
n debug,调试级别的日志。Maven默认不会输出该级别的日志,不过用户可以在执行mvn命令的时候使用 -X参数开启调试日志,该级别的日志是用来帮助程序员了解插件具体运行状态的,因此应该尽量详细。需要注意的是,不要指望你的用户会主动去看该界别的日志
n info,消息级别的日志。Maven默认会输出该级别的日志,该级别的日志应该足够简洁,帮助用户了解插件重要的与运行状态。例如,maven-compiler-plugin会使用该级别的日志告诉用户源代码编译的目标目录
n warn:警告级别的日志,当插件运行的时候遇到了一些问题或错误,不过这类问题不会导致运行失败的时候,就应该使用该级别的日志警告用户尽快修复
n error:错误级别的日志。当插件运行的时候遇到了一些问题或者错误,并且这类问题导致Mojo无法继续运行,就应该使用该级别的日志提供详细的错误信息
上述每个级别的日志都提供了三个方法
n void debug (CharSequence content)
n void debug (CharSequence content, Throwable error)
n void debug(Throwable error)
在编写插件的时候,应该根据实际情况选择适应的方法。基本的原则是,如果有异常出现,就应该尽量使用适宜的日志方法将异常堆栈记录下来,方便将来的问题分析
编写自动化的集成测试代码来验证Maven插件的行为
1、既然是集成测试,那么就一定需要一个实际的Maven项目,配置该项目使用插件,然后在该项目上运行Maven构建,最后再验证该构建成功与否,可能还需要检查构建的输出
2、Maven社区有一个用来帮助插件集成测试的插件,它就是maven-invoker-plugin,该插件能够用来在一组项目上执行maven,并检查每个项目的构建是否成功,最后,它还可以执行BeanShell或者Groovy脚本来验证项目构建的输出
BeanShell和Groovy
BeanShell和Grooy都是基于JVM平台的脚本语言,读者可以访问http://www.beanshell.org/和http://groovy.codehaus.org了解更多的信息