maven進階——開發(fā)自定義插件

前言

maven不僅僅只是項目的依賴管理工具,其強大的核心來源自豐富的插件,可以說插件才是maven工具的靈魂。本篇文章將對如何自定義maven插件進行講解,希望對各位讀者有所幫助。

知識背景

什么是maven插件?
講如何開發(fā)maven插件之前,不妨先來聊一下什么是maven的插件。

我們知道,maven中主要有三大生命周期,clean,defaultreport,不同的生命周期中提供了一些快捷的操作命令給開發(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)了setLoggetLog方法,只留下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
image.png
(二)驗證插件

插件開發(fā)成功后,我們可以有2種方式來使用我們的自定義插件

方式一:直接通過maven命令(我們在第三章中會詳細講一下這個maven命令的使用)

maven命令的格式如下:

mvn 插件groupId:插件artifactId[:插件版本]:插件目標名稱

對應我們的案例,執(zhí)行的命令即為mvn com.qiqv:timer-plugin-demo:TimerPlugin,執(zhí)行命令后可以看到控制臺中就已經(jīng)出現(xiàn)了我們插件中打印的語句。

image.png
方式二:把自定義插件綁定在項目的生命周期中

如果我們的自定義插件是希望在打包的過程某個固定的生命周期發(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命令看一下結果

執(zhí)行結果

我們可以看到,我們的自定義插件順利隨著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為我們想要配置的值。

以下面的配置為例,我們分別為userNamestatus配置不同的值:

 <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í)行的結果

image.png

方式二:maven命令傳參

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

使用maven命令傳參
(二)插件中可以接收的參數(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,他的groupIdorg.apache.maven.plugins,所以能夠直接找到。

但是我們自己定義的插件,如果也讓maven能夠找到,需要下面的配置:
~/.m2/settings.xml中配置自定義插件組,我們需要在pluginGroups中加入自定義的插件組groupId,如:

<pluginGroup>com.qiqv</pluginGroup>

這樣當我們通過前綴調(diào)用插件的時候,maven除了會在2個默認的組中查找,還會在這些自定義的插件組中找,一般情況下我們自定義的插件通常使用同樣的groupId。
當我們配置了上面的自定義插件組后,我們就可以改為用下面的命令來調(diào)用我們的自定義插件了。

mvn timer:TimerPlugin
使用簡化命令調(diào)用我們的插件

通過上面這種方式,我們就可以實現(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
插件執(zhí)行日志

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

執(zhí)行效果圖

總結

這篇文章我們對maven自定義插件的開發(fā)方法進行了介紹,了解插件的參數(shù)如何傳遞以及不同的參數(shù)如何傳遞,并配合兩個簡單的案例來進行講解??偟膩碚f,自定義插件的開發(fā)并不復雜,掌握了基本知識后,難度其實在于插件所要實現(xiàn)的具體功能,后期有時間了我們可以出一期文章來分析某些優(yōu)秀的開源maven插件是怎么實現(xiàn)的。

本篇文章的源碼,可以從我的gitee上面獲取,地址如下:https://gitee.com/moutory/maven-plugin-demo
最后,也要十分感謝參考文章中的作者,他詳盡的文章給了我很大的幫助,也十分推薦各位讀者去看看這篇文章。

參考文章:

自定義插件,Maven的生命交給你來設計

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容