說說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了?
工程實踐
我們會模擬上面的過程,
- 建一個Maven module,用來存放外掛api契約介面;
- 建一個Maven module,引入api,實現外掛api,這樣,我們的外掛就算是實現好了;
- 接下來,把這兩個工程編譯一下,把jar包安裝到本地倉庫;
- 再新建一個工程,模擬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了。這又是為啥呢?