自定義Eclipse插件的ClassLoader

背景

相關(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-CapabilityProvide-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方案為例)

  1. 安裝spifly dynamic-bundle,這是一個(gè)OSGi Bundle,安裝后需要將啟動(dòng)級別設(shè)置為低于默認(rèn)啟動(dòng)級別的值,autostart設(shè)置成true,表示其要自動(dòng)啟動(dòng)。
  2. 提供SPI 實(shí)現(xiàn)的插件,需要在MANIFEST.MF中增加一個(gè)header:SPI-Provider,值為擴(kuò)展實(shí)現(xiàn)的接口的全限定名,或者“*”表示所有擴(kuò)展實(shí)現(xiàn)的接口。
  3. 使用SPI擴(kuò)展的插件,需要在MANIFEST.MF中增加一個(gè)header:SPI-Consumer,值為*或者使用的ServiceLoader及其方法。
  4. 安裝SPI實(shí)現(xiàn)或consumer插件后,需要將這些插件的autostart設(shè)為true,因?yàn)閟pifly只會(huì)從active的插件中檢測擴(kuò)展。

總的來說,spifly可以滿足加載SPI擴(kuò)展的基本使用要求。但是也有一些缺點(diǎn):

  1. 使用較繁瑣,特別是在我們這種使用場景下,插件都是通過工具來自動(dòng)打包生成的,那么在什么時(shí)候添加SPI header就是一個(gè)比較困難的事情。
  2. 使用場景有限,例如如果在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á)到的效果:

  1. SPI API打包為Eclipse 插件。
  2. SPI擴(kuò)展包放到RCP產(chǎn)品的根目錄下的“addons”文件夾中。
  3. SPI API插件在加載類和資源時(shí),除了可以加載本插件內(nèi)的,還可以加載產(chǎn)品根目錄下的“addons”文件夾中的jar中的資源。

實(shí)現(xiàn)步驟:

  1. 創(chuàng)建一個(gè)fragment工程,名稱我們假定為com.ming.osgi.hook,Host Plugin選擇“org.eclipse.osgi”,也可以使用該插件的別名“system.bundle”,效果是一樣的。
  2. 創(chuàng)建類AddonsClassLoader繼承自java.net.URLClassLoader,負(fù)責(zé)從“addons”文件夾中加載jar及加載jar中的資源。
  3. 創(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);
}
  1. 創(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);
}
  1. 創(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());
}
  1. 到現(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
  1. 最后,如果需要在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è)目的。

參考資料

  1. SPI Fly :: Apache Aries
  2. Instrumenting OSGi Bundles Through Equinox Adaptor Hooks
  3. OSGi規(guī)范中關(guān)于extension的說明
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容