Java SPI机制实现插件化扩展功能
1.背景
我们有一个图数据库的服务,用户希望在不修改现有源代码的情况下扩展自定义的分词器,达到可插件式扩展功能的目标。
通过Java的SPI机制实现插件式的扩展功能还是比较简便的,下面分主程序部分和插件实现2部分来说明。
特别的,在实现过程中遇到一个比较怪异的问题:
ServiceLoader.load()时抛出NoClassDefFoundError异常,经过Google及StackOverflow都没能找到原因,问题表现与这几个链接中描述的类似:serviceloader-issue-in-jetty、serviceloader-in-glassfish4-java-ee-app、serviceloader-next-causing-a-noclassdeffounderror。
文末会记录一下这个问题的解决过程及原因分析。
2.SPI插件实现要素
主程序部分主要包括:
- 定义插件接口
- 加载插件实现的Jar包
- 加载插件实现类对象
插件实现部分主要包括:
- 实现插件接口
- 配置SPI入口
- 打Jar包
3.实现插件化的流程
下面以扩展一个分词器实例来说明插件化的流程。
-
定义接口
定义接口com.baidu.hugegraph.plugin.HugeGraphPlugin,内容如下:public interface HugeGraphPlugin { public String name(); public void register(); public String supportsMinVersion(); public String supportsMaxVersion(); } -
加载插件实现的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} -
加载插件实现类实例
在主程序中,我们通过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); } } } -
实现插件接口,并注册自定义分词器
新建一个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()); } } -
配置SPI入口
- 确保services目录存在:hugegraph-plugin-demo/resources/META-INF/services
- 在services目录下建立文本文件:com.baidu.hugegraph.plugin.HugeGraphPlugin
- 文件内容如下:com.baidu.hugegraph.plugin.DemoPlugin
-
打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详解。
-
判断类是否真的没有定义?
分析发现,只是通过ServiceLoader加载DemoPlugin类时才报这个错误(DemoPluginimplementsHugeGraphPlugin),如果将DemoPlugin与主程序放在同一个项目中是没问题的。也就是说代码本身是正确的,只是因为以插件方式加载才导致了问题。 -
判断
ServiceLoader是否使用了Context ClassLoader?
经过调试发现ServiceLoader中使用的类加载器确实是通过Thread.currentThread().getContextClassLoader()方法获取的,并且和主程序中的AppClassLoader是同一个实例。 -
判断是否在加载
DemoPlugin类时HugeGraphPlugin类的Jar包还没有被载入?
这个假设是在遇到问题比较迷惑的时候才会提出来的(当时甚至怀疑SPI官方文档是不是写错了),事实上,通过Java参数-verbose:class打印类加载信息,在错误发生之前HugeGraphPlugin类就已经被加载进来了。 -
判断是否循环依赖导致?
插件中DemoPlugin类依赖来自主程序的HugeGraphPlugin类,加载插件时主程序又依赖插件中的DemoPlugin类,难道是循环依赖导致的?于是将HugeGraphPlugin类拆分到单独Jar包中,主程序和插件分别依赖该独立Jar包,不过结果还是同样的错误。 -
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–>

本文介绍了如何利用Java SPI机制实现插件化扩展功能,详细阐述了背景、实现要素及流程。在实现过程中遇到了NoClassDefFoundError异常,通过对问题的深入分析和源码研究,揭示了异常的根本原因在于类加载机制,最后提出了两种有效的解决方案。

被折叠的 条评论
为什么被折叠?



