我们有一个图数据库的服务,用户希望在不修改现有源代码的情况下扩展自定义的分词器,达到可插件式扩展功能的目标。
通过Java的SPI机制实现插件式的扩展功能还是比较简便的,下面分主程序部分和插件实现2部分来说明。
特别的,在实现过程中遇到一个比较怪异的问题:
ServiceLoader.load()时抛出NoClassDefFoundError异常,经过Google及StackOverflow都没能找到原因,问题表现与这几个链接中描述的类似:serviceloader-issue-in-jetty、serviceloader-in-glassfish4-java-ee-app、serviceloader-next-causing-a-noclassdeffounderror。
文末会记录一下这个问题的解决过程及原因分析。
主程序部分主要包括:
插件实现部分主要包括:
下面以扩展一个分词器实例来说明插件化的流程。
com.baidu.hugegraph.plugin.HugeGraphPlugin,内容如下:public interface HugeGraphPlugin {
public String name();
public void register();
public String supportsMinVersion();
public String supportsMaxVersion();
}
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);
}
}
}
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());
}
}
plugins目录,重启主程序即可生效。在实现过程中,遇到一个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
根据错误信息从网上搜索,并没有发现根本解决方法。初步分析觉得跟类加载器ClassLoader有关,因为本身HugeGraphPlugin类是明显定义了的。
注意
ServiceLoader.load()有一点比较特殊的地方,它的类加载器是Thread Context ClassLoader,关于类加载器的介绍可参考Java Classloader详解。
判断类是否真的没有定义?
分析发现,只是通过ServiceLoader加载DemoPlugin类时才报这个错误(DemoPlugin implements HugeGraphPlugin),如果将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;
}
}
问题根源找到了,解决方法就很简单了,归根到底有2种解决方法,选择其中一种即可:
java.ext.dirs下。classpath下。<–end–>