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官方文檔,我們定義了一個目錄
來存放插件的Jar包,在啟動Java主程式服務時通過參數plugins
指定插件Jar包的目錄。當需要擴充新的插件時,隻需要把插件Jar包拷貝到-Djava.ext.dirs=plugins
目錄下,重新開機主程式服務即可生效。完整的啟動指令示例:plugins
-
加載插件實作類執行個體
在主程式中,我們通過
來加載所有插件執行個體。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()
,關于類加載器的介紹可參考Java Classloader詳解。
Thread Context ClassLoader
-
判斷類是否真的沒有定義?
分析發現,隻是通過
加載ServiceLoader
類時才報這個錯誤(DemoPlugin
implementsDemoPlugin
),如果将HugeGraphPlugin
與主程式放在同一個項目中是沒問題的。也就是說代碼本身是正确的,隻是因為以插件方式加載才導緻了問題。DemoPlugin
- 判斷
是否使用了ServiceLoader
Context ClassLoader
?
經過調試發現
中使用的類加載器确實是通過ServiceLoader
方法擷取的,并且和主程式中的Thread.currentThread().getContextClassLoader()
是同一個執行個體。AppClassLoader
- 判斷是否在加載
類時DemoPlugin
HugeGraphPlugin
類的Jar包還沒有被載入?
這個假設是在遇到問題比較迷惑的時候才會提出來的(當時甚至懷疑SPI官方文檔是不是寫錯了),事實上,通過Java參數
列印類加載資訊,在錯誤發生之前-verbose:class
類就已經被加載進來了。HugeGraphPlugin
-
判斷是否循環依賴導緻?
插件中
類依賴來自主程式的DemoPlugin
類,加載插件時主程式又依賴插件中的HugeGraphPlugin
類,難道是循環依賴導緻的?于是将DemoPlugin
類拆分到單獨Jar包中,主程式和插件分别依賴該獨立Jar包,不過結果還是同樣的錯誤。HugeGraphPlugin
-
ClassLoader類加載機制導緻?
綜合第2點和第3點結果分析,會更加發現問題的詭異之處,主程式和插件使用的是同一個
來加載我們定義的類,而且ClassLoader
類明明已經被加載了的,那為何加載HugeGraphPlugin
類時還報錯找不到DemoPlugin
HugeGraphPlugin
類?
結合
相關源碼分析發現,ClassLoader
在加載AppClassLoader
類時,需要委托給雙親DemoPlugin
來加載(因為插件的Jar包配置在ExtClassLoader
路徑下),而java.ext.dirs
類繼承自DemoPlugin
類,HugeGraphPlugin
又需要拿到或加載ExtClassLoader
類,但是HugeGraphPlugin
所屬的Jar包不在HugeGraphPlugin
路徑下進而找不到ext
(事實上它在HugeGraphPlugin
裡面,AppClassLoader
隻會加載ExtClassLoader
目錄和lib/ext
java.ext.dirs
目錄)。
總結一下,就是配置了DemoPlugin Jar包到
,而插件Jar包所依賴的HugeGraphPlugin Jar包在ext
下,導緻父加載器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–>