编写Maven插件的的一般步骤:
- 创建一个maven-plugin项目:插件本身也是Maven项目,特殊的地方在于它的packaging必须是maven-plugin,用户可以使用maven-archetype-plugin快捷创建一个Maven插件项目。
- 为插件编写目标:每个插件都必须包含一个或者多个目标,Maven称之为Mojo。编写插件的时候必须提供一个或者多个继承自AbstractMojo的类。
- 为目标提供配置点:大部分Maven插件及其目标都是可配置的,因此在编写Mojo的时候需要注意提供可配置的参数。
- 编写代码实现目标行为:根据实际的需要实现Mojo。
- 错误处理及日志:当Mojo发生异常时,根据情况控制Maven的运行状态。在代码中编写必要的日志以便为用户提供足够的信息。
- 测试插件:编写自动化的测试代码测试行为,然后再实际运行插件以验证其行为。
编写一个用于代码行统计的Maven插件
要创建一个Maven插件项目首先使用maven-archetype-plugin命令:
mvn archetype:generate
然后选择 490 maven-archetype-plugin (An archetype which contains a sample Maven plugin.)
输入Maven坐标等信息之后,一个Maven插件项目就创建好了。打开项目的pom.xml可以看到如下代码
<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.juvenxu.mvnbook</groupId>
<artifactId>maven-loc-plugin</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>maven-plugin</packaging>
<name>maven-loc-plugin 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>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
<version>3.2</version>
<configuration>
<goalPrefix>maven-loc-plugin</goalPrefix>
<skipErrorNoDescriptorsFound>true</skipErrorNoDescriptorsFound>
</configuration>
<executions>
<execution>
<id>mojo-descriptor</id>
<goals>
<goal>descriptor</goal>
</goals>
</execution>
<execution>
<id>help-goal</id>
<goals>
<goal>helpmojo</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>run-its</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-invoker-plugin</artifactId>
<version>1.7</version>
<configuration>
<debug>true</debug>
<cloneProjectsTo>${project.build.directory}/it</cloneProjectsTo>
<pomIncludes>
<pomInclude>*/pom.xml</pomInclude>
</pomIncludes>
<postBuildHookScript>verify</postBuildHookScript>
<localRepositoryPath>${project.build.directory}/local-repo</localRepositoryPath>
<settingsFile>src/it/settings.xml</settingsFile>
<goals>
<goal>clean</goal>
<goal>test-compile</goal>
</goals>
</configuration>
<executions>
<execution>
<id>integration-test</id>
<goals>
<goal>install</goal>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Maven插件项目的POM有两个特殊的地方
- 它的packing必须为maven-plugin,这种特殊的打包类型能控制Maven为其在生命周期阶段绑定插件处理相关的目标,例如在compile阶段,Maven需要为插件项目构建一个特殊插件描述符文件。
- 从上述代码中可以看到一个artifactId为maven-plugin-api的依赖,该依赖中包含了插件开发所必需的类,例如稍后会看到的AbstractMojo。需要注意的是我们开发的插件中并没有默认Archetype生成的maven-plugin-api版本(2.0),而是升级到了3.0,这样做的目的是与Maven的版本保持一致。
插件项目创建好之后,下一步是为插件编写目标。使用Archetype生成的插件项目包含了一个名为MyMojo的Java文件,我们将其删除,然后自己创建一个CountMojo,如下:
package com.juvenxu.mvnbook;
/*
* Copyright 2001-2005 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.maven.model.Resource;
@Mojo( name = "count", defaultPhase = LifecyclePhase.PROCESS_SOURCES )
public class CountMojo extends AbstractMojo
{
private static final String[] INCLUDES_DEFAULT = {"java", "xml", "sql", "properties"};
private static final String[] RATIOS_DEFAULT = {"1.0", "0.25", "0.25", "0.25"};
private static final String DOT = ".";
@Parameter( defaultValue = "${project.basedir}", property = "basedir", required = true ,readonly = true)
private File basedir;
@Parameter( defaultValue = "${project.build.sourceDirectory}", property = "sourcedir", required = true ,readonly = true)
private File sourcedir;
@Parameter( defaultValue = "${project.build.testSourceDirectory}", property = "testSourcedir", required = true ,readonly = true)
private File testSourcedir;
@Parameter( defaultValue = "${project.resources}", property = "resources", required = true ,readonly = true)
private List<Resource> resources;
@Parameter( defaultValue = "${project.testResources}", property = "testResources", required = true ,readonly = true)
private List<Resource> testResources;
@Parameter
private String[] includes;
@Parameter
private String[] ratios;//TODO 定义为double[],从xml读取时提示java.lang.ClassCastException: [D cannot be cast to [Ljava.lang.Object;
private Map<String, Double> ratioMap = new HashMap<String, Double>();
private long realTotal;
private long fakeTotal;
public void execute() throws MojoExecutionException
{
initRatioMap();
try{
countDir(sourcedir);
countDir(testSourcedir);
for(Resource res : resources){
countDir(new File(res.getDirectory()));
}
for(Resource res : testResources){
countDir(new File(res.getDirectory()));
}
getLog().info("TOTAL LINES:"+fakeTotal+ " ("+realTotal+")");
}catch (IOException e){
throw new MojoExecutionException("Unable to count lines of code", e);
}
}
private void initRatioMap() throws MojoExecutionException{
if(includes == null || includes.length == 0){
includes = INCLUDES_DEFAULT;
ratios = RATIOS_DEFAULT;
}
if(ratios == null || ratios.length == 0){
ratios = new String[includes.length];
for(int i=0; i<includes.length; i++){
ratios[i] = "1.0";
}
}
if(includes.length != ratios.length){
throw new MojoExecutionException("pom.xml error: the length of includes is inconsistent with ratios!");
}
ratioMap.clear();
for(int i=0; i<includes.length; i++){
ratioMap.put(includes[i].toLowerCase(), Double.parseDouble(ratios[i]));
}
}
private void countDir(File dir) throws IOException {
if(! dir.exists()){
return;
}
List<File> collected = new ArrayList<File>();
collectFiles(collected, dir);
int realLine = 0;
int fakeLine = 0;
for(File file : collected){
int[] line = countLine(file);
realLine += line[0];
fakeLine += line[1];
}
String path = dir.getAbsolutePath().substring(basedir.getAbsolutePath().length());
StringBuilder info = new StringBuilder().append(path).append(" : ").append(fakeLine).append(" ("+realLine+")")
.append(" lines of code in ").append(collected.size()).append(" files");
getLog().info(info.toString());
}
private void collectFiles(List<File> collected, File file)
throws IOException{
if(file.isFile()){
if(isFileTypeInclude(file)){
collected.add(file);
}
}else{
for(File files : file.listFiles()){
collectFiles(collected, files);
}
}
}
private int[] countLine(File file)
throws IOException{
BufferedReader reader = new BufferedReader(new FileReader(file));
int realLine = 0;
try{
while(reader.ready()){
reader.readLine();
realLine ++;
}
}finally{
reader.close();
}
int fakeLine = (int) (realLine * getRatio(file));
realTotal += realLine;
fakeTotal += fakeLine;
StringBuilder info = new StringBuilder().append(file.getName()).append(" : ").append(fakeLine).append(" ("+realLine+")")
.append(" lines");
getLog().debug(info.toString());
return new int[]{realLine, fakeLine};
}
private double getRatio(File file){
double ratio = 1.0;
String type = getFileType(file);
if(ratioMap.containsKey(type)){
ratio = ratioMap.get(type);
}
return ratio;
}
private boolean isFileTypeInclude(File file){
boolean result = false;
String fileType = getFileType(file);
if(fileType != null && ratioMap.keySet().contains(fileType.toLowerCase())){
result = true;
}
return result;
}
private String getFileType(File file){
String result = null;
String fname = file.getName();
int index = fname.lastIndexOf(DOT);
if(index > 0){
String type = fname.substring(index+1);
result = type.toLowerCase();
}
return result;
}
}
首先每个插件目标类或者说Mojo都必须继承AbstractMojo并实现execute()方法,只有这样Maven才能识别该插件目标,并执行execute()方法中的行为。这里要关注的是@Mojo任何一个mojo都必须使用该注解写明自己的目标名称,有了目标定义之后我们才能在项目中配置该插件目标,或者在命令行调用之。例如:
mvn com.juvenxu.mvnbook:maven-loc-plugin:0.0.1-SNAPSHOT:count
创建一个Mojo所必要的工作就是这三项:继承Abstract,实现execute()方法,提供@Mojo注解。
下一步是为插件提供配置点。我们希望该插件默认统计所有Java,XML,以及properties文件,但是允许用户配置包含哪些类型的文件。代码中includes字段就是用来为用户提供该配置点的,他的类型为String数组,并且使用了@Parameter参数表示用户可以在使用该插件的时候在POM中配置该字段,如下:
<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>
</executions>
</plugin>
execute()方法包含了简单的异常处理,代码行统计的时候由于涉及了文件操作,因此可能会抛出IOException。当捕获到IOException的时候,使用MojoExecutationException对其简单包装后再抛出,Maven执行插件目标的时候如果遇到MojoExecutationException就会在命令显示“BUILD ERROR”信息。
countDir()方法最后一行使用了AbstractMojo的getLog()方法,该方法返回一个类似于Log4J的日志对象,可以用来
将输出日志到Maven命令行。这里使用了info级别打印日志。
使用mvn clean install 命令将该插件项目构建并安装到本地仓库后,就能使用它统计Maven项目的代码行了,如下
mvn com.juvenxu.mvnbook:maven-loc-plugin:0.0.1-SNAPSHOT:count
如果嫌命令行太长太复杂,可以将该插件的groupId添加到setting.xml中。如下:
<settings>
<pluginGroups>
<pluginGroup>com.juvenxu.mvnbook</pluginGroup>
</pluginGroups>
</settings>
现在Maven命令行就可以简化成
mvn loc:count