前言
maven不僅僅只是項目的依賴管理工具,其強大的核心來源自豐富的插件,可以說插件才是maven工具的靈魂。本篇文章將對如何自定義maven插件進行講解,希望對各位讀者有所幫助。
知識背景
什么是maven插件?
講如何開發(fā)maven插件之前,不妨先來聊一下什么是maven的插件。
我們知道,maven中主要有三大生命周期,clean,default和report,不同的生命周期中提供了一些快捷的操作命令給開發(fā)人員去進行項目的清理、編譯、打包等操作。之所以我們可以通過類似于mvn clean compile等命令快速完成項目的清理和構建,實際上是因為maven在項目核心的生命周期節(jié)點上,已經(jīng)設置好了默認的運行插件,我們執(zhí)行命令的時候,實際上調(diào)用的就是綁定對應生命周期的插件。
maven插件本質(zhì)上就是一個jar包,不同的插件里面有著不同功能的代碼,當我們調(diào)用該插件的時候,其實就是通過執(zhí)行jar包中的方法,去達到對應的goal,實現(xiàn)我們的目的。除了官網(wǎng)的maven插件之外,其實maven還允許我們根據(jù)實際的項目需要,自定義去開發(fā)maven插件,從而滿足我們的項目構建需要。
關于maven的生命周期和插件,我們可以從官方文檔中了解更多信息:
maven的生命周期:https://maven.apache.org/ref/3.8.6/maven-core/lifecycles.html
maven生命周期綁定的默認插件:https://maven.apache.org/ref/3.8.6/maven-core/default-bindings.html
一、自定義插件demo開發(fā)
(一)開發(fā)自定義插件
步驟一:配置pom文件
我們先在idea中創(chuàng)建一個maven項目,并在pom文件中寫入如下配置,這里的目的是標識我們這個項目是maven插件項目,需要按照插件的方式來進行打包。
<packaging>maven-plugin</packaging>
其次,引入插件開發(fā)所需要的依賴,這里版本不一定需要和我一致,只要兩個依賴之前版本不要差距過大就行。
<!--這個依賴引入了插件開發(fā)需要的相關基礎類-->
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>3.0</version>
</dependency>
<!--這個依賴引入了插件開發(fā)需要的相關注解-->
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>3.4</version>
<scope>provided</scope>
</dependency>
步驟二:定義插件類
我們上一個步驟已經(jīng)在pom文件中定義了我們這個項目是一個maven插件類型的項目,而maven插件是需要對外提供各種實際應用能力的,在這個步驟中,我們將定義我們這個插件對外提供的實際能力,這樣其他項目想要使用我們插件的時候,就可以選擇對應的goal來執(zhí)行了。
PS:需要注意的是,一個插件是可以同時提供多種能力的,但本案例只作為演示所以只會寫一個功能作
1、定義插件類的2種方式
定義插件類有2種方式,第一種是創(chuàng)建java類,實現(xiàn)org.apache.maven.plugin.Mojo接口。
void execute() throws MojoExecutionException, MojoFailureException;
void setLog( Log log );
Log getLog();
這個接口一共定義了三個接口方法:
- execute 這個方法為核心方法,當使用mvn命令調(diào)用插件的目標的時候,最后具體調(diào)用的就是這個方法
- setLog:注入一個標準的Maven日志記錄器,允許這個Mojo向用戶傳遞事件和反饋
- getLog:獲取注入的日志記錄器
一般來說,我們并不會直接通過實現(xiàn)接口的方式來定義插件,而是會采用繼承maven提供的抽象類org.apache.maven.plugin.AbstractMojo來定義我們的插件類。
public abstract class AbstractMojo implements Mojo, ContextEnabled {
...
}
我們可以看到,這個抽象類實現(xiàn)了Mojo接口,同時這個抽象類還實現(xiàn)了setLog和getLog方法,只留下execute方法給開發(fā)人員去實現(xiàn)。這個類中Log默認可以向控制臺輸出日志信息,maven中自帶的插件都繼承這個類,一般情況下我們開發(fā)插件目標可以直接繼承這個類,然后實現(xiàn)execute方法就可以了。
2、在我們的代碼中進行實操
在剛剛創(chuàng)建的插件項目中,創(chuàng)建一個普通的java類,繼承org.apache.maven.plugin.AbstractMojo這個抽象類,同時需要在類上面使用@Mojo注解。這樣maven在執(zhí)行我們的插件的時候,才能夠找到我們的入口類。
在execute方法中,我們簡單地打印一句話到控制臺
@Mojo(name = "TimerPlugin")
public class TimerPlugin extends AbstractMojo {
public void execute() throws MojoExecutionException, MojoFailureException {
String currentTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
getLog().info("timer plugin is running,current time is " + currentTime);
}
}
步驟三:將我們的插件上傳到我們的本地倉庫
執(zhí)行如下maven命令實現(xiàn)插件的上傳,打包完成后我們就可以在我們本地倉庫中找到我們的插件了。
mvn clean install

