Maven實戰讀書筆記(18)

編寫Maven插件的通常步驟java

1、建立一個maven-plugin項目:插件自己也是Maven項目,特殊的地方在於它的packaging必須是maven-plugin,用戶可使用maven-archetype-plugin快速建立一個Maven插件項目程序員

2、爲插件編寫目標:每一個插件都必須包含一個或者多個目標,Maven稱之爲Mojo(POJO對應,後綴指Plain Old Java Object,這裏指Maven Old Java Object)。編寫插件的時候必須提供一個或者多個繼承自AbstractMojo的類sql

3、爲目標提供配置點:大部分Maven插件及其目標都是可配置的,所以在編寫Mojo的時候須要注意提供可配置的參數shell

4、編寫代碼實現目標行爲:根據實際的須要實現Mojoexpress

5、錯誤處理及日誌:當Mojo發生異常時,根據狀況控制Maven的運行狀態。在代碼中編寫必要的日誌以便爲用戶提供足夠的信息apache

6、測試插件:編寫自動化的測試代碼測試行爲,而後再實際運行插件以驗證其行爲api

 

編寫一個用於代碼行統計的Maven插件數組

使用該插件,用戶能夠了解到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、從上述代碼中能夠看到一個artifactIdmaven-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、有了目標定義名稱以後,咱們才能在項目中配置該插件目標,或者在命令行調用之

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

5、還包含了basedirsourceDirectorytestSourceDirectory等字段,它們都使用了@parameter標註,但同時關鍵字expression表示從系統屬性讀取這幾個字段的值

6${project.basedir}項目的基礎目錄,${project.build.sourceDirectory}主代碼目錄,${project.build.testSourceDirectory}測試代碼目錄

7@readonly標註表示不容許用戶對其進行配置,由於對於一個項目來講,這幾個目錄位置都是固定的

 

瞭解這些簡單的配置點以後,下一步就該實現插件的具體行爲了

1、從execute()方法中能夠看到,若是用戶沒有配置includes則就是用默認的統計包含配置

2、而後再分別統計項目主代碼目錄、測試代碼目錄、主資源目錄,以及測試資源目錄

3、這裏涉及了一個countDir()方法

 

簡單解釋CollectFiles()countLine()CountDir()這三個方法

1collectFiles()方法用來遞歸地蒐集一個目錄下全部應當被統計的文件

2countLine()方法用來統計單個文件的行數

3countDir()則藉助上述兩個方法統計某個目錄下共有多少文件被統計,以及這些文件共包含了多少代碼航

 

簡單解釋execute()方法包含的異常處理

1execute()方法包含了簡單的異常處理,代碼行統計的時候因爲涉及了文件操做,所以可能會拋出IOException

2、當捕獲到IOException的時候,使用MojoExecutationException對其簡單包裝後再拋出

3Maven執行插件目標的時候若是遇到MojoExecutationException,就會在命令行顯示"BULD ERROR" 信息

 

countDir()方法是最後一行使用了AbstractMojogetLog()方法

1、該方法返回一個相似於Log4j的日誌對象,能夠用來將輸出日誌到Maven命令行

2、這裏使用了info級別的日誌告訴用戶某個路徑下有多少文件被通緝,共包含了多少代碼行

 

下一步是爲插件提供配置點

1、咱們但願該插件默認統計全部的JavaXML,以及properties文件,可是容許用戶配置包含哪些類型的文件

2includes字段就是用來爲用戶提供該配置點的,它的類型爲String數組,而且使用了@parameter參數表示用戶能夠在使用該插件的時候在POM中配置該字段

 

配置CountMojoincludes參數

<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統計JavaSQL文件,而不是默認的JavaXMLproperties

 

使用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-plugintest目標就帶有@phase test標註

n         @requiresDependencyResolution <scope>,表示在運行該Mojo以前必須解析全部指定範圍的依賴。例如,maven-surefire-plugintest目標帶有@requiresDependencyResolution test標註,表示在執行測試以前,全部測試範圍的依賴必須獲得解析。這裏可用的依賴範圍有compiletestruntime,默認值爲runtime

