記一次Maven插件的開發過程


需求描述

開發一個掃描類信息(如:方法名,注解名等)的腳本程序,由於掃描的是提供Jar包中的代碼,不希望在運行期進行這些邏輯的運行,減少重復的運行操作,希望每次Jar包中代碼的變更能夠對應一次信息的上報。

我們的項目打包都是通過Maven來進行的,而Maven提供了插件機制,使我們能在Maven管理我們項目的各個生命周期中進行一些騷操作。

我這次的需求剛好用Maven的插件機制滿足,在這次需求中學習到了Maven插件的編寫,並且踩了很多坑,在這里記錄下來,希望能夠幫助其他人在開發的時候進行避免。

Maven插件篇

Mojo工程

概念

Mojo 就是 Maven plain Old Java Object。每一個 Mojo 就是 Maven 中的一個執行目標(executable goal),而插件則是對單個或多個相關的 Mojo 做統一分發。一個 Mojo 包含一個簡單的 Java 類。插件中多個類似 Mojo 的通用之處可以使用抽象父類來封裝。

創建Maven工程

命名:一般來說,我們會將自己的插件命名為 -maven-plugin,而不推薦使用 maven—plugin,因為后者是 Maven 團隊維護官方插件的保留命名方式,使用這個命名方式會侵犯 Apache Maven 商標。

創建:正常創建Maven項目就可以了,之后我們需要對pom.xml進行一些修改

修改:pom.xml需要添加對maven-plugin-api的依賴,這個依賴里面會包含一些 Mojo 的接口與抽象類。

<dependency>
  <groupId>org.apache.maven</groupId>
  <artifactId>maven-plugin-api</artifactId>
  <version>2.0</version>
</dependency>

與普通 pom.xml 文件一個重要的不同之處是它的打包方式:

<packaging>maven-plugin</packaging>

編寫Maven工程

Mojo 是一個簡單的 Java 類,那我們創建第一個 Mojo 類用於打印一行輸出。

Mojo類需要繼承 AbstractMojo 這個抽象類,並實現了 execute() 方法,該方法就是用來定義這個 Mojo 具體操作內容,我們只需要根據自己的需要來編寫自己的實現即可。

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
/**
 * @goal hello
 */
public class HelloMojo extends AbstractMojo {

    public void execute() throws MojoExecutionException, MojoFailureException {
        getLog().info("Hello Mojo");
    }

}
@Mojo(name = "hello")
public class HelloMojo extends AbstractMojo {

    public void execute() throws MojoExecutionException, MojoFailureException {
        getLog().info("Hello Mojo");
    }

}

怎么讓 Maven 知道這是一個 Mojo 而不是一個普通的 Java 類呢?這里,就需要說一下 Mojo 的查找機制了,在處理源碼的時候,plugin-tools 會把使用了 @Mojo 注解或 Javadoc 里包含 @goal 注釋的類來當作一個 Mojo 類。

使用 @Mojo 注解,我們需要引入一個新包:

<dependency>
  <groupId>org.apache.maven.plugin-tools</groupId>
  <artifactId>maven-plugin-annotations</artifactId>
  <version>3.1</version>
</dependency>

運行自定義Plugin

與使用其它插件類似,我們需要在 pom.xml 文件中引入插件:

<build>
    <plugins>
        <plugin>
            <groupId>XXX</groupId>
            <artifactId>XXX</artifactId>
            <version>XXX</version>
        </plugin>
    </plugins>
</build>

我們還可通過配置指定Maven插件在生命周期的哪個階段執行,還可以通過一些命令賦值。

比如我這次的需求,就需要在編譯期執行,將需要掃描的類名傳入

    <build>
        <plugins>
            <plugin>
                <groupId>com.sankuai</groupId>
                <artifactId>athena-nr-maven-plugin</artifactId>
                <version>0.0.2</version>
                <executions>
                    <execution>
                        <!-- 配置compile執行 -->
                        <phase>compile</phase>
                        <goals>
                            <!-- 配置執行目標 -->
                            <goal>reporter</goal>
                        </goals>
                        <configuration>
                            <!-- className是要傳入的值 -->
                            <className>XXX</className>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
//執行目標
@Mojo(name="reporter" ,requiresDependencyResolution = ResolutionScope.COMPILE)
public class Reporter extends AbstractMojo{

		//傳入參數
    @Parameter(property = "reporter.className")
    private String className;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
      	//執行邏輯
    		......
    }

}

Maven插件調試