(二)驗證插件
插件開發(fā)成功后,我們可以有2種方式來使用我們的自定義插件
方式一:直接通過maven命令(我們在第三章中會詳細講一下這個maven命令的使用)
maven命令的格式如下:
mvn 插件groupId:插件artifactId[:插件版本]:插件目標名稱
對應我們的案例,執(zhí)行的命令即為mvn com.qiqv:timer-plugin-demo:TimerPlugin,執(zhí)行命令后可以看到控制臺中就已經(jīng)出現(xiàn)了我們插件中打印的語句。

方式二:把自定義插件綁定在項目的生命周期中
如果我們的自定義插件是希望在打包的過程某個固定的生命周期發(fā)生作用,那么我們可以在pom文件中把插件綁定在我們項目的生命周期中。
插件寫在build->plugins->plugin標簽下,groupId這些信息和插件保持一直,然后需要在executions標簽中定義我們這個使用這個自定義插件的哪個能力,也就是goal,phase標簽里面定義要綁定的生命周期,id用于命令,可以自己定義。
<build>
<plugins>
<plugin>
<groupId>com.qiqv</groupId>
<artifactId>timer-plugin-demo</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<id>test</id>
<phase>compile</phase>
<goals>
<goal>TimerPlugin</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
在外部項目中綁定好插件后,我們通過mvn compile命令看一下結果

我們可以看到,我們的自定義插件順利隨著compile生命周期執(zhí)行了。
二、插件傳參
在第一章中,我們已經(jīng)簡單實現(xiàn)了一個maven插件,但實際上這種沒有任何參數(shù)的插件一般來說很難支持比較復雜、靈活的功能。所以這一章中,我們將學習如何把參數(shù)傳遞給我們的插件。
(一)在Mojo類中定義我們接收數(shù)據(jù)的參數(shù)
延續(xù)第一章的例子,我們在插件類中定義2個變量,并加上@Paramter注解,這個注解將變量標識為mojo參數(shù)。注解的defaultValue參數(shù)定義變量的默認值。property參數(shù)可用于通過引用用戶通過-D選項設置的系統(tǒng)屬性,即通過從命令行配置mojo參數(shù),如mvn ... -Dtimer.username=moutoryJava可以將moutory的值傳遞給userName參數(shù)。
當然了,如果說不打算通過命令行執(zhí)行,只想傳參給和生命周期綁定的插件,那么也需要要加上注解,注解里面可以不用配置其他屬性。
@Mojo(name = "TimerPlugin")
public class TimerPlugin extends AbstractMojo {
@Parameter(property = "timer.username" ,defaultValue = "moutory")
private String userName;
@Parameter(property = "timer.status", defaultValue = "happy")
private String status;
public void execute() throws MojoExecutionException, MojoFailureException {
String currentTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
getLog().info("timer plugin is running, current time is " + currentTime);
getLog().info(String.format("hi %s ! Now you are %s",userName,status));
}
}
(二)在使用中向我們的插件傳遞參數(shù)
方式一:在pom文件進行定義
這種做法一般是用于和生命周期綁定使用的插件,我們可以在配置插件的時候,利用configuration標簽來配置我們插件的值。configuration標簽內(nèi)可以寫入我們想要配置的參數(shù),標簽的key為我們插件定義的屬性名,標簽的value為我們想要配置的值。
以下面的配置為例,我們分別為userName和status配置不同的值:
<build>
<plugins>
<plugin>
<groupId>com.qiqv</groupId>
<artifactId>timer-plugin-demo</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<id>test</id>
<phase>compile</phase>
<goals>
<goal>TimerPlugin</goal>
</goals>
</execution>
</executions>
<configuration>
<userName>Jim</userName>
<status>good</status>
</configuration>
</plugin>
</plugins>
</build>
使用mvn compile命令,看一下執(zhí)行的結果

方式二:maven命令傳參
這種方式比較適用于那些獨立使用的插件,比如代碼掃描工具或者依賴掃描插件。使用maven命令傳參也十分簡單,在命令后面使用-Dkey=value。這里的key為@property中配置的property屬性的值,value為我們實際傳入的值。對應到我們上面的案例,這里的參數(shù)就可以是-Dtimer.username=Tonny -Dtimer.status=bad。

