Java SPI机制实现插件化扩展功能

版权声明:转载请注明出处 https://blog.csdn.net/javeme/article/details/82790406

Java SPI机制实现插件化扩展功能

1.背景

我们有一个图数据库的服务,用户希望在不修改现有源代码的情况下扩展自定义的分词器,达到可插件式扩展功能的目标。

通过Java的SPI机制实现插件式的扩展功能还是比较简便的,下面分主程序部分和插件实现2部分来说明。

特别的,在实现过程中遇到一个比较怪异的问题:ServiceLoader.load()时抛出NoClassDefFoundError异常,经过Google及StackOverflow都没能找到原因,问题表现与这几个链接中描述的类似:serviceloader-issue-in-jettyserviceloader-in-glassfish4-java-ee-appserviceloader-next-causing-a-noclassdeffounderror
文末会记录一下这个问题的解决过程及原因分析。

2.SPI插件实现要素

主程序部分主要包括:

  1. 定义插件接口
  2. 加载插件实现的Jar包
  3. 加载插件实现类对象

插件实现部分主要包括:

  1. 实现插件接口
  2. 配置SPI入口
  3. 打Jar包

3.实现插件化的流程

下面以扩展一个分词器实例来说明插件化的流程。

  1. 定义接口
    定义接口com.baidu.hugegraph.plugin.HugeGraphPlugin,内容如下:
    public interface HugeGraphPlugin {
      public String name();
      public void register();
      public String supportsMinVersion();
      public String supportsMaxVersion();
    }
    
  2. 加载插件实现的Jar包
    参考SPI官方文档,我们定义了一个目录plugins来存放插件的Jar包,在启动Java主程序服务时通过参数-Djava.ext.dirs=plugins指定插件Jar包的目录。当需要扩展新的插件时,只需要把插件Jar包拷贝到plugins目录下,重启主程序服务即可生效。完整的启动命令示例:
    java -Djava.ext.dirs=plugins -Dname="HugeGraphServer" ${JAVA_OPTIONS} -cp ${CP}:${CLASSPATH} com.baidu.hugegraph.dist.HugeGraphServer ${APP_ARGS}
    
  3. 加载插件实现类实例
    在主程序中,我们通过ServiceLoader来加载所有插件实例。
    private static void registerPlugins() {
      LOG.info("Loading plugins...");
      ServiceLoader<HugeGraphPlugin> plugins = ServiceLoader.load(HugeGraphPlugin.class);
      for (HugeGraphPlugin plugin : plugins) {
        LOG.info("Loading plugin {}({})",
                 plugin.name(), plugin.getClass().getCanonicalName());
        try {
          plugin.register();
          LOG.info("Loaded plugin {}", plugin.name());
        } catch (Exception e) {
          throw new HugeException("Failed to load plugin '%s'",
                                  plugin.name(), e);
        }
      }
    }
    
  4. 实现插件接口,并注册自定义分词器
    新建一个project来实现自定义的分词器,命名为hugegraph-plugin-demo
    这里简单的实现一个以空格来切分词语的分词器。
    package com.baidu.hugegraph.plugin;
    import java.util.Arrays;
    import java.util.HashSet;
    import java.util.Set;
    import com.baidu.hugegraph.analyzer.Analyzer;
    public class SpaceAnalyzer implements Analyzer {
        @Override
        public Set<String> segment(String text) {
            return new HashSet<>(Arrays.asList(text.split(" ")));
        }
    }
    
    实现插件接口HugeGraphPlugin.register(),并把自定义好的分词器注册到主程序中去。
    package com.baidu.hugegraph.plugin;
    public class DemoPlugin implements HugeGraphPlugin {
        @Override
        public String name() {
            return "demo";
        }
        @Override
        public void register() {
            HugeGraphPlugin.registerAnalyzer("demo", SpaceAnalyzer.class.getName());
        }
    }
    
  5. 配置SPI入口
    1. 确保services目录存在:hugegraph-plugin-demo/resources/META-INF/services
    2. 在services目录下建立文本文件:com.baidu.hugegraph.plugin.HugeGraphPlugin
    3. 文件内容如下:com.baidu.hugegraph.plugin.DemoPlugin
  6. 打Jar包
    通过IDE或maven等工具将实现的插件打成Jar包,并且拷贝到主程序的plugins目录,重启主程序即可生效。

4.异常NoClassDefFoundError分析

4.1 问题表现

在实现过程中,遇到一个NoClassDefFoundError问题,在ServiceLoader加载插件时提示找不到插件接口定义类HugeGraphPlugin,异常栈如下:

