編寫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、從上述代碼中能夠看到一個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瞭解更多的信息