我們將按照復雜性增加的順序考察一些類加載的典型問題,開發一小段代碼 來解決這些問題中最有趣的一個。即使你不打算馬上寫一個代碼生成框架,這篇 文章也會讓你對靜態定義依賴的模塊運行時(如OSGi系統)的低級操作有比較深 入的了解。
這篇文章還包括一個可以工作的演示項目,該項目不僅包含這裡演示的代碼 ,還有兩個基於ASM的代碼生成器可供實踐。
類加載地點轉換
把一個框架移植到OSGi系統通常需要把框架按照extender模式重構。這個模 式允許框架把所有的類加載工作委托給OSGi框架,與此同時保留對應用代碼的生 命周期的控制。轉換的目標是使用應用bundle的類加載來代替傳統的繁復的類加 載策略。例如我們希望這樣替換代碼:
ClassLoader appLoader = Thread.currentThread ().getContextClassLoader();
Class appClass = appLoader.loadClass ("com.acme.devices.SinisterEngine");
...
ClassLoader appLoader = ...
Class appClass = appLoader.loadClass ("com.acme.devices.SinisterEngine");
替換為:
Bundle appBundle = ...
Class appClass = appBundle.loadClass ("com.acme.devices.SinisterEngine");
盡管我們必須做大量的工作以便OSGi為我們加載應用代碼,我們至少有一種 優美而正確的方式來完成我們的工作,而且會比以前工作得更好!現在用戶可以 通過向OSGi容器安裝/卸載bundle而增加/刪除應用。用戶還可以把他們的應用分 解為多個bundle,在應用之間共享庫並利用模塊化的其他能力。
由於上下文類加載器是目前框架加載應用代碼的標准方式,我們在此對其多 說兩句。當前OSGi沒有定義設置上下文類加載器的策略。當一個框架依賴於上下 文類加載器時,開發者需要預先知道這點,在每次調用進入那個框架時手工設置 上下文類加載器。由於這樣做易於出錯而其不方便,所以在OSGi下幾乎不使用上 下文類加載器。在定義OSGi容器如何自動管理上下文類加載器方面,目前有些人 正在進行嘗試。但在一個官方的標准出現之前,最好把類加載轉移到一個具體的 應用bundle。
適配器類加載器
有時候我們轉換的代碼有外部化的類加載策略。這意味著框架的類和方法接 收明確的ClassLoader 參數,允許我們來決定他們從哪裡加載應用代碼。在這種 情況下,把系統轉換到OSGi就僅僅是讓Bundle對象適配ClassLoader API的問題 。這是一個經典的適配器模式的應用。
public class BundleClassLoader extends ClassLoader {
private final Bundle delegate;
public BundleClassLoader(Bundle delegate) {
this.delegate = delegate;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return delegate.loadClass(name);
}
}
現在我們可以把這個適配器傳給轉換的框架代碼。隨著新bundle的增減,我 們還可以增加bundle跟蹤代碼來創建新的適配器 —— 例如,我們可以“在外部 ”把一個Java框架適配到OSGi,避免浏覽該框架的代碼庫以及變換每個單獨的類 加載場所。下面是將一個框架轉換到使用OSGi 類加載的示意性的例子:
...
Bundle app = ...
BundleClassLoader appLoader = new BundleClassLoader (app);
DeviceSimulationFramework simfw = ...
simfw.simulate("com.acme.devices.SinisterEngine", appLoader);
...
橋接類加載器
許多有趣的Java框架的客戶端代碼在運行時做了很多花哨的類處理工作。其 目的通常是在應用的類空間中構造本不存在的類。讓我們把這些生成的類稱作增 強(enhancement)。通常,增強類實現了一些應用可見的接口或者繼承自一個 應用可見的類。有時,一些附加的接口及其實現也可以被混入。
增強類擴充了應用代碼 - 應用可以直接調用生成的對象。例如,一個傳遞 給應用代碼的服務代理對象就是這種增強類對象,它使得應用代碼不必去跟蹤一 個動態服務。簡單的說,增加了一些AOP特征的包裝器被作為原始對象的替代品 傳遞給應用代碼。
增強類的生命始於字節數組byte[],由你喜愛的類工程庫(ASM,BCEL, CGLIB)產生。一旦我們生成了我們的類,必須把這些原始的字節轉換為一個 Class對象,換言之,我們必須讓某個類加載器對我們的字節調用它的 defineClass()方法。我們有三個獨立的問題要解決:
類空間完整性 - 首先我們必須決定可以定義我們增強類的類空間。該類空間 必須“看到”足夠多的類以便讓增強類能夠被完全鏈接。
可見性 - ClassLoader.defineClass()是一個受保護的方法。我們必須找到 一個好方法來調用它。
類空間一致性 - 增強類從框架和應用bundle混入類,這種加載類的方式對於 OSGi容器來說是“不可見的”。作為結果,增強類可能被暴露給相同類的不兼容 的版本。
類空間完整性
增強類的背後支持代碼對於生成它們的Java框架來說是未公開的 - 這意味著 該框架應該會把該新類加入到其類空間。另一方面,增強類實現的接口或者擴展 的類在應用的類空間是可見,這意味著我們應該在這裡定義增強類。我們不能同 時在兩個類空間定義一個類,所以我們有個麻煩。
因為沒有類空間能夠看到所有需要的類,我們別無選擇,只能創建一個新的 類空間。一個類空間等同於一個類加載器實例,所以我們的第一個工作就是在所 有的應用bundle之上維持一個專有的類加載器。這些叫做橋接類加載器,因為他 們通過鏈接加載器合並了兩個類空間:
public class BridgeClassLoader extends ClassLoader {
private final ClassLoader secondary;
public BridgeClassLoader(ClassLoader primary, ClassLoader secondary) {
super(primary);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return secondary.loadClass(name);
}
}
現在我們可以使用前面開發的BundleClassLoader:
/* Application space */
Bundle app = ...
ClassLoader appSpace = new BundleClassLoader(app);
/*
* Framework space
*
* We assume this code is executed inside the framework
*/
ClassLoader fwSpace = this.getClass().getClassLoader ();
/* Bridge */
ClassLoader bridge = new BridgeClassLoader(appSpace, fwSpace);
這個加載器首先從應用空間為請求提供服務 - 如果失敗,它將嘗試框架空間 。請注意我們仍然讓OSGi為我們做很多重量工作。當我們委托給任何一個類空間 時,我們實際上委托給了一個基於OSGi的類加載器 - 本質上,primary和 secondary加載器可以根據他們各自bundle的導入/導出(import/export)元數 據將加載工作委托給其他 bundle加載器。
此刻我們也許會對自己滿意。然而,真相是苦澀的,合並的框架和應用類空 間也許並不夠!這一切的關鍵是JVM鏈接類(也叫解析類)的特殊方式。對於JVM 鏈接類的工作有很多解釋:
簡短的回答:
JVM以一種細粒度(一次一個符號)的方式來做解析工作的。
冗長的回答:
當JVM鏈接一個類時,它不需要被連接類的所有引用類的完整的描述。它只需 要被鏈接類真正使用的個別的方法、字段和類型的信息。我們直覺上認為對於 JVM來說,其全部是一個類名稱,加上一個超類,加上一套實現的接口,加上一 套方法簽名,加上一套字段簽名。所有這些符號是被獨立且延遲解析的。例如, 要鏈接一個方法調用,調用者的類空間只需要給類對象提供目標類和方法簽名中 用到的所有類型。目標類中的其他許多定義是不需要的,調用者的類加載器永遠 不會收到加載它們(那些不需要的定義)的請求。
正式的答案:
類空間SpaceA的類A必須被類空間SpaceB的相同類對象所代表,當且僅當:
SpaceB存在一個類B,在它的符號表(也叫做常量池)中引用著A。
OSGi容器已經將SpaceA作為類A的提供者(provider)提供給SpaceB。該聯系 是建立在容器所有bundle的靜態元數據之上的。
例如:
假設我們有一個bundle BndA導出一個類A。類A有3個方法,分布於3個接口中 :
IX.methodX(String)
IY.methodY(String)
IZ.methodZ(String)
還假設我們有一個bundle BndB,其有一個類B。類B中有一個引用 A a = … …和一個方法調用a.methodY("Hello!")。為了能夠解析類B,我們需要為BndB的 類空間引入類A和類String。這就夠了!我們不需要導入IX或者IZ。我們甚至不 需要導入IY,因為類B沒有用IY - 它只用了A。在另一方面,bundle BndA導出時 會解析類A,必須提供IX,IY,IZ,因為他們作為被實現的接口直接被引用。最 終,BndA也不需要提供IX,IY,IZ的任何父接口,因為他們也沒有被直接引用。
現在假設我們希望給類空間BndB的類B呈現類空間BndA的類A的一個增強版本 。該增強類需要繼承類A並覆蓋它的一些或全部方法。因此,該增強類需要看到 在所有覆蓋的方法簽名中使用的類。然而,BndB僅當調用了所有被覆蓋的方法時 才會導入所有這些類。BndB恰好調用了我們的增強覆蓋的所有的A 的方法的可能 性非常小。因此,BndB很可能在他的類空間中不會看到足夠的類來定義增強類。 實際上完整的類集合只有BndA能夠提供。我們有麻煩了!
結果是我們必須橋接的不是框架和應用空間,而是框架空間和增強類的空間 - 所以,我們必須把策略從“每個應用空間一個橋”變為“每個增強類空間一個 橋”。我們需要從應用空間到一些第三方bundle的類空間做過渡跳接,在那裡, 應用導入其想讓我們增強的類。但是我們如何做過渡跳接呢?很簡單!如我們所 知,每個類對象可以告訴我們它第一次被定義的類空間是什麼。例如,我們要得 到A 的類加載器,所需要做的就是調用A.class.getClassLoader()。在很多情況 下我們沒有一個類對象,只有類的名字,那麼我們如何從一開始就得到A.class ?也很簡單!我們可以讓應用bundle給我們它所看到的名稱“A”對應的類對象 。然後我們就可以橋接那個類的空間與框架的空間。這是很關鍵的一步,因為我 們需要增強類和原始類在應用內是可以互換的。在類A可能的許多版本中,我們 需要挑選被應用所使用的那個類的類空間。下面是框架如何保持類加載器橋緩存 的示意性例子:
...
/* Ask the app to resolve the target class */
Bundle app = ...
Class target = app.loadClass ("com.acme.devices.SinisterEngine");
/* Get the defining classloader of the target */
ClassLoader targetSpace = target.getClassLoader();
/* Get the bridge for the class space of the target */
BridgeClassLoaderCache cache = ...
ClassLoader bridge = cache.resolveBridge(targetSpace);
橋緩存看起來會是這樣:
public class BridgeClassLoaderCache {
private final ClassLoader primary;
private final Map<ClassLoader, WeakReference<ClassLoader>> cache;
public BridgeClassLoaderCache(ClassLoader primary) {
this.primary = primary;
this.cache = new WeakHashMap<ClassLoader, WeakReference<ClassLoader>>();
}
public synchronized ClassLoader resolveBridge(ClassLoader secondary) {
ClassLoader bridge = null;
WeakReference<ClassLoader> ref = cache.get (secondary);
if (ref != null) {
bridge = ref.get();
}
if (bridge == null) {
bridge = new BridgeClassLoader(primary, secondary);
cache.put(secondary, new WeakReference<ClassLoader> (bridge));
}
return bridge;
}
}
為了防止保留類加載器帶來的內存洩露,我們必須使用弱鍵和弱值。目標是 不在內存中保持一個已卸載的bundle的類空間。我們必須使用弱值,因為每個映 射項目的值(BridgeClassLoader)都強引用著鍵(ClassLoader),於是以此方 式否定它的“弱點”。這是WeakHashMap javadoc規定的標准建議。通過使用一 個弱緩存我們避免了跟蹤所有的bundle,而且不必對他們的生命周期做出反應。
可見性
好的,我們終於有了自己的外來的橋接類空間。現在我們如何在其中定義我 們的增強類?如前所述問題,defineClass()是 BridgeClassLoader的一個受保 護的方法。我們可以用一個公有方法來覆蓋它,但這是粗野的做法。如果做覆蓋 ,我們還需要自己編碼來檢查所請求的增強類是否已經被定義。更好的辦法是遵 循類加載器設計的意圖。該設計告訴我們應該覆蓋findClass(),當findClass() 認為它可以由任意二進制源提供所請求類時會調用defineClass()方法。在 findClass()中我們只依賴所請求的類的名稱來做決定。所以我們的 BridgeClassLoade必須自己拿主意:
這是一個對“A$Enhanced”類的請求,所以我必須調用一個叫做"A"的類的增 強類生成器!然後我在生成的字節數組上調用defineClass()方法。然後我返回 一個新的類對象。
這段話中有兩個值得注意的地方。
我們為增強類的名稱引入了一個文本協議
- 我們可以給我們的類加載器傳入數據的單獨一項 - 所請求的類的名稱的字 符串。同時我們需要傳入數據中的兩項 - 原始類的名稱和一個標志,將其(原 始類)標志為增強類的主語。我們將這兩項打包為一個字符串,形式為[目標類 的名稱]"$Enhanced"。現在 findClass()可以尋找增強類的標志$Enhanced,如 果存在,則提取出目標類的名稱。這樣我們引入了我們增強類的命名約定。無論 何時,當我們在堆棧中看到一個類名以$Enhanced結尾,我們知道這是一個動態 生成的類。為了減少與正常類名稱沖突的風險,我們將增強類標志做得盡可能特 殊(例如:$__service_proxy__)
增強是按需生成的
- 我們永遠不會把一個增強類生成兩次。我們繼承的loadClass()方法首先會 調用findLoadedClass(),如果失敗會調用 parent.loadClass(),只有失敗的時 候它才會調用 findClass()。由於我們為名稱用了一個嚴格的協議,保證 findLoadedClass()在第二次請求相同類的增強類時候不會失敗。這與橋接類加 載器緩存相結合,我們得到了一個非常有效的方案,我們不會橋接同樣的bundle 空間兩次,或者生產冗余的增強類。
這裡我們必須強調通過反射調用defineClass()的選項。cglib使用這種方法 。當我們希望用戶給我們傳遞一個可用的類加載器時這是一種可行的方案。通過 使用反射我們避免了在類加載器之上創建另一個類加載器的需要,只要調用它的 defineClass()方法即可。
類空間一致性
到了最後,我們所做的是使用OSGi的模塊層合並兩個不同的、未關聯的類空 間。我們還引入了在這些空間中一種搜索順序,其與邪惡的Java類路徑搜索順序 相似。實際上,我們破壞了OSGi容器的類空間一致性。這裡是糟糕的事情發生的 一個場景:
框架使用包com.acme.devices,需要的是1.0版本。
應用使用包com.acme.devices,需要的是2.0版本。
類A直接飲用com.acme.devices.SinisterDevice。
類A$Enhanced在他自己的實現中使用了com.acme.devices.SinisterDevice。
因為我們搜索應用空間,首先A$Enhanced會被鏈接到 com.acme.devices.SinisterDevice 2.0版,而他的內部代碼是基於 com.acme.devices.SinisterDevice 1.0編譯的。
結果應用將會看到詭異的LinkageErrors或者ClassCastExceptions。不用說 ,這是個問題。
唉,自動處理這個問題的方式還不存在。我們必須簡單的確保增強類的內部 代碼直接引用的是“非常私有的”類實現,不會被其他類使用。我們甚至可以為 任何我們可能希望使用的外部API定義私有的適配器,然後在增強類代碼中引用 這些適配器。一旦我們有了一個良好定義的實現子空間,我們可以用這個知識來 限制類洩露。現在我們僅僅向框架空間委托特殊的私有實現類的請求。這還會限 定搜索順序問題,使得應用優先搜索還是框架優先搜索對結果沒有影響。讓所有 的事情都可控的一個好策略是有一個專有的包來包含所有增強類實現代碼。那麼 橋接加載器就可以檢查以那個包開頭的類的名稱並將它們委托給框架加載器做加 載。最終,我們有時候可以對特定的單實例(singleton)包放寬這個隔離策略 ,例如org.osgi.framework - 我們可以安全的直接基於org.osgi.framework編 譯我們的增強類代碼,因為在運行時所有在OSGi容器中的代碼都會看到相同的 org.osgi.framework - 這是由OSGi核心保證的。
把事情放到一起
所有關於這個類加載的傳說可以被濃縮為下面的100行代碼:
public class Enhancer {
private final ClassLoader privateSpace;
private final Namer namer;
private final Generator generator;
private final Map<ClassLoader , WeakReference<ClassLoader>> cache;
public Enhancer(ClassLoader privateSpace, Namer namer, Generator generator) {
this.privateSpace = privateSpace;
this.namer = namer;
this.generator = generator;
this.cache = new WeakHashMap<ClassLoader , WeakReference<ClassLoader>>();
}
@SuppressWarnings("unchecked")
public <T> Class<T> enhance(Class<T> target) throws ClassNotFoundException {
ClassLoader context = resolveBridge(target.getClassLoader ());
String name = namer.map(target.getName());
return (Class<T>) context.loadClass(name);
}
private synchronized ClassLoader resolveBridge(ClassLoader targetSpace) {
ClassLoader bridge = null;
WeakReference<ClassLoader> ref = cache.get (targetSpace);
if (ref != null) {
bridge = ref.get();
}
if (bridge == null) {
bridge = makeBridge(targetSpace);
cache.put(appSpace, new WeakReference<ClassLoader> (bridge));
}
return bridge;
}
private ClassLoader makeBridge(ClassLoader targetSpace) {
/* Use the target space as a parent to be searched first */
return new ClassLoader(targetSpace) {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
/* Is this used privately by the enhancements? */
if (generator.isInternal(name)) {
return privateSpace.loadClass(name);
}
/* Is this a request for enhancement? */
String unpacked = namer.unmap(name);
if (unpacked != null) {
byte[] raw = generator.generate(unpacked, name, this);
return defineClass(name, raw, 0, raw.length);
}
/* Ask someone else */
throw new ClassNotFoundException(name);
}
};
}
}
public interface Namer {
/** Map a target class name to an enhancement class name. */
String map(String targetClassName);
/** Try to extract a target class name or return null. */
String unmap(String className);
}
public interface Generator {
/** Test if this is a private implementation class. */
boolean isInternal(String className);
/** Generate enhancement bytes */
byte[] generate(String inputClassName, String outputClassName, ClassLoader context);
}
Enhancer僅僅針對橋接模式。代碼生成邏輯被具體化到一個可插拔的 Generator中。該Generator接收一個上下文類加載器,從中可以得到類,使用反 射來驅動代碼生成。增強類名稱的文本協議也可以通過Name接口插拔。這裡是一 個最終的示意性代碼,展示這麼一個增強類框架是如何使用的:
...
/* Setup the Enhancer on top of the framework class space */
ClassLoader privateSpace = getClass().getClassLoader();
Namer namer = ...;
Generator generator = ...;
Enhancer enhancer = new Enhancer(privateSpace, namer, generator);
...
/* Enhance some class the app sees */
Bundle app = ...
Class target = app.loadClass ("com.acme.devices.SinisterEngine");
Class<SinisterDevice> enhanced = enhancer.enhance (target);
...
這裡展示的Enhance框架不僅是偽代碼。實際上,在撰寫這篇文章期間,這個 框架被真正構建出來並用兩個在同一OSGi容器中同時運行的樣例代碼生成器進行 了測試。結果是類加載正常,現在代碼在Google Code上,所有人都可以拿下來 研究。
對於類生成過程本身感興趣的人可以研究這兩個基於ASM的生成器樣例。那些 在service dynamics上閱讀文章的人也許注意到proxy generator使用 ServiceHolder代碼作為一個私有實現。
結論
這裡展現的類加載特技在許多OSGi之外的基礎框架中使用。例如橋接類加載 器被用在Guice,Peaberry中,Spring Dynamic Modules則用橋接類加載器來使 他們的AOP包裝器和服務代理得以工作。當我們聽說Spring的伙計們在將Tomcat 適配到OSGi方面做了大量工作時,我們可以推斷他們還得做類加載位置轉換或者 更大量的重構來外化Tomcat的servlet加載。