調試方法有很多,選一個比較簡單需要使用IDEA的。

在終端使用mvnDebug groupID:artifactID:version:goal命令來啟動插件,這個時候會啟動8000端口。

我們還需要使用maven的遠程調試,在IDEA中用remote連接過去,並且在插件的execute方法上打上斷點。

Maven插件依賴問題(第一個坑)

我們的邏輯是將全類名傳入Mojo類,通過全類名去加載該類,然后對該類進行一些操作。

然而卻爆出了找不到該類的問題,為什么當前項目的ClassLoader,在運行插件的時候加載不了當前項目的類呢?

在maven reference網站上有關於maven類加載機制的說明。

Maven有四種類加載機制:

  1. System Classloader
  2. Core Classloader
  3. Plugin Classloaders
  4. Custom Classloaders

我們關心的是第三種,plugin classloaders。這個類加載器從類加載的層次關系來看是繼承與System classloader 和Core Classloader的,憑想當然的理解在插件goal執行的時候插件的classloader已經包含的project pom 中申明的依賴包。但是,plugin classloader說明中有這么一句話:

Please note that the plugin classloader does neither contain the dependencies of the current project nor its build output. Instead, plugins can query the project's compile, runtime and test class path from the MavenProject in combination with the mojo annotation requiresDependencyResolution from the Mojo API Specification. For instance, flagging a mojo with @requiresDependencyResolution runtime enables it to query the runtime class path of the current project from which it could create further classloaders.

翻譯一下:

請注意,plugin classloader既不包含當前工程的dependencies,也不包含當前工程的輸出目錄。但是,如果你現在插件運行的時候想引用當前工程的編譯(compile)、運行時(runtime)、測試(test)的classpath,可以通過MavenProject 這個組合在成員對象來調用,這個mojo對象需要有“@requiresDependencyResolution”這個annotation

參考該說明,我們知道,maven plugin 不能拿到當前工程的dependencies,也不能包含當前工程的輸出目錄,我們要向加載我們需要的類可以通過兩個方法解決。

解決的第一個方法

在plugin配置的時候為plugin配置節點單獨配置一個dependance

    <build>
        <plugins>
            <plugin>
                <groupId>com.sankuai</groupId>
                <artifactId>athena-nr-maven-plugin</artifactId>
                <version>0.0.2</version>
                <executions>
                    <execution>
                        <!-- 配置compile執行 -->
                        <phase>compile</phase>
                        <goals>
                            <!-- 配置執行目標 -->
                            <goal>reporter</goal>
                        </goals>
                        <configuration>
                            <!-- className是要傳入的值 -->
                            <className>XXX</className>
                        </configuration>
                    </execution>
                </executions>
              	<!-- 配置需要引入的依賴 -->
                <dependencies>
        					<dependency>
            				<groupId>org.apache.httpcomponents</groupId>
            				<artifactId>httpclient</artifactId>
            				<version>4.5.3</version>
        					</dependency>
					    </dependencies>
            </plugin>
        </plugins>
    </build>

但是這樣一來,每次升級Jar包,我還要將依賴的配置升一下級,好煩。

解決的第二個方法

動態讀取目標項目所依賴的classpath並根據這些classpath生成相應的url數組,以這個url數組作為參數得到的類加載器可以實現在maven插件中動態加載目標項目類及第三方引用包的目的。

@Parameter(defaultValue = "${project}", readonly = true, required = true)
private MavenProject project;

在mojo類中引入MavenProject參數,這個參數是目標項目的抽象,僅僅引入這個參數不需要多余的操作即可。

通過MavenProject參數,我們可以調用project的getCompileClasspathElements()拿到路徑的String 列表。

通過這個列表,可以構建URL數組,構建自己的類加載器,該類加載器可以達到實現在maven插件中動態加載目標項目類及第三方引用包的目的。

自定義類加載器篇

首先來復習一下類加載器的一些知識

ClassLoader類加載器

主要的作用是將class文件加載到jvm虛擬機中。jvm啟動的時候,並不是一次性加載所有的類,而是根據需要動態去加載類,主要分為隱式加載和顯示加載。

隱式加載

程序代碼中不通過調用ClassLoader來加載需要的類,而是通過JVM類自動加載需要的類到內存中。例如,當我們在類中繼承或者引用某個類的時候,JVM在解析當前這個類的時,發現引用的類不在內存中,那么就會自動將這些類加載到內存中。

顯式加載

代碼中通過Class.forName()this.getClass.getClassLoader.LoadClass(),自定義類加載器中的findClass()方法等。

