Java中隔離容器的實現

Java中隔離容器用於隔離各個依賴庫環境,解決Jar包衝突問題。

問題

應用App依賴庫LibA和LibB,而LibA和LibB又同時依賴LibBase,而LibA和LibB都是其他團隊開發的,其中LibA發佈了一個重要的修復版本,但是依賴LibBase v2.0,而LibB還沒有升級版本,LibBase還不是兼容的,那麼此時升級就會面臨困難。在生產環境中這種情況往往更惡劣,可能是好幾層的間接依賴關係。

隔離容器用於解決這種問題。它把LibA和LibB的環境完全隔離開來,LibBase即使類名完全相同也不互相沖突,使得LibA和LibB的升級互不影響。衆所周知,Java中判定兩個類是否相同,看的是類名和其對應的class loader,兩者同時相同才表示相等。隔離容器正是利用這種特性實現的。

KContainer

這裏我實現了一個demo,稱爲KContainer,源碼見github kcontainer。這個container模仿了一些OSGI的東西,這裏把LibA和LibB看成是兩個bundle,bundle之間是互相隔離的,每個bundle有自己所依賴的第三方庫,bundle之間的第三方庫完全對外隱藏。bundle可以導出一些類給其他bundle用,bundle可以開啓自己的服務。由於是個demo,我只實現關鍵的部分。

KContainer的目錄結構類似:

.
|-- bundle
    |-- test1
        |-- test1.prop
        |-- classes
        |-- lib
            |-- a.jar
            |-- b.jar
    |-- test2
        |-- test2.prop
        |-- classes
|-- lib
    |-- kcontainer.jar
    |-- kcontainer.interface.jar

bundle目錄存放了所有會被自動載入的bundle。每一個bundle都有一個配置文件bundle-name.prop,用於描述自己導出哪些類,例如:

init=com.codemacro.test.B
export-class=com.codemacro.test.Export; com.codemacro.test.Export2

init指定bundle啓動時需要調用的類,用戶可以在這個類裏開啓自己的服務;export-class描述需要導出的類列表。bundle之間的所有類都是隔離的,但export-class會被統一放置,作爲所有bundle共享的類。後面會描述KContainer如何處理類加載問題,這也是隔離容器的主要內容。

bundle依賴的類可以直接以*.class文件存放到classes目錄,也可以作爲*.jar放到lib目錄。作爲extra pratice,我還會把*.jar中的jar解壓同時作爲類加載的路徑。

KContainer本身可以作爲一個framework被使用。爲了示例,我寫了一個入口類,加載啓動完所有bundle後就退出了,這個僅作爲例子,用不了生產。

隔離的核心實現

隔離的目的是分開各個bundle中的類。利用的語言特性包括:

  • class的區分由class name和載入其的class loader共同決定
  • 當在class A中使用了class B時,JVM默認會用class A的class loader去加載class B
  • class loader中的雙親委託機制
  • URLClassLoader會從指定的目錄及*.jar中加載類

KContainer的主要任務,就是爲bundle實現一個自定義的class loader。

當KContainer加載一個bundle時,在處理其export-classinit時,都是需要加載bundle中的類的。在這之前,我給每一個bundle關聯一個獨立的BundleClassLoader。然後用這個class loader去加載bundle中的類,利用class loader傳遞特性,使得一個bundle中的所有類都是由其關聯的class loader加載的,從而實現bundle之間類隔離效果。

實現class loader時,是實現loadClass還是findClass?通過實現loadClass我們可以改變class loader的雙親委託模式,制定加載類的具體順序。但我的目的僅僅是隔離bundle,想了下其實實現findClass就可以達成目的。關於loadClassfindClass的區別可以參考這裏 (實現自己的類加載時,重寫方法loadClass與findClass的區別)。簡單來說,就是findClass只有在類確實找不到的情況下才會被調用,在此之前,loadClass默認都是走的雙親委託模式。

BundleClassLoader派生於URLClassLoader,默認的parent class loader就是system class loader (app class loader)。這使得KContainer中的bundle類加載有三層選擇:自己的class path;其他bundle共享的classes;jvm的class path。通過實現findClass,在默認路徑都無法加載到類時,才嘗試bundle共享的class,優先級最低。

其實現大概爲:

public class BundleClassLoader extends URLClassLoader {
  public BundleClassLoader(File home, SharedClassList sharedClasses) {
    // getClassPath將bundle目錄下的classes和各個jar作爲class path傳給URLClassLoader
    super(getClassPath(home)); 
    this.sharedClasses = sharedClasses;
  }

  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
    logger.debug("try find class {}", name);
    Class<?> claz = null;
    try {
      claz = super.findClass(name);
    } catch (ClassNotFoundException e) {
      claz = null;
    }
    if (claz != null) {
      logger.debug("load from class path for {}", name);
      return claz;
    }
    claz = sharedClasses.get(name);
    if (claz != null) {
      logger.debug("load from shared class for {}", name);
      return claz;
    }
    logger.warn("not found class {}", name);
    throw new ClassNotFoundException(name);
  }
}

完整代碼參看BundleClassLoader.java

創建bundle時,會爲其創建自己的class loader,並使用這個class loader來載入export-classinit-class

  public static Bundle create(File home, String name, SharedClassList sharedClasses, 
      BundleConf conf) {
    BundleClassLoader loader = new BundleClassLoader(home, sharedClasses);
    List<String> exports = conf.getExportClassNames();
    if (exports != null) {
      logger.info("load exported classes for {}", name);
      loadExports(loader, sharedClasses, exports);
    }
    return new Bundle(name, conf.getInitClassName(), loader);
  }

  private static void loadExports(ClassLoader loader, SharedClassList sharedClasses,
      List<String> exports) {
      for (String claz_name: exports) {
        try {
          Class<?> claz = loader.loadClass(claz_name); // 載入class
          sharedClasses.put(claz_name, claz);
        } catch (ClassNotFoundException e) {
          logger.warn("load class {} failed", claz_name);
        }
      }
  }

以上。

擴展

擴展的地方有很多,例如支持導出package,導出一個完整的jar。當然可能需要實現loadClass,以改變類加載的優先級,讓共享類的優先級高於jvm class path的優先級。

其他

線程ContextClassLoader

提到class loader,我們看下最常接觸的幾類:

  • XX.class.getClassLoader,獲取加載類XX的class loader
  • Thread.currentThread().getContextClassLoader(),獲取當前線程的ContextClassLoader
  • ClassLoader.getSystemClassLoader(),獲取system class loader

system class loader的parent就是ext class loader,再上面就是bootstrap class loader了 (不是java類,實際獲取不到)。默認情況下以上三個class loader都是一個:

System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(Main.class.getClassLoader());
System.out.println(Thread.currentThread().getContextClassLoader());

Output:

sun.misc.Launcher$AppClassLoader@157c2bd
sun.misc.Launcher$AppClassLoader@157c2bd
sun.misc.Launcher$AppClassLoader@157c2bd

創建線程時,新的線程ContextClassLoader就是父線程的ContextClassLoader。在載入一個新的class時,推薦優先使用線程context class loader,例如框架Jodd中包裝的。關於線程context class loader和Class.getClassLoader這裏有個解釋算是相對合理:ContextClassLoader淺析

即,當你把一個對象A傳遞到另一個線程中,這個線程由對象B創建,A/B兩個對象對應的類關聯的class loader不同,在B的線程中調用A.some_method,some_method加載資源或類時,如果使用了Class.getClassLoaderClass.forName時,實際使用的是A的class loader,而這個行爲可能不是預期的。這個時候就需要將代碼改爲Thread.currentThread().getContextClassLoader()

完。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章