java.lang.NoClassDefFoundError: com/baidu/hugegraph/plugin/HugeGraphPlugin
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:411)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:348)
	at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:370)
	at java.util.ServiceLoader$LazyIterator.next(ServiceLoader.java:404)
	at java.util.ServiceLoader$1.next(ServiceLoader.java:480)
	at com.baidu.hugegraph.dist.HugeGraphServer.registerPlugins(HugeGraphServer.java:62)
	at com.baidu.hugegraph.dist.HugeGraphServer.main(HugeGraphServer.java:44)
Caused by: java.lang.ClassNotFoundException: com.baidu.hugegraph.plugin.HugeGraphPlugin
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 20 more
4.2 问题分析

根据错误信息从网上搜索,并没有发现根本解决方法。初步分析觉得跟类加载器ClassLoader有关,因为本身HugeGraphPlugin类是明显定义了的。

注意ServiceLoader.load()有一点比较特殊的地方,它的类加载器是Thread Context ClassLoader,关于类加载器的介绍可参考Java Classloader详解

  1. 判断类是否真的没有定义?
    分析发现,只是通过ServiceLoader加载DemoPlugin类时才报这个错误(DemoPlugin implements HugeGraphPlugin),如果将DemoPlugin与主程序放在同一个项目中是没问题的。也就是说代码本身是正确的,只是因为以插件方式加载才导致了问题。

  2. 判断ServiceLoader是否使用了Context ClassLoader
    经过调试发现ServiceLoader中使用的类加载器确实是通过Thread.currentThread().getContextClassLoader()方法获取的,并且和主程序中的AppClassLoader是同一个实例。

    扫描二维码关注公众号,回复: 3408655 查看本文章
  3. 判断是否在加载DemoPlugin类时HugeGraphPlugin类的Jar包还没有被载入?
    这个假设是在遇到问题比较迷惑的时候才会提出来的(当时甚至怀疑SPI官方文档是不是写错了),事实上,通过Java参数-verbose:class打印类加载信息,在错误发生之前HugeGraphPlugin类就已经被加载进来了。

  4. 判断是否循环依赖导致?
    插件中DemoPlugin类依赖来自主程序的HugeGraphPlugin类,加载插件时主程序又依赖插件中的DemoPlugin类,难道是循环依赖导致的?于是将HugeGraphPlugin类拆分到单独Jar包中,主程序和插件分别依赖该独立Jar包,不过结果还是同样的错误。

  5. ClassLoader类加载机制导致?
    综合第2点和第3点结果分析,会更加发现问题的诡异之处,主程序和插件使用的是同一个ClassLoader来加载我们定义的类,而且HugeGraphPlugin类明明已经被加载了的,那为何加载DemoPlugin类时还报错找不到HugeGraphPlugin类?

    结合ClassLoader相关源码分析发现,AppClassLoader在加载DemoPlugin类时,需要委托给双亲ExtClassLoader来加载(因为插件的Jar包配置在java.ext.dirs路径下),而DemoPlugin类继承自HugeGraphPlugin类,ExtClassLoader又需要拿到或加载HugeGraphPlugin类,但是HugeGraphPlugin所属的Jar包不在ext路径下从而找不到HugeGraphPlugin(事实上它在AppClassLoader里面,ExtClassLoader只会加载lib/ext目录和java.ext.dirs目录)。

    总结一下,就是配置了DemoPlugin Jar包到ext,而插件Jar包所依赖的HugeGraphPlugin Jar包在classpath下,导致父加载器ExtClassLoader无法找到属于子加载器AppClassLoader所负责的类。

    下面是ClassLoader.loadClass()源码:

    // java.lang.ClassLoader.loadClass()
    protected Class<?> loadClass(String name, boolean resolve)
              throws ClassNotFoundException
    {
      synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
          long t0 = System.nanoTime();
          try {
            // 双亲委派机制,DemoPlugin就是在这里被AppClassLoader委派给ExtClassLoader的。
            if (parent != null) {
              c = parent.loadClass(name, false);
            } else {
              c = findBootstrapClassOrNull(name);
            }
          } catch (ClassNotFoundException e) {
            // ClassNotFoundException thrown if class not found
            // from the non-null parent class loader
          }
          if (c == null) {
            // If still not found, then invoke findClass in order
            // to find the class.
            long t1 = System.nanoTime();
            c = findClass(name);
            // this is the defining class loader; record the stats
            sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
            sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
            sun.misc.PerfCounter.getFindClasses().increment();
          }
        }
        if (resolve) {
          resolveClass(c);
        }
        return c;
      }
    }
    
4.3 解决方法

问题根源找到了,解决方法就很简单了,归根到底有2种方法,选择其中一种即可:

  • 将DemoPlugin Jar包以及它依赖的所有Jar包都放在java.ext.dirs下。
  • 将DemoPlugin Jar包放在classpath下。

<–end–>

猜你喜欢

转载自blog.csdn.net/javeme/article/details/82790406