jvm自帶的加載器

BootStrap ClassLoader

主要加載%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。可以通過System.getProperty("sun.boot.class.path")查看加載路徑

Extention ClassLoader

主要加載目錄%JRE_HOME%\lib\ext目錄下的jar包和class文件。也可以通過System.out.println(System.getProperty("java.ext.dirs"))查看加載類文件的路徑。

AppClassLoader

主要加載當前應用下的classpath路徑下的類。之前我們在環境變量中配置的classpath就是指定AppClassLoader的類加載路徑。

類加載器的繼承關系

ExtClassLoader,AppClassLoder繼承URLClassLoader,而URLClassLoader繼承ClassLoader,BoopStrap ClassLoder不在上圖中,因為它是由C/C++編寫的,它本身是虛擬機的一部分,並不是一個java類。jvm加載的順序:BoopStrap ClassLoder-〉ExtClassLoader->AppClassLoder

AppClassLoader的父加載器為ExtClassLoader,ExtClassLoader的父加載器為null,BoopStrap ClassLoader為頂級加載器。

demo驗證

package test;

public class Test {
    
    public static void main(String []args){
        
        System.out.println(Test.class.getClassLoader().toString());
        
        System.out.println(Test.class.getClassLoader().getParent().toString());
        
        System.out.println(Test.class.getClassLoader().getParent().getParent().toString());
    }
}

類加載機制

例如:當jvm要加載Test.class的時候

  1. 首先會到自定義加載器中查找,看是否已經加載過,如果已經加載過,則返回字節碼。
  2. 如果自定義加載器沒有加載過,則詢問上一層加載器(即AppClassLoader)是否已經加載過Test.class。
  3. 如果沒有加載過,則詢問上一層加載器(ExtClassLoader)是否已經加載過。
  4. 如果沒有加載過,則繼續詢問上一層加載(BoopStrap ClassLoader)是否已經加載過。
  5. 如果BoopStrap ClassLoader依然沒有加載過,則到自己指定類加載路徑下"sun.boot.class.path"查看是否有Test.class字節碼,有則返回,沒有通知下一層加載器ExtClassLoader到自己指定的類加載路徑下java.ext.dirs查看。
  6. 依次類推,最后到自定義類加載器指定的路徑還沒有找到Test.class字節碼,則拋出異常ClassNotFoundException。

類加載過程

loadClass > findLoadedClass > findClass

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,檢查是否已經加載過
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //父加載器不為空,調用父加載器的loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        //父加載器為空則,調用Bootstrap Classloader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //父加載器沒有找到,則調用findclass
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                //調用resolveClass()
                resolveClass(c);
            }
            return c;
        }
    }

自定義類加載器

/**
 * created by zhangzhiyuan in 2019/8/2
 */
public class EngineClassLoader extends URLClassLoader {
    public EngineClassLoader() {
        this(getSystemClassLoader());
    }

    public EngineClassLoader(ClassLoader parent) {
        super(new URL[] {}, parent);
    }

    public void addURL(URL... urls) {
        if (urls != null) {
            for (URL url : urls) {
                super.addURL(url);
            }
        }
    }

}

加載不到注解問題(第二個坑)

用了該加載器,確實可以通過全類名加載到指定的類,但是又出現了另一個問題。

調用該類是否有某注解的時候,顯示沒有,並且也無法拿到指定注解

boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)

但是在打斷點的時候,是顯示有該注解的,為什么呢?

因為該注解是由我們的自定義類加載器獲得的,和我們調用的類雖然是一個類,但不是一個類

一個類由不同的類加載器實例加載的話,會在方法區產生兩個不同的類,彼此不可見,並且在堆中生成不同Class實例。

無奈只能全程反射了……麻煩的一匹。

參考

https://blog.csdn.net/u012620150/article/details/78652624

https://blog.csdn.net/m0_37635806/article/details/86711423

https://blog.csdn.net/weixin_40318210/article/details/85055133

https://blog.csdn.net/iteye_10738/article/details/81794471

https://blog.csdn.net/imlsz/article/details/51013556

https://blog.csdn.net/tianlihu/article/details/83669738

https://stackoverflow.com/questions/9318935/get-project-build-directory-from-mavenproject

https://stackoverflow.com/questions/13462107/mavenproject-get-the-available-classes-for-use-on-my-plugin

https://stackoverflow.com/questions/35457401/maven-plugin-api-get-mavenproject-from-artifact


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM