說說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了。這又是為啥呢?

