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-class
或init
時,都是需要加載bundle中的類的。在這之前,我給每一個bundle關聯一個獨立的BundleClassLoader
。然後用這個class loader去加載bundle中的類,利用class loader傳遞特性,使得一個bundle中的所有類都是由其關聯的class loader加載的,從而實現bundle之間類隔離效果。
實現class loader時,是實現loadClass
還是findClass
?通過實現loadClass
我們可以改變class loader的雙親委託模式,制定加載類的具體順序。但我的目的僅僅是隔離bundle,想了下其實實現findClass
就可以達成目的。關於loadClass
和findClass
的區別可以參考這裏 (實現自己的類加載時,重寫方法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-class
和init-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 loaderThread.currentThread().getContextClassLoader()
,獲取當前線程的ContextClassLoaderClassLoader.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.getClassLoader
或Class.forName
時,實際使用的是A的class loader,而這個行爲可能不是預期的。這個時候就需要將代碼改爲Thread.currentThread().getContextClassLoader()
。
完。