(二)插件中可以接收的參數(shù)類型
除了String類型,自定義插件還可以接收其他多種參數(shù)類型,下面就對這些參數(shù)進行簡單的介紹。
1、boolean類型
@Parameter
private boolean myBoolean;
<configuration>
<myBoolean>true</myBoolean>
</configuration>
2、數(shù)字類型
數(shù)字類型包含:byte, Byte, int, Integer, long, Long, short, Short,讀取配置時,XML文件中的文本將使用適當類的integer.parseInt()或valueOf()方法轉(zhuǎn)換為整數(shù)值,這意味著字符串必須是有效的十進制整數(shù)值,僅由數(shù)字0到9組成,前面有一個可選的-表示負值。
@Parameter
private Integer myNum;
<configuration>
<myNum>2</myNum>
</configuration>
3、File類型參數(shù)
讀取配置時,XML文件中的文本用作所需文件或目錄的路徑。如果路徑是相對的(不是以/或C:之類的驅(qū)動器號開頭),則路徑是相對于包含POM的目錄的
@Parameter
private File myFile;
<configuration>
<myFile>C:\test</myFile>
</configuration>
4、枚舉類型參數(shù)
public enum Color {
GREEN,
RED,
BLUE
}
/**
* My Enum
*/
@Parameter
private Color myColor;
<myColor>GREEN</myColor>
5、數(shù)組類型參數(shù)
@Parameter
private String[] myArr;
<configuration>
<myArr>
<param>aa</param>
<param>bb</param>
</myArr>
</configuration>
6、Collections類型參數(shù)
和數(shù)組類型參數(shù)用法一樣
7、Maps類型參數(shù)
@Parameter
private Map myMap;
<configuration>
<myMap>
<key1>value1</key1>
<key2>value2</key2>
</myMap>
</configuration>
8、Properties類型參數(shù)
@Parameter
private Properties myProperties;
<myProperties>
<property>
<name>propertyName1</name>
<value>propertyValue1</value>
<property>
<property>
<name>propertyName2</name>
<value>propertyValue2</value>
<property>
</myProperties>
9、自定義類型參數(shù)
@Parameter
private MyObject myObject;
<myObject>
<myField>test</myField>
</myObject>
三、自定義插件前綴的使用
理論上我們開發(fā)自定義插件時,artifactId是可以隨便寫,并沒有嚴格的書寫規(guī)定。但即使把artifactId定義得比較短,我們在通過命令使用插件時,還是不得不敲上一長串的命令,無法像mvn clean這些官方插件一樣,使用很簡單的命令就可以完成操作。出于簡化命令的目的,其實maven官方已經(jīng)定義一套插件命名規(guī)范,只要滿足這套命名規(guī)范,就可以在執(zhí)行命令的時候簡化我們的語法。
若自定義插件的artifactId滿足下面的格式:
xxx-maven-plugin
如果采用這種格式的maven會自動將xxx指定為插件的前綴,其他格式也可以,不過此處我們只說這種格式,這個是最常用的格式。
比如我們上面自定義插件改為timer-maven-plugin,他的前綴就是timer。
當我們配置了插件前綴,可以插件前綴來調(diào)用插件的目標了,命令如下:
mvn 插件前綴:插件目標
但是上面的命令支提供了插件前綴,那么maven是如何知道對應的groupId的呢?
maven默認會在倉庫"org.apache.maven.plugins" 和 "org.codehaus.mojo"2個位置查找插件,比如:
mvn clean:help。這個是調(diào)用maven-clean-plugin插件的help目標,maven-clean-plugin的前綴就是clean,他的groupId是org.apache.maven.plugins,所以能夠直接找到。
但是我們自己定義的插件,如果也讓maven能夠找到,需要下面的配置:
在~/.m2/settings.xml中配置自定義插件組,我們需要在pluginGroups中加入自定義的插件組groupId,如:
<pluginGroup>com.qiqv</pluginGroup>
這樣當我們通過前綴調(diào)用插件的時候,maven除了會在2個默認的組中查找,還會在這些自定義的插件組中找,一般情況下我們自定義的插件通常使用同樣的groupId。
當我們配置了上面的自定義插件組后,我們就可以改為用下面的命令來調(diào)用我們的自定義插件了。
mvn timer:TimerPlugin

