17.2 案例:编写一个用于代码行统计的Maven插件

为了便于大家实践,下面将详细演示如何实际编写一个简单的用于代码行统计的Maven插件。使用该插件,用户可以了解到Maven项目中各个源代码目录下文件的数量,以及它们加起来共有多少代码行。不过,笔者强烈反对使用代码行来考核程序员,因为大家都知道,代码的数量并不能真正反映一个程序员的价值。

要创建一个Maven插件项目,首先使用maven-archetype-plugin命令:


$mvn archetype:generate


然后选择:


maven-archetype-plugin(An archetype which contains a sample Maven plugin.)


输入Maven坐标等信息之后,一个Maven插件项目就创建好了。打开项目的pom.xml可以看到如代码清单17-1所示的内容。

代码清单17-1 代码行统计插件的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</name>

<url>http://www.juvenxu.com/</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。需要注意的是,代码清单17-1中并没有使用默认Archetype生成的maven-plugin-api版本,而是升级到了3.0,这样做的目的是与Maven的版本保持一致。

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

代码清单17-2 CountMojo的主要代码


/**

*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);

}

}

}


首先,每个插件目标类,或者说Mojo,都必须继承AbstractMojo并实现execute()方法,只有这样Maven才能识别该插件目标,并执行execute()方法中的行为。其次,由于历史原因,上述CountMojo类使用了Java 1.4风格的标注(将标注写在注释中),这里要关注的是@goal,任何一个Mojo都必须使用该标注写明自己的目标名称,有了目标定义之后,我们才能在项目中配置该插件目标,或者在命令行调用之。例如:


$mvn com.juvenxu.mvnbook:maven-loc-plugin:0.0.1-SNAPSHOT:count


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

下一步是为插件提供配置点。我们希望该插件默认统计所有Java、XML,以及properties文件,但是允许用户配置包含哪些类型的文件。代码清单17-2中的includes字段就是用来为用户提供该配置点的,它的类型为String数组,并且使用了@parameter参数表示用户可以在使用该插件的时候在POM中配置该字段,如代码清单17-3所示。

代码清单17-3 配置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>

</executions>

</plugin>


代码清单17-3配置了CountMojo统计Java和SQL文件,而不是默认的Java、XML和Properties。

代码清单17-2中还包含了basedir、sourceDirectory、testSourceDirectory等字段,它们都使用了@parameter标注,但同时关键字expression表示从系统属性读取这几个字段的值。${project.basedir}、${project.build.sourceDirectory}、${project.build.testSourceDirectory}

等表达式读者应该已经熟悉,它们分别表示了项目的基础目录、主代码目录和测试代码目录。@readonly标注表示不允许用户对其进行配置,因为对于一个项目来说,这几个目录位置都是固定的。

了解这些简单的配置点之后,下一步就该实现插件的具体行为了。从代码清单17-2的execute()方法中大家能看到这样一些信息:如果用户没有配置includes则就是用默认的统计包含配置,然后再分别统计项目主代码目录、测试代码目录、主资源目录,以及测试资源目录。这里涉及一个countDir()方法,其具体实现如代码清单17-4所示。

代码清单17-4 CountMojo的具体行为实现


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;

}


这里简单解释一下上述三个方法:collectFiles()方法用来递归地收集一个目录下所有应当被统计的文件,countLine()方法用来统计单个文件的行数,而countDir()则借助上述两个方法统计某一目录下共有多少文件被统计,以及这些文件共包含了多少代码行。

代码清单17-2中的execute()方法包含了简单的异常处理,代码行统计的时候由于涉及了文件操作,因此可能会抛出IOException。当捕获到IOException的时候,使用MojoExecutationException对其简单包装后再抛出,Maven执行插件目标的时候如果遇到MojoExecutationException,就会在命令行显示“BUILD ERROR”信息。

代码清单17-4中的countDir()方法的最后一行使用了AbstractMojo的getLog()方法,该方法返回一个类似于Log4j的日志对象,可以用来将输出日志到Maven命令行。这里使用了info级别的日志告诉用户某个路径下有多少文件被统计,共包含了多少代码行,因此在使用该插件的时候可以看到如下的Maven输出:


[INFO]——maven-loc-plugin:0.0.1-SNAPSHOT:count(default)@app——

[INFO]\src\main\java:13 lines of code in 1 files

[INFO]\src\test\java:38 lines of code in 1 files


使用mvn clean install命令将该插件项目构建并安装到本地仓库后,就能使用它统计Maven项目的代码行了。如下所示:


$ mvn com.juvenxu.mvnbook:maven - loc - plugin:0.0.1 - SNAPSHOT:count

[INFO] Scanning for projects…

[INFO]

[INFO] ----------------------------------------------------------------------

[INFO] Building Account Captcha 1.0.0 - SNAPSHOT

[INFO] ----------------------------------------------------------------------

[INFO]

[INFO] ---maven-loc - plugin:0.0.1-SNAPSHOT:count (default-cli) @ account-captcha

---

[INFO] \src\main\java: 179 lines of code in 4 files

[INFO] \src\test\java: 112 lines of code in 2 files

[INFO] \src\main\resources: 11 lines of code in 1 files

[INFO] \src\test\resources: 0 lines of code in 0 files

[INFO] ----------------------------------------------------------------------

[INFO] BUILD SUCCESS

[INFO] ----------------------------------------------------------------------

[INFO] Total time: 0.423s

[INFO] Finished at: Sat Jun 05 16:28:35 CST 2010

[INFO] Final Memory: 1M/4M

[INFO] ----------------------------------------------------------------------


如果嫌命令行太长太复杂,可以将该插件的groupId添加到settings.xml中。如下所示:


<settings>

<pluginGroups>

<pluginGroup>com.juvenxu.mvnbook</pluginGroup>

</pluginGroups>

</settings>


现在Maven命令行就可以简化成:


$mvn loc:count


这里面的具体原理可参考7.8.4节。