n         @requiresProject <true/false>,表示該目標是否必須在一個Maven項目中運行,默認爲true。大部分插件目標都須要依賴一個項目才能執行,但有一些例外。例如maven-help-pluginsystem目標,它用來顯示系統屬性和環境變量信息,不須要實際項目,所以使用了@requiresProject false標註。另外maven-archetype-plugingenerate目標也是一個很好的例子

n         @requiresDirectInvocation <true/false>,當值爲true的時候,該目標就只能經過命令行直接調用,若是試圖在POM中將其綁定到生命週期階段,Maven就會報錯,默認值爲false。若是你但願編寫的插件只能在命令行獨立運行,就應當使用該標註

n         @requiresOnline <true/false>,表示是否要求Maven必須是在線狀態,默認值是false

n         @requiresReport <true/false>,表示會否要求項目報告已經生成,默認值是false

n         @aggregator,當Mojo在多模塊項目上運行時,使用該標註表示該目標只會在頂層模塊運行,例如maven-javadoc0pluginaggregator-jar使用了@aggregator標註,它不會爲多模塊項目的每一個模塊生成javadoc,而是在頂層項目生成一個已經聚合的Javadoc文檔

n         @execute goal = "<goal>",在運行該目標以前先讓Maven運行另一個目標,若是是本插件的目標,則直接使用目標名稱,不然使用"prefix:goal"的形式,即註明目標前綴。例如,maven-pmd-plugin是一個使用PMD來分析項目源碼的工具,它包含pmdcheck等目標,其中pmd用來生成報告,而check用來驗證報告。因爲check是依賴於pmd生成的內容的,所以能夠看到它使用了標註@execute goal = pmd

n         @execute phase = "<phase>",在運行該目標以前讓Maven選運行一個並行的生命週期,到指定的階段爲止。例如maven-dependency-pluginanalyze使用了標註@execute phase=test-compile」,所以當用戶在命令行執行dependencyanalyze的時候,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、咱們可使用@parameterMojo的某個字段標註爲可配置的參數,即Mojo參數

2、事實上幾乎每一個Mojo都有一個或者多個Mojo的參數,經過配置這些參數,Maven用戶能夠自定義插件的行爲

3Maven支持種類多樣的Mojo參數,包括單值的booleanintfloatStringDateFileURL,多值的數組、CollectionMapProperties


n         boolean (包含booleanBoolean)

/**

* @parameter

*/

private boolean sampleBoolean

對應的配置以下:

<sampleBoolean>true</sampleBoolean>

n         int (包含IntegerlongLongshortShortbyteByte)

/**

* @parameter

*/

private int sampleInt

對應的配置以下

<sampleInt>8</sampleInt>

n         float(包含FloatdoubleDouble

/**

* @parameter

*/

private float sampleFloat

對應的配置以下

<sampleFloat>8.8</sampleFloat>

n         String(包含StringBuffercharCharacter

/**

* @parameter

*/

private String sampleString

對應的配置以下

<sampleString>Hello World</sampleString>

n         Date(格式爲yyyy-MM-dd HHmmss.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接口的類,如ArrayListHashSet)

/**

* @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參數進行賦值,這是很是有用的特性。配置@parameterexpression以後,用戶能夠再命令行配置該Mojo參數。例如,maven-surefire-plugintest目標有以下源碼:

/**

* @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就會報錯

 

關於錯誤處理和日誌

1AbstractMojo實現了Mojo接口,execute()方法正是在這個接口中定的

void execute() throws MojoExecutionException, MojoFailureException;

2、這個方法能夠拋出兩種異常,分別是MojoExecutionExceptionMojoFailureException

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構建,最後再驗證該構建成功與否,可能還須要檢查構建的輸出

2Maven社區有一個用來幫助插件集成測試的插件,它就是maven-invoker-plugin,該插件可以用來在一組項目上執行maven,並檢查每一個項目的構建是否成功,最後,它還能夠執行BeanShell或者Groovy腳原本驗證項目構建的輸出

 

BeanShellGroovy

BeanShellGrooy都是基於JVM平臺的腳本語言,讀者能夠訪問http://www.beanshell.org/http://groovy.codehaus.org瞭解更多的信息

相關文章
相關標籤/搜索