天天看點

Java SPI機制實作插件化擴充功能

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插件實作要素

主程式部分主要包括:

  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

    目錄下,重新開機主程式服務即可生效。完整的啟動指令示例:
  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

    是同一個執行個體。
  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–>