编写Maven插件的的一般步骤:

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

编写一个用于代码行统计的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有两个特殊的地方

 

  1. 它的packing必须为maven-plugin,这种特殊的打包类型能控制Maven为其在生命周期阶段绑定插件处理相关的目标,例如在compile阶段,Maven需要为插件项目构建一个特殊插件描述符文件。
  2. 从上述代码中可以看到一个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