通過上面這種方式,我們就可以實現(xiàn)用簡化命令來調(diào)用我們的插件啦~~下面是需要額外注意的事項:
1、官方的插件命名一般是maven-xxx-plugin,第三方的自定義插件命名一般是xxx-maven-plugin,我們的命名規(guī)范盡量不要和官方插件的一致。
2、如果不想配置settings.xml這么麻煩,也可以直接在命令中加上groupId,這樣也可以讓maven可以找到你的自定義插件。
四、開發(fā)一個功能性更強的自定義組件
通過前面三章的介紹,其實我們已經(jīng)能夠掌握自定義插件開發(fā)的大部分知識了,接下來我們就以開發(fā)功能性更強的插件為例,來作為收尾的一個小案例。
對一些web項目來說,打包完之后我們需要把war包部署到tomcat上面進行部署,每次都需要手動復制包十分的麻煩,我們不妨嘗試著自己開發(fā)一個插件,來解決這個場景的問題。
(當然了,現(xiàn)在idea都有這種自動部署的功能,但作為學習的案例,自己手動實現(xiàn)一下這個功能也是十分有意思的)
步驟一:創(chuàng)建一個新的插件項目,定義pom文件
這一步和之前的并沒有什么區(qū)別,還是定義項目的打包類型以及相關依賴
<packaging>maven-plugin</packaging>
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>3.0</version>
</dependency>
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>3.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
步驟二:創(chuàng)建插件的核心執(zhí)行類
這里的話沒有寫的很復雜,插件使用者提供一下war包位置,tomcat路徑以及最終的文件名,插件做一下簡單的IO而已。
@Mojo(name="deploy")
public class DeployPlugin extends AbstractMojo {
@Parameter
private String sourceWarPath;
@Parameter
private String tomcatHome;
@Parameter
private String targetFileName;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
this.getLog().info("deploy plugin starting...");
this.getLog().info("sourceWarPath:[{"+sourceWarPath+"}] , tomcatHome:[{"+tomcatHome+"}]");
boolean cpSuccess = this.deployFile(sourceWarPath, tomcatHome, targetFileName);
if(cpSuccess){
this.getLog().info("deploy success");
return;
}
this.getLog().error("deploy fail");
}
private boolean deployFile(String sourceWarPath, String tomcatHome,String targetFileName) {
if(StringUtils.isBlank(sourceWarPath) || StringUtils.isBlank(tomcatHome)){
throw new RuntimeException("missing required param!");
}
InputStream inputS = null;
OutputStream outputS = null;
String targetWarPath = tomcatHome + File.separator +"webapps" + File.separator + targetFileName;
// 通過 String 創(chuàng)建文件
try{
File destF = new File(targetWarPath);
File srcF = new File(sourceWarPath);
// 通過 File 創(chuàng)建 文件流
inputS = new FileInputStream(srcF);
outputS = new FileOutputStream(destF);
// 讀寫流
byte[] buffer = new byte[1024];
int length = 0;
while ((length = inputS.read(buffer)) > 0) {
outputS.write(buffer, 0, length);
}
}catch (Exception e){
e.printStackTrace();
return false;
}finally {
closeIO(inputS,outputS);
}
return true;
}
private void closeIO(InputStream inputS, OutputStream outputS) {
// 關閉流
if(inputS != null){
try{
inputS.close();
}catch (Exception e){
this.getLog().error("close input stream fail..");
e.printStackTrace();
}
}
if(outputS != null){
try{
outputS.close();
}catch (Exception e){
this.getLog().error("close output stream fail..");
e.printStackTrace();
}
}
}
}
步驟三:在我們的其他項目引入我們的插件
這里需要注意,除了需要傳參之外,還需要把插件綁定在package的生命周期
<build>
<plugins>
<plugin>
<groupId>com.qiqv</groupId>
<artifactId>webDeploy-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<executions>
<execution>
<id>test</id>
<phase>package</phase>
<goals>
<goal>deploy</goal>
</goals>
</execution>
</executions>
<configuration>
<sourceWarPath>D:\devWorkPlace\devCode\mime\maven-plugin\order-api\target\order-api-1.0-SNAPSHOT.war</sourceWarPath>
<tomcatHome>D:\devWorkPlace\devTool\apache-tomcat-9.0.68</tomcatHome>
<targetFileName>order-api-1.0-SNAPSHOT.war</targetFileName>
</configuration>
</plugin>
</plugins>
</build>
步驟四:項目打包,觀察插件的運行日志:
mvn package

從日志上面來看,war包已經(jīng)復制成功了,那么切換到對應的webapp目錄上面,也可以看到,war包確實已經(jīng)復制過來了。

總結
這篇文章我們對maven自定義插件的開發(fā)方法進行了介紹,了解插件的參數(shù)如何傳遞以及不同的參數(shù)如何傳遞,并配合兩個簡單的案例來進行講解??偟膩碚f,自定義插件的開發(fā)并不復雜,掌握了基本知識后,難度其實在于插件所要實現(xiàn)的具體功能,后期有時間了我們可以出一期文章來分析某些優(yōu)秀的開源maven插件是怎么實現(xiàn)的。
本篇文章的源碼,可以從我的gitee上面獲取,地址如下:https://gitee.com/moutory/maven-plugin-demo
最后,也要十分感謝參考文章中的作者,他詳盡的文章給了我很大的幫助,也十分推薦各位讀者去看看這篇文章。