背景
相關(guān)軟件版本:
Eclipse:2020-06(4.16)
JDK:1.8.0_172
Eclipse插件ClassLoader現(xiàn)狀
通常Eclipse的插件的ClassLoader默認(rèn)為org.eclipse.osgi.internal.loader.EquinoxClassLoader,該ClassLoader負(fù)責(zé)Eclipse插件的加載類、加載Resource,在該ClassLoader內(nèi)部,loadClass或者findResource的操作一般都是代理給org.eclipse.osgi.internal.loader.BundleLoader來執(zhí)行的,BundleLoader以一定的順序(Import-Package -> Require-Bundle -> Local等)來加載類或者資源。
需求場景
什么時(shí)候需要自定義插件的ClassLoader呢?
場景
我們的一個(gè)RCP產(chǎn)品里包含了一個(gè)純Java編寫的流程引擎,該引擎包含了許多jar包,作為一個(gè)整體打包成了一個(gè)Eclipse插件供RCP UI層調(diào)用。目前我們想讓這個(gè)流程引擎支持Java SPI方式的擴(kuò)展,擴(kuò)展包和實(shí)現(xiàn)可以由外部用戶自行開發(fā),RCP端支持下載用戶自行開發(fā)的擴(kuò)展,且這些下載安裝后的擴(kuò)展可以被內(nèi)部的流程引擎插件發(fā)現(xiàn)及加載。
問題及難點(diǎn)
問題
在OSGi環(huán)境中SPI Consumer(也就是調(diào)用java的ServiceLoader.load()方法的一方)如何發(fā)現(xiàn)SPI擴(kuò)展?
OSGi Service Loader Mediator & spifly
純Java的SPI擴(kuò)展也是一個(gè)純的jar包,但是RCP只能支持安裝Eclipse插件,這就需要一個(gè)自動(dòng)工具將Java SPI擴(kuò)展編譯打包為Eclipse插件,BND Tools可以做到這一點(diǎn)。
但是這樣的話就會(huì)面臨另一個(gè)問題,SPI擴(kuò)展插件如何被流程引擎插件發(fā)現(xiàn)其中的擴(kuò)展。眾所周知,Eclipse插件的擴(kuò)展發(fā)現(xiàn)機(jī)制是主要是通過plugin.xml文件來實(shí)現(xiàn),而Java的SPI擴(kuò)展發(fā)現(xiàn)機(jī)制是通過META-INF/services來實(shí)現(xiàn)的,同時(shí)要求實(shí)現(xiàn)包需要和API包在同一個(gè)ClassLoader內(nèi),否則就不能發(fā)現(xiàn)擴(kuò)展的文件。Eclipse每個(gè)插件都有自己的ClassLoader,通常情況下,一個(gè)插件的ClassLoader只負(fù)責(zé)加載自己插件內(nèi)的類和資源,以及所依賴的插件和導(dǎo)入的包中的類和資源。Java SPI擴(kuò)展文件不屬于代碼文件也沒有被Export,所以SPI擴(kuò)展實(shí)現(xiàn)插件中的文件資源是不會(huì)被API所在的插件所發(fā)現(xiàn)的,也就意味說在OSGi環(huán)境下,SPI的Consumer是不能發(fā)現(xiàn)其他插件中的SPI實(shí)現(xiàn)的。
為了在OSGi的bundle中可以使用Java SPI機(jī)制,OSGi規(guī)范中提供了一種稱為Service Loader Mediator的支持方案,通過在bundle的MANIFEST.MF中添加Require-Capability和Provide-Capability來達(dá)到定義API和暴露SPI實(shí)現(xiàn)的目的。詳情可以參考OSGi 5以上規(guī)范的第133章節(jié)。
OSGi的“Service Loader Mediator”只是一種規(guī)范,還需要有該規(guī)范的實(shí)現(xiàn)才能真正使用。apache提供了一種實(shí)現(xiàn)spifly。spifly提供了靜態(tài)和動(dòng)態(tài)兩種模式,靜態(tài)模式就是將擴(kuò)展插件預(yù)先轉(zhuǎn)換為spifly支持的擴(kuò)展插件然后再安裝到RCP中;動(dòng)態(tài)模式就是spifly提供了一個(gè)動(dòng)態(tài)的bundle,它在啟動(dòng)時(shí)會(huì)自動(dòng)發(fā)現(xiàn)當(dāng)前OSGi容器中的擴(kuò)展插件并通過字節(jié)碼增強(qiáng)的方式自動(dòng)轉(zhuǎn)換。
spifly除了支持OSGi的Service Loader規(guī)范外,也提供了一種spifly自己支持的方案:SPI-Provider和SPI-Consumer。與OSGi的支持方案相比,OSGi的方案通用性更好,只要是OSGi規(guī)范的實(shí)現(xiàn)者都會(huì)支持這種方式;spifly的方案更簡單易用,但只能用于spifly,可移植性稍差。
spifly參考資源:SPI Fly :: Apache Aries
使用spifly的話需要遵循以下原則或配置:(以下步驟以動(dòng)態(tài)模式的spifly方案為例)
- 安裝spifly dynamic-bundle,這是一個(gè)OSGi Bundle,安裝后需要將啟動(dòng)級別設(shè)置為低于默認(rèn)啟動(dòng)級別的值,autostart設(shè)置成true,表示其要自動(dòng)啟動(dòng)。
- 提供SPI 實(shí)現(xiàn)的插件,需要在MANIFEST.MF中增加一個(gè)header:SPI-Provider,值為擴(kuò)展實(shí)現(xiàn)的接口的全限定名,或者“*”表示所有擴(kuò)展實(shí)現(xiàn)的接口。
- 使用SPI擴(kuò)展的插件,需要在MANIFEST.MF中增加一個(gè)header:SPI-Consumer,值為*或者使用的ServiceLoader及其方法。
- 安裝SPI實(shí)現(xiàn)或consumer插件后,需要將這些插件的autostart設(shè)為true,因?yàn)閟pifly只會(huì)從active的插件中檢測擴(kuò)展。
總的來說,spifly可以滿足加載SPI擴(kuò)展的基本使用要求。但是也有一些缺點(diǎn):
- 使用較繁瑣,特別是在我們這種使用場景下,插件都是通過工具來自動(dòng)打包生成的,那么在什么時(shí)候添加SPI header就是一個(gè)比較困難的事情。
- 使用場景有限,例如如果在API代碼中需要加載實(shí)現(xiàn)中提供的資源文件,換句話說就是擴(kuò)展是通過資源文件的方式來提供的,而SPI的ServiceLoader只能加載到擴(kuò)展類實(shí)現(xiàn),那這種場景spifly就無法滿足了。
現(xiàn)在回過頭來再看一下上面的場景,我們可以發(fā)現(xiàn)其實(shí)只要在OSGi插件中,還能像Java SPI程序中那樣,API和實(shí)現(xiàn)包都在一個(gè)ClassLoader中,那么這些問題就迎刃而解,并且在OSGi中SPI程序的表現(xiàn)跟在OSGi外面運(yùn)行的表現(xiàn)一樣。
但是Eclipse插件的ClassLoader都是默認(rèn)創(chuàng)建的,能不能為插件指定ClassLoader呢?經(jīng)過各種調(diào)研,我發(fā)現(xiàn)答案是肯定的。
自定義插件的ClassLoader
想要自定義插件的ClassLoader,需要通過OSGi提供的擴(kuò)展機(jī)制。
關(guān)于OSGi擴(kuò)展的詳情可以參考OSGi規(guī)范中的定義:http://docs.osgi.org/specification/osgi.core/7.0.0/framework.module.html#framework.module.extensionbundles
這里我們只講如何來做。
先說明我們要達(dá)到的效果:
- SPI API打包為Eclipse 插件。
- SPI擴(kuò)展包放到RCP產(chǎn)品的根目錄下的“addons”文件夾中。
- SPI API插件在加載類和資源時(shí),除了可以加載本插件內(nèi)的,還可以加載產(chǎn)品根目錄下的“addons”文件夾中的jar中的資源。
實(shí)現(xiàn)步驟:
- 創(chuàng)建一個(gè)
fragment工程,名稱我們假定為com.ming.osgi.hook,Host Plugin選擇“org.eclipse.osgi”,也可以使用該插件的別名“system.bundle”,效果是一樣的。 - 創(chuàng)建類
AddonsClassLoader繼承自java.net.URLClassLoader,負(fù)責(zé)從“addons”文件夾中加載jar及加載jar中的資源。 - 創(chuàng)建類
TestModuleClassLoader繼承自org.eclipse.osgi.internal.loader.EquinoxClassLoader,保持在加載插件中的類和資源時(shí)和原來一樣的表現(xiàn)。覆寫findLocalClass、findLocalResource、findLocalResources方法,修改方法邏輯為在parent中加載不到資源時(shí),嘗試從我們的AddonsClassLoader中加載。
@Override
public Class<?> findLocalClass(String classname) throws ClassNotFoundException {
try {
return super.findLocalClass(classname);
} catch (ClassNotFoundException e) {
return externalClassLoader.findClass(classname);
}
}
@Override
public URL findLocalResource(String resource) {
URL result = super.findLocalResource(resource);
if (result == null) {
result = externalClassLoader.findResource(resource);
}
return result;
}
@Override
public Enumeration<URL> findLocalResources(String resource) {
Enumeration<URL> result = super.findLocalResources(resource);
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
tmp[0] = result;
try {
tmp[1] = externalClassLoader.findResources(resource);
} catch (IOException e) {
e.printStackTrace();
}
return new CombinedEnumeration<URL>(tmp);
}
- 創(chuàng)建類
TestClassLoaderHook繼承org.eclipse.osgi.internal.hookregistry.ClassLoaderHook,該抽象類中有很多hook方法,我們只關(guān)心createClassLoader()方法。覆寫該方法,可以根據(jù)插件id或其他信息來為特定插件創(chuàng)建我們上面自定義的ModuleClassLoader了。
@Override
public ModuleClassLoader createClassLoader(ClassLoader parent, EquinoxConfiguration configuration,
BundleLoader delegate, Generation generation) {
String externalClasspath = generation.getHeaders().get("External-ClassPath");
if (externalClasspath != null) {
externalClasspath = externalClasspath.trim();
if (!externalClasspath.isEmpty()) {
System.out
.println("use external classloader for " + delegate.getWiring().getBundle().getSymbolicName());
Set<String> classpathSet = new HashSet<String>();
String[] classpaths = externalClasspath.split(",");
for (String classpath : classpaths) {
classpathSet.add(classpath.trim());
}
return new TestModuleClassLoader(parent, configuration, delegate, generation,
classpathSet.toArray(new String[classpathSet.size()]));
}
}
return super.createClassLoader(parent, configuration, delegate, generation);
}
- 創(chuàng)建類
TestHookConfigurator,實(shí)現(xiàn)org.eclipse.osgi.internal.hookregistry.HookConfigurator接口。覆寫addHooks,用來向HookRegistry添加我們自定義的ClassLoaderHook。
@Override
public void addHooks(HookRegistry hookRegistry) {
hookRegistry.addClassLoaderHook(new TestClassLoaderHook());
}
- 到現(xiàn)在為止,需要的類我們都創(chuàng)建完了,現(xiàn)在我們需要讓osgi能發(fā)現(xiàn)我們的
TestHookConfigurator擴(kuò)展。
在fragment工程根目錄下創(chuàng)建hookconfigurators.properties文件,在其中添加如下內(nèi)容:
hook.configurators=com.ming.osgi.hook.TestHookConfigurator
- 最后,如果需要在Eclipse內(nèi)調(diào)試,則需要在啟動(dòng)配置中添加一個(gè)JVM參數(shù)“-Dosgi.framework.extensions=com.ming.osgi.hook”,讓osgi從我們的fragment中加載擴(kuò)展。
發(fā)布時(shí)只需將fragment插件添加到build列表中,或者加到product的插件列表中,打包程序會(huì)自動(dòng)在生成的config.ini中添加osgi.framework.extensions的配置。
其實(shí)我們上面的ClassLoaderHook實(shí)現(xiàn)中是采用了一種更好的方案,就是在要自定義ClassLoader的插件的MANIFEST.MF中添加一個(gè)自定義的headerExternal-ClassPath,值為一個(gè)外部的目錄(可以是addons,也可以是其他),這樣不同的插件可以使用各自的擴(kuò)展,達(dá)到隔離的一個(gè)目的。