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
类时才报这个错误(DemoPlugin
implementsHugeGraphPlugin
),如果将DemoPlugin
与主程序放在同一个项目中是没问题的。也就是说代码本身是正确的,只是因为以插件方式加载才导致了问题。 -
判断
ServiceLoader
是否使用了Context ClassLoader
?
经过调试发现ServiceLoader
中使用的类加载器确实是通过Thread.currentThread().getContextClassLoader()
方法获取的,并且和主程序中的AppClassLoader
是同一个实例。扫描二维码关注公众号,回复: 3408655 查看本文章 -
判断是否在加载
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–>