自定义Eclipse插件的ClassLoader

背景

相关软件版本:
Eclipse:2020-06(4.16)
JDK:1.8.0_172

Eclipse插件ClassLoader现状

通常Eclipse的插件的ClassLoader默认为org.eclipse.osgi.internal.loader.EquinoxClassLoader,该ClassLoader负责Eclipse插件的加载类、加载Resource,在该ClassLoader内部,loadClass或者findResource的操作一般都是代理给org.eclipse.osgi.internal.loader.BundleLoader来执行的,BundleLoader以一定的顺序(Import-Package -> Require-Bundle -> Local等)来加载类或者资源。

需求场景

什么时候需要自定义插件的ClassLoader呢?

场景

我们的一个RCP产品里包含了一个纯Java编写的流程引擎,该引擎包含了许多jar包,作为一个整体打包成了一个Eclipse插件供RCP UI层调用。目前我们想让这个流程引擎支持Java SPI方式的扩展,扩展包和实现可以由外部用户自行开发,RCP端支持下载用户自行开发的扩展,且这些下载安装后的扩展可以被内部的流程引擎插件发现及加载。

问题及难点

问题

在OSGi环境中SPI Consumer(也就是调用java的ServiceLoader.load()方法的一方)如何发现SPI扩展?

OSGi Service Loader Mediator & spifly

纯Java的SPI扩展也是一个纯的jar包,但是RCP只能支持安装Eclipse插件,这就需要一个自动工具将Java SPI扩展编译打包为Eclipse插件,BND Tools可以做到这一点。
但是这样的话就会面临另一个问题,SPI扩展插件如何被流程引擎插件发现其中的扩展。众所周知,Eclipse插件的扩展发现机制是主要是通过plugin.xml文件来实现,而Java的SPI扩展发现机制是通过META-INF/services来实现的,同时要求实现包需要和API包在同一个ClassLoader内,否则就不能发现扩展的文件。Eclipse每个插件都有自己的ClassLoader,通常情况下,一个插件的ClassLoader只负责加载自己插件内的类和资源,以及所依赖的插件和导入的包中的类和资源。Java SPI扩展文件不属于代码文件也没有被Export,所以SPI扩展实现插件中的文件资源是不会被API所在的插件所发现的,也就意味说在OSGi环境下,SPI的Consumer是不能发现其他插件中的SPI实现的。

为了在OSGi的bundle中可以使用Java SPI机制,OSGi规范中提供了一种称为Service Loader Mediator的支持方案,通过在bundle的MANIFEST.MF中添加Require-CapabilityProvide-Capability来达到定义API和暴露SPI实现的目的。详情可以参考OSGi 5以上规范的第133章节。

OSGi的“Service Loader Mediator”只是一种规范,还需要有该规范的实现才能真正使用。apache提供了一种实现spifly。spifly提供了静态和动态两种模式,静态模式就是将扩展插件预先转换为spifly支持的扩展插件然后再安装到RCP中;动态模式就是spifly提供了一个动态的bundle,它在启动时会自动发现当前OSGi容器中的扩展插件并通过字节码增强的方式自动转换。
spifly除了支持OSGi的Service Loader规范外,也提供了一种spifly自己支持的方案:SPI-Provider和SPI-Consumer。与OSGi的支持方案相比,OSGi的方案通用性更好,只要是OSGi规范的实现者都会支持这种方式;spifly的方案更简单易用,但只能用于spifly,可移植性稍差。
spifly参考资源:SPI Fly :: Apache Aries

使用spifly的话需要遵循以下原则或配置:(以下步骤以动态模式的spifly方案为例)

  1. 安装spifly dynamic-bundle,这是一个OSGi Bundle,安装后需要将启动级别设置为低于默认启动级别的值,autostart设置成true,表示其要自动启动。
  2. 提供SPI 实现的插件,需要在MANIFEST.MF中增加一个header:SPI-Provider,值为扩展实现的接口的全限定名,或者“*”表示所有扩展实现的接口。
  3. 使用SPI扩展的插件,需要在MANIFEST.MF中增加一个header:SPI-Consumer,值为*或者使用的ServiceLoader及其方法。
  4. 安装SPI实现或consumer插件后,需要将这些插件的autostart设为true,因为spifly只会从active的插件中检测扩展。

