【曹工雜談】說說Maven框架和外掛的契約

三國夢迴發表於2021-09-09

說說Maven框架和外掛的契約

前言

Maven框架就像現在公司內的各種平臺方,規定一些契約,然後想辦法拉動業務方,一起在這個平臺上去做生態共建。Maven也是這樣,其實它就是一個外掛執行的框架,Maven剛開始肯定不知道會有誰去貢獻外掛,外掛如果寫得五花八門的話,那對於平臺方來說,可能就是一個災難,所以,平臺方就要負責定標準,要在我平臺上寫外掛,必須怎麼怎麼樣。

Maven給外掛就定了契約,這個契約,是通過api jar包的方式。每次釋出Maven新版本,與之伴隨的,都會有一個api jar包。

如果有人要基於這個版本的api jar包來開發外掛,就需要把這個外掛引入到自己的外掛工程中。然後根據api jar包中的契約介面,來實現自己的外掛邏輯。

比如,maven clean外掛的工程程式碼中,就依賴了api jar包。如下:

api jar包中的契約介面長啥樣呢?

public interface Mojo
{
	...
    void execute()
        throws MojoExecutionException, MojoFailureException;
}

核心方法就是這個,只要你實現這個介面就完事了。

作為框架方,怎麼去呼叫這個外掛呢?簡而言之,就是:

1、找到外掛的實現類jar包,然後構造一個該外掛的類載入器,去載入這個jar包,然後找到對應的實現了契約介面的類,比如這裡的CleanMojo

2、載入了這個CleanMojo的class之後,當然是反射生成物件,然後強制轉換為契約介面,然後呼叫契約介面就行。比如:

Class cleanMojoClass = 外掛的類載入器載入外掛的jar包;
Mojo cleanMojo = (Mojo)cleanMojoClass.newInstance();
cleanMojo.execute();

到此為止,我們的理論知識已經足夠了,我們是不是可以show the code了?

工程實踐

我們會模擬上面的過程,

  1. 建一個Maven module,用來存放外掛api契約介面;
  2. 建一個Maven module,引入api,實現外掛api,這樣,我們的外掛就算是實現好了;
  3. 接下來,把這兩個工程編譯一下,把jar包安裝到本地倉庫;
  4. 再新建一個工程,模擬Maven框架去載入外掛,並執行外掛。

外掛api工程

直接用maven的archetype中的quickstart,新建一個module,裡面很簡單,就一個介面:

然後執行mvn install,安裝到本地倉庫。

外掛實現工程

在pom中,我們會引入api。

  <dependencies>
    <dependency>
      <groupId>org.example</groupId>
      <artifactId>my-plugin-api</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>
  </dependencies>

程式碼也很簡單,就一個實現類。

然後執行mvn install,安裝到本地倉庫。

主工程,模擬框架去呼叫外掛

主工程就是模擬我們的Maven框架,由於我們呼叫外掛,肯定是通過api的方式,所以,pom中肯定是要引入api的。

  <dependencies>
    <dependency>
      <groupId>org.example</groupId>
      <artifactId>my-plugin-api</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>
  </dependencies>

接下來,我們寫了個測試類:

public static void main( String[] args ) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
    	// 1.1處
        URL urlForPluginApi = new URL("file:/C:\\Users\\Administrator\\.m2\\repository\\org\\example\\my-plugin-api\\1.0-SNAPSHOT\\my-plugin-api-1.0-SNAPSHOT.jar");
        URL urlForPluginImpl = new URL("file:/C:\\Users\\Administrator\\.m2\\repository\\org\\example\\my-plugin-implementation\\1.0-SNAPSHOT\\my-plugin-implementation-1.0-SNAPSHOT.jar");
        URL[] urls = {urlForPluginApi, urlForPluginImpl};
    
    	// 1.2
        URLClassLoader urlClassLoader = new URLClassLoader(urls,ClassLoader.getSystemClassLoader()){
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try{
                    // 保證:尋找類時,優先查詢自己的classpath,找不到,再去交給parent classloader
                    Class<?> clazz = findClass(name);
                    return clazz;
                }catch (ClassNotFoundException exception ){
                    return super.loadClass(name);
                }
            }
        };
        
    	// 1.3
        Class<?> implClazzByPluginClassloader = urlClassLoader.loadClass("org.example.MyMojoImplementation");
    	// 1.4 
        MojoInterface mojoInterface = (MojoInterface) implClazzByPluginClassloader.newInstance();
    	// 1.5 
        mojoInterface.execute();

        System.out.println( "Hello World!" );
    }

我先大概講解一下上述程式碼:

  • 1.1處,構造了兩個url,分別指向我本地倉庫的兩個檔案,也就是api.jar和外掛對應的實現的jar

  • 1.2處,使用1.1中的url,構造了一個classloader,這個classloader的parent classloader,我們傳的是,系統的AppClassloader。

    同時,我們重寫了這個classloader的行為,重寫後的行為如下:遇到要載入的類時,自己優先載入,也就是會去自己的兩個url裡面找,看看能不能找到,如果找不到,就會進入異常,異常被我們捕獲後,交給parent classloader去載入;

  • 1.3處,我們用新建的classloader,去載入了外掛的實現類

  • 1.4處,利用1.3處載入的實現類的class,反射生成物件,強轉為MojoInterface介面物件

  • 1.5處,多型方式執行外掛邏輯

大家不妨思考下,大家覺得,最終的執行結果是啥?我們的“hello world”能列印出來嗎?

這個程式碼,我們上傳了gitee,大家可以拉下來看。

https://gitee.com/ckl111/maven-3.8.1-source-learn

我這邊給大家展示下,執行結果:

大家看看,這像話嗎,明明我的外掛程式碼裡,是實現了介面的,怎麼就不能向上轉型呢?:

public class MyMojoImplementation implements MojoInterface{

    @Override
    public void execute() {
        System.out.println("implementation execute business logic");
    }
}

這個。。。怎麼說呢。。。這麼跟你解釋吧,我們載入MyMojoImplementation時,發現這個類吧,還實現了介面MojoInterface,那麼,這個介面類也就需要載入,因為我們classloader進行了改寫(優先由自己進行載入),因此,最終呢,MojoInterface也就和MyMojoImplementation一樣,都是由外掛類載入器去載入的。

最終呢,在向上轉型時,會出現下邊這個情況,兩邊不匹配,就報錯了。

        MojoInterface(框架中的這個類,是由框架的類載入器載入的) mojoInterface = (MojoInterface) implClazzByPluginClassloader.newInstance();(這個實現類實現的介面,是由外掛類載入器載入的)

課後題

我們對程式碼進行了修改,改成了如下的樣子,結果,就可以跑通我們的hello world了。這又是為啥呢?

相關文章