总的来说,spifly可以满足加载SPI扩展的基本使用要求。但是也有一些缺点:

  1. 使用较繁琐,特别是在我们这种使用场景下,插件都是通过工具来自动打包生成的,那么在什么时候添加SPI header就是一个比较困难的事情。
  2. 使用场景有限,例如如果在API代码中需要加载实现中提供的资源文件,换句话说就是扩展是通过资源文件的方式来提供的,而SPI的ServiceLoader只能加载到扩展类实现,那这种场景spifly就无法满足了。

现在回过头来再看一下上面的场景,我们可以发现其实只要在OSGi插件中,还能像Java SPI程序中那样,API和实现包都在一个ClassLoader中,那么这些问题就迎刃而解,并且在OSGi中SPI程序的表现跟在OSGi外面运行的表现一样。

但是Eclipse插件的ClassLoader都是默认创建的,能不能为插件指定ClassLoader呢?经过各种调研,我发现答案是肯定的。

自定义插件的ClassLoader

想要自定义插件的ClassLoader,需要通过OSGi提供的扩展机制。
关于OSGi扩展的详情可以参考OSGi规范中的定义:http://docs.osgi.org/specification/osgi.core/7.0.0/framework.module.html#framework.module.extensionbundles

这里我们只讲如何来做。
先说明我们要达到的效果:

  1. SPI API打包为Eclipse 插件。
  2. SPI扩展包放到RCP产品的根目录下的“addons”文件夹中。
  3. SPI API插件在加载类和资源时,除了可以加载本插件内的,还可以加载产品根目录下的“addons”文件夹中的jar中的资源。

实现步骤:

  1. 创建一个fragment工程,名称我们假定为com.ming.osgi.hook,Host Plugin选择“org.eclipse.osgi”,也可以使用该插件的别名“system.bundle”,效果是一样的。
  2. 创建类AddonsClassLoader继承自java.net.URLClassLoader,负责从“addons”文件夹中加载jar及加载jar中的资源。
  3. 创建类TestModuleClassLoader继承自org.eclipse.osgi.internal.loader.EquinoxClassLoader,保持在加载插件中的类和资源时和原来一样的表现。覆写findLocalClassfindLocalResourcefindLocalResources方法,修改方法逻辑为在parent中加载不到资源时,尝试从我们的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 findLocalResources(String resource) {
    Enumeration result = super.findLocalResources(resource);

    Enumeration[] tmp = (Enumeration[]) new Enumeration[2];
    tmp[0] = result;
    try {
        tmp[1] = externalClassLoader.findResources(resource);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return new CombinedEnumeration(tmp);
}
  1. 创建类TestClassLoaderHook继承org.eclipse.osgi.internal.hookregistry.ClassLoaderHook,该抽象类中有很多hook方法,我们只关心createClassLoader()方法。覆写该方法,可以根据插件id或其他信息来为特定插件创建我们上面自定义的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 classpathSet = new HashSet();
            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. 创建类TestHookConfigurator,实现org.eclipse.osgi.internal.hookregistry.HookConfigurator接口。覆写addHooks,用来向HookRegistry添加我们自定义的ClassLoaderHook。
@Override
public void addHooks(HookRegistry hookRegistry) {
    hookRegistry.addClassLoaderHook(new TestClassLoaderHook());
}
  1. 到现在为止,需要的类我们都创建完了,现在我们需要让osgi能发现我们的TestHookConfigurator扩展。
    在fragment工程根目录下创建hookconfigurators.properties文件,在其中添加如下内容:
hook.configurators=com.ming.osgi.hook.TestHookConfigurator
  1. 最后,如果需要在Eclipse内调试,则需要在启动配置中添加一个JVM参数“-Dosgi.framework.extensions=com.ming.osgi.hook”,让osgi从我们的fragment中加载扩展。
    发布时只需将fragment插件添加到build列表中,或者加到product的插件列表中,打包程序会自动在生成的config.ini中添加osgi.framework.extensions的配置。

其实我们上面的ClassLoaderHook实现中是采用了一种更好的方案,就是在要自定义ClassLoader的插件的MANIFEST.MF中添加一个自定义的headerExternal-ClassPath,值为一个外部的目录(可以是addons,也可以是其他),这样不同的插件可以使用各自的扩展,达到隔离的一个目的。

参考资料

  1. SPI Fly :: Apache Aries
  2. Instrumenting OSGi Bundles Through Equinox Adaptor Hooks
  3. OSGi规范中关于extension的说明

你可能感兴趣的:(自定义Eclipse插件的ClassLoader)