在 Class 文件格式與執(zhí)行引擎這部分中,用戶的程序能直接影響的內(nèi)容并不太多,Class 文件以何種格式存儲,類型何時加載、如何連接,以及虛擬機如何執(zhí)行字節(jié)碼指令等都是由虛擬機直接控制的行為,用戶程序無法對其進行改變。能通過程序進行操作的,主要是字節(jié)碼生成與類加載器這兩部分的功能,但僅僅在如何處理這兩點上,就已經(jīng)出現(xiàn)了許多值得欣賞和借鑒的思路,這些思路后來成為了許多常用功能和程序?qū)崿F(xiàn)的基礎(chǔ)。在本章中,我們將看一下前面所學(xué)的知識在實際開發(fā)之中是如何應(yīng)用的。
在案例分析部分,筆者準(zhǔn)備了 4 個例子,關(guān)于類加載器和字節(jié)碼的案例各有兩個。并且這兩個領(lǐng)域的案例中各有一個案例是大多數(shù) Java 開發(fā)人員都使用過的工具或技術(shù),另外一個案例雖然不一定每個人都使用過,但卻特別精彩地演繹出這個領(lǐng)域中的技術(shù)特性。希望這些案例能引起讀者的思考,并給讀者的日常工作帶來靈感。
一、tomcat正統(tǒng)的類加載器架構(gòu)
主流的 Java Web 服務(wù)器,如 Tomcat、Jetty、WebLogic、WebSphere 或其他筆者沒有列舉的服務(wù)器,都實現(xiàn)了自己定義的類加載器(一般都不止一個)。因為一個功能健全的 Web 服務(wù)器,要解決如下幾個問題:
- 部署在同一個服務(wù)器上的兩個 Web 應(yīng)用程序所使用的 Java 類庫可以實現(xiàn)相互隔離。這是最基本的需求,兩個不同的應(yīng)用程序可能會依賴同一個第三方類庫的不同版本,不能要求一個類庫在一個服務(wù)器中只有一份,服務(wù)器應(yīng)當(dāng)保證兩個應(yīng)用程序的類庫可以互相獨立使用。
部署在同一個服務(wù)器上的兩個 Web 應(yīng)用程序所使用的 Java 類庫可以互相共享。這個需求也很常見,例如,用戶可能有 10 個使用 Spring 組織的應(yīng)用程序部署在同一臺服務(wù)器上,如果把 10 份 Spring 分別存放在各個應(yīng)用程序的隔離目錄中,將會是很大的資源浪費——這主要倒不是浪費磁盤空間的問題,而是指類庫在使用時都要被加載到服務(wù)器內(nèi)存,如果類庫不能共享,虛擬機的方法區(qū)就會很容易出現(xiàn)過度膨脹的風(fēng)險。 - 服務(wù)器需要盡可能地保證自身的安全不受部署的 Web 應(yīng)用程序影響。目前,有許多主流的 Java Web 服務(wù)器自身也是使用 Java 語言來實現(xiàn)的。因此,服務(wù)器本身也有類庫依賴的問題,一般來說,基于安全考慮,服務(wù)器所使用的類庫應(yīng)該與應(yīng)用程序的類庫相互獨立。
支持 JSP 應(yīng)用的 Web 服務(wù)器,大多數(shù)都需要支持 HotSwap 功能。我們知道,JSP 文件最終要編譯成 Java Class 才能由虛擬機執(zhí)行,但 JSP 文件由于其純文本存儲的特性,運行時修改的概率遠遠大于第三方類庫或程序自身的 Class 文件。而且 ASP、PHP 和 JSP 這些網(wǎng)頁應(yīng)用也把修改后無須重啟作為一個很大的 “優(yōu)勢” 來看待,因此 “主流” 的 Web 服務(wù)器都會支持 JSP 生成類的熱替換,當(dāng)然也有 “非主流” 的,如運行在生產(chǎn)模式(Producation Mode)下的 WebLogic 服務(wù)器默認就不會處理 JSP 文件的變化。
由于存在上述問題,在部署 Web 應(yīng)用時,單獨的一個 ClassPath 就無法滿足需求了,所以各種 Web 服務(wù)器都 “不約而同” 地提供了好幾個 ClassPath 路徑供用戶存放第三方類庫,這些路徑一般都以 “l(fā)ib” 或 “classes” 命名。被放置到不同路徑中的類庫,具備不同的訪問范圍和服務(wù)對象,通常,每一個目錄都會有一個相對應(yīng)的自定義類加載器去加載放置在里面的 Java 類庫?,F(xiàn)在,筆者就以 Tomcat 服務(wù)器(注:本案例中選用的是 Tomcat 5.x 服務(wù)器的目錄和類加載器結(jié)構(gòu),在 Tomcat 6.x 的默認配置下,/common、/server 和 /shared 三個目錄已經(jīng)合并到一起了)為例,看一看 Tomcat 具體是如何規(guī)劃用戶類庫結(jié)構(gòu)和類加載器的。
- 在 Tomcat 目錄結(jié)構(gòu)中,有 3 組目錄(“/common/”、“/server/” 和 “/shared/”)可以存放 Java 類庫,另外還可以加上 Web 應(yīng)用程序自身的目錄 “/WEB-INF/”,一共 4 組,把 Java 類庫放置在這些目錄中的含義分別如下。
放置在 /common 目錄中:類庫可被 Tomcat 和所有的 Web 應(yīng)用程序共同使用。
放置在 /server 目錄中:類庫可被 Tomcat 使用,對所有的 Web 應(yīng)用程序都不可見。
放置在 /shared 目錄中:類庫可被所有的 Web 應(yīng)用程序共同使用,但對 Tomcat 自己不可見。
放置在 /WebApp/WEB-INF 目錄中:類庫僅僅可以被此 Web 應(yīng)用程序使用,對 Tomcat 和其他 Web 應(yīng)用程序都不可見。
為了支持這套目錄結(jié)構(gòu),并對目錄里面的類庫進行加載和隔離,Tomcat 自定義了多個類加載器,這些類加載器按照經(jīng)典的雙親委派模型來實現(xiàn),其關(guān)系如圖 9-1 所示。

灰色背景的 3 個類加載器是 JDK 默認提供的類加載器,這 3 個加載器的作用前面已經(jīng)介紹過了。而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 則是 Tomcat 自己定義的類加載器,它們分別加載 /common/、/server/、/shared/* 和 /WebApp/WEB-INF/* 中的 Java 類庫。其中 WebApp 類加載器和 Jsp 類加載器通常會存在多個實例,每一個 Web 應(yīng)用程序?qū)?yīng)一個 WebApp 類加載器,每一個 JSP 文件對應(yīng)一個 Jsp 類加載器。
從圖 9-1 的委派關(guān)系中可以看出,CommonClassLoader 能加載的類都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加載的類則與對方相互隔離。WebAppClassLoader 可以使用 SharedClassLoader 加載到的類,但各個 WebAppClassLoader 實例之間相互隔離。而 JasperLoader 的加載范圍僅僅是這個 JSP 文件所編譯出來的那一個 Class,它出現(xiàn)的目的就是為了被丟棄:當(dāng)服務(wù)器檢測到 JSP 文件被修改時,會替換掉目前的 JasperLoader 的實例,并通過再建立一個新的 Jsp 類加載器來實現(xiàn) JSP 文件的 HotSwap 功能。
對于 Tomcat 的 6.x 版本,只有指定了 tomcat/conf/catalina.properties 配置文件的 server.loader 和 share.loader 項后才會真正建立 CatalinaClassLoader 和 SharedClassLoader 的實例,否則會用到這兩個類加載器的地方都會用 CommonClassLoader 的實例代替,而默認的配置文件中沒有設(shè)置這兩個 loader 項,所以 Tomcat 6.x 順理成章地把 /common、/server 和 /shared 三個目錄默認合并到一起變成一個 /lib 目錄,這個目錄里的類庫相當(dāng)于以前 /common 目錄中類庫的作用。這是 Tomcat 設(shè)計團隊為了簡化大多數(shù)的部署場景所做的一項改進,如果默認設(shè)置不能滿足需要,用戶可以通過修改配置文件指定 server.loader 和 share.loader 的方式重新啟用 Tomcat 5.x 的加載器架構(gòu)。
Tomcat 加載器的實現(xiàn)清晰易懂,并且采用了官方推薦的 “正統(tǒng)” 的使用類加載器的方式。如果讀者閱讀完上面的案例后,能完全理解 Tomcat 設(shè)計團隊這樣布置加載器架構(gòu)的用意,那說明已經(jīng)大致掌握了類加載器 “主流”的使用方式,那么筆者不妨再提一個問題來讓讀者思考一下:前面曾經(jīng)提到過一個場景,如果有 10 個 Web 應(yīng)用程序都是用 Spring 來進行組織和管理的話,可以把 Spring 放到 common 或 shared 目錄下讓這些程序共享。Spring 要對用戶程序的類進行管理,自然要能訪問到用戶程序的類,而用戶的程序顯然是放在 /WebApp/WEB-INF 目錄中的,那么被 CommonClassLoader 或 SharedClassLoader 加載的 Spring 如何訪問并不在其加載范圍的用戶程序呢?
二、osgi靈活的類加載器架構(gòu)
Java 程序社區(qū)中流傳著這么一個觀點:“學(xué)習(xí) JEE 規(guī)范,去看 JBoss 源碼;學(xué)習(xí)類加載器,就去看 OSGi 源碼”。盡管 “JEE 規(guī)范” 和 “類加載器的知識” 并不是一個對等的概念,不過,既然這個觀點能在程序員中流傳開來,也從側(cè)面說明了 OSGi 對類加載器的運用確實有其獨到之處。
OSGi(Open Service Gateway Initiative) 是 OSGi 聯(lián)盟(OSGi Alliance)制定的一個基于 Java 語言的動態(tài)模塊化規(guī)范,這個規(guī)范最初由 Sun、IBM、愛立信等公司聯(lián)合發(fā)起,目的是使用服務(wù)提供商通過住宅網(wǎng)關(guān)為各種家用智能設(shè)備提供各種服務(wù),后來這個規(guī)范在 Java 的其他技術(shù)領(lǐng)域也有相當(dāng)不錯的發(fā)展,現(xiàn)在已經(jīng)成為 Java 世界中 “事實上” 的模塊化標(biāo)準(zhǔn),并且已經(jīng)有了 Equinox、Felix 等成熟的實現(xiàn)。OSGi 在 Java 程序員中最著名的應(yīng)用案例就是 Eclipse IDE,另外還有許多大型的軟件平臺和中間件服務(wù)器都基于或聲明將會基于 OSGi 規(guī)范來實現(xiàn),如 IBM Jazz 平臺、GlassFish 服務(wù)器、JBoss OSGi 等。
OSGi 中的每個模塊(稱為 Bundle)與普通的 Java 類庫區(qū)別并不太大,兩者一般都以 JAR 格式進行封裝,并且內(nèi)部存儲的都是 Java Package 和 Class。但是一個 Bundle 可以聲明它所依賴的 Java Package(通過 Import-Package 描述),也可以聲明它允許導(dǎo)出發(fā)布的 Java Package(通過 Export-Package 描述)。在 OSGi 里面,Bundle 之間的依賴關(guān)系從傳統(tǒng)的上層模塊依賴底層模塊轉(zhuǎn)變?yōu)槠郊壞K之間的依賴(至少外觀上如此),而且類庫的可見性能得到非常精確的控制,一個模塊里只有被 Export 過的 Package 才可能由外界訪問,其他的 Package 和 Class 將會隱藏起來。除了更精確的模塊劃分和可見性控制外,引入 OSGi 的另外一個重要理由是,基于 OSGi 的程序很可能(只是很可能,并不是一定會)可以實現(xiàn)模塊級的熱插拔功能,當(dāng)程序升級更新或調(diào)試除錯時,可以只停用、重新安裝然后啟用程序的其中一部分,這對企業(yè)級程序開發(fā)來說是一個非常有誘惑力的特性。
OSGi 之所以能有上述 “誘人” 的特點,要歸功于它靈活的類加載器架構(gòu)。OSGi 的 Bundle 類加載器之間只有規(guī)則,沒有固定的委派關(guān)系。例如,某個 Bundle 聲明了一個它依賴的 Package,如果有其他 Bundle 聲明發(fā)布了這個 Package,那么所有對這個 Package 的類加載動作都會委派給發(fā)布它的 Bundle 類加載器去完成。不涉及某個具體的 Package 時,各個 Bundle 加載器都是平級關(guān)系,只有具體使用某個 Package 和 Class 的時候,才會根據(jù) Package 導(dǎo)入導(dǎo)出定義來構(gòu)造 Bundle 間的委派和依賴。
另外,一個 Bundle 類加載器為其他 Bundle 提供服務(wù)時,會根據(jù) Export-Package 列表嚴格控制訪問范圍。如果一個類存在于 Bundle 的類庫中但是沒有被 Export,那么這個 Bundle 的類加載器能找到這個類,但不會提供給其他 Bundle 使用,而且 OSGi 平臺也不會把其他 Bundle 的類加載請求分配給這個 Bundle 來處理。
我們可以舉一個更具體一些的簡單例子,假設(shè)存在 Bundle A、Bundle B、Bundle C 三個模塊,并且這三個 Bundle 定義的依賴關(guān)系如下。
Bundle A:聲明發(fā)布了 packageA,依賴了 java.* 的包。
Bundle B:聲明依賴了 packageA 和 packageC,同時也依賴了 java.* 的包。
Bundle C:聲明發(fā)布了 packageC,依賴了 packageA。
那么,這三個 Bundle 之間的類加載器及父類加載器之間的關(guān)系如圖 9-2 所示。

由于沒有牽扯到具體的 OSGi 實現(xiàn),所以圖 9-2 中的類加載器都沒有指明具體的加載器實現(xiàn),只是一個體現(xiàn)了加載器之間關(guān)系的概念模型,并且只是體現(xiàn)了 OSGi 中最簡單的加載器委派關(guān)系。一般來說,在 OSGi 中,加載一個類可能發(fā)生的查找行為和委派關(guān)系會比圖 9-2 中顯示的復(fù)雜得多,類加載時可能進行的查找規(guī)則如下:
以 java.* 開頭的類,委派給父類加載器加載。
否則,委派列表名單內(nèi)的類,委派給父類加載器加載。
否則,Import 列表中的類,委派給 Export 這個類的 Bundle 的類加載器加載。
否則,查找當(dāng)前 Bundle 的 Classpath,使用自己的類加載器加載。
否則,查找是否在自己的 Fragment Bundle 中,如果是,則委派給 Fragment Bundle 的類加載器加載。
否則,查找 Dynamic Import 列表的 Bundle,委派給對應(yīng) Bundle 的類加載器加載。
否則,類查找失敗。
從圖 9-2 中還可以看出,在 OSGi 里面,加載器之間的關(guān)系不再是雙親委派模型的屬性結(jié)構(gòu),而是已經(jīng)進一步發(fā)展成了一種更為復(fù)雜的、運行時才能確定的網(wǎng)狀結(jié)構(gòu)。這種網(wǎng)狀的類加載器架構(gòu)在帶來更好的靈活性的同時,也可能會產(chǎn)生許多新的隱患。筆者曾經(jīng)參與過將一個非 OSGi 的大型系統(tǒng)向 Equinox OSGi 平臺遷移的項目,由于歷史原因,代碼模塊之間的的依賴關(guān)系錯綜復(fù)雜,勉強分離出各個模塊的 Bundle 后,發(fā)現(xiàn)在高并發(fā)環(huán)境下經(jīng)常出現(xiàn)死鎖。我們很容易就找到了死鎖的原因:如果出現(xiàn)了 Bundle A 依賴于 Bundle B 的 Package B,而 Bundle B 又依賴了 Bundle A 的 Package A,這兩個 Bundle 進行類加載時就很容易發(fā)生死鎖。具體情況是當(dāng) Bundle A 加載 Package B 的類時,首先需要鎖定當(dāng)前類加載器的實例對象(java.lang.ClassLoader.loadClass() 是一個 synchronized 方法),然后把請求委派給 Bundle B 的加載器處理,但如果這時候 Bundle B 也正好想加載 Package A 的類,它也先鎖定自己的加載器再去請求 Bundle A 的加載器處理,這樣,兩個加載器都在等待對方處理自己的請求,而對方處理完之前自己又一直處于同步鎖定的狀態(tài),因此它們就互相死鎖,永遠無法完成加載請求了。Equinox 的 Bug List 中有關(guān)于這類問題的 Bug,也提供了一個以犧牲性能為代價的解決方案——用戶可以啟用 osgi.classloader.singleThreadLoads 參數(shù)來按單線程串行化的方式強制進行類加載器動作。在 JDK 1.7 中,為非樹狀繼承關(guān)系下的類加載器架構(gòu)進行了一次專門的升級,目的是從底層避免這類死鎖出現(xiàn)的可能。
總體來說,OSGi 描繪了一個很美好的模塊化開發(fā)的目標(biāo),而且定義了實現(xiàn)這個目標(biāo)所需要的各種服務(wù),同時也有成熟框架對其提供實現(xiàn)支持。對于單個虛擬機下的應(yīng)用,從開發(fā)初期就建立在 OSGi 是一個很不錯的選擇,這樣便于約束依賴。但并非所有的應(yīng)用都適合采用 OSGi 作為基礎(chǔ)架構(gòu),OSGi 在提供強大功能的同時,也引入了額外的復(fù)雜度,帶來了線程死鎖和內(nèi)存泄露的風(fēng)險。
三、字節(jié)碼生成技術(shù)與動態(tài)代理的實現(xiàn)
“字節(jié)碼生成” 并不是什么高深的技術(shù),讀者在看到 “字節(jié)碼生成” 這個標(biāo)題時也不必去向諸如 Javassit、CGLib、ASM 之類的字節(jié)碼類庫,因為 JDK 里面的 javac 命令就是字節(jié)碼生成技術(shù)的 “老祖宗”,并且 javac 也是一個由 Java 語言寫成的程序,它的代碼存放在 OpenJDK 的 langtools/src/share/classes/com/sun/tools/javac 目錄中。要深入了解字節(jié)碼生成,閱讀 javac 的源碼是個很好的途徑,不過 javac 對于我們這個例子來說太過龐大了。在 Java 里面除了 javac 和字節(jié)碼類庫外,使用字節(jié)碼生成的例子還有很多,如 Web 服務(wù)器中的 JSP 編譯器,編譯時植入的 AOP 框架,還有很常用的動態(tài)代理技術(shù),甚至在使用反射的時候虛擬機都有可能會在運行時生成字節(jié)碼來提高執(zhí)行速度。我們選擇其中相對簡單的動態(tài)代理來看看字節(jié)碼生成技術(shù)是如何影響程序運作的。
相信許多 Java 開發(fā)人員都使用過動態(tài)代理,即使沒有直接使用過 java.lang.reflect.Proxy 或?qū)崿F(xiàn)過 java.lang.reflect.InvocationHandler 接口,應(yīng)該也用過 Spring 來做過 Bean 的組織管理。如果使用過 Spring,那大多數(shù)情況都會用過動態(tài)代理,因為如果 Bean 是面向接口編程,那么在 Spring 內(nèi)部都是通過動態(tài)代理的方式來對 Bean 進行增強的。動態(tài)代理中所謂的 “動態(tài)”,是針對使用 Java 代碼實際編寫了代理類的 “靜態(tài)” 代理而言的,它的優(yōu)勢不在于省去了編寫代理類哪一點工作量,而是實現(xiàn)了可以在原始類和接口還未知的時候,就確定代理類的代理行為,當(dāng)代理類與原始類脫離直接聯(lián)系后,就可以很靈活地重用于不同的應(yīng)用場景之中。
代碼清單 9-1 演示了一個最簡單的動態(tài)代理的用法,原始的邏輯是打印一句 “hello world”,代理類的邏輯是在原始類方法執(zhí)行前打印一句 “welcome”。我們先看一下代碼,然后再分析 JDK 是如何做到的。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class DynamicProxyTest {
interface IHello {
void sayHello();
}
static class Hello implements IHello {
@Override
public void sayHello() {
System.out.println("hello world");
}
}
static class DynamicProxy implements InvocationHandler {
Object originalObj;
Object bind(Object originalObj) {
this.originalObj = originalObj;
return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(),
originalObj.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("welcome");
return method.invoke(originalObj, args);
}
}
public static void main(String[] args) throws Exception {
IHello hello = (IHello) new DynamicProxy().bind(new Hello());
hello.sayHello();
}
}
運行結(jié)果:
welcome
hello world
上述代碼里,唯一的 “黑匣子” 就是 Proxy.newProxyInstance() 方法,除此之外再沒有任何特殊之處。這個方法返回一個實現(xiàn)了 IHello 的接口,并且代理了 new Hello() 實例行為的對象。跟蹤這個方法的源碼,可以看到程序進行了驗證、優(yōu)化、緩存、同步、生成字節(jié)碼、顯式類加載等操作,前面的步驟并不是我們關(guān)注的重點,而最后它調(diào)用了 sun.misc.ProxyGenerator.generateProxyClass() 方法來完成生成字節(jié)碼的動作,這個方法可以在運行時產(chǎn)生一個描述代理類的字節(jié)碼 byte[] 數(shù)組。如果想看一看這個再運行時產(chǎn)生的代理類中寫了什么,可以在main() 方法中加入下面這句:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
加入這句代碼后再次運行程序,磁盤中將會產(chǎn)生一個名為 “$Proxy0.class” 的代理類 Class 文件(注:應(yīng)該先在【項目目錄】非【ClassPath 目錄】下,建立和包名對應(yīng)的文件夾,如圖 a 所示),反編譯后可以看見如代碼清單 9-2 所示的內(nèi)容。
package org.fenixsoft.def;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0
extends Proxy
implements DynamicProxyTest.IHello
{
private static Method m3;
private static Method m1;
private static Method m0;
private static Method m2;
public $Proxy0(InvocationHandler paramInvocationHandler)
{
super(paramInvocationHandler);
}
public final void sayHello()
{
try
{
this.h.invoke(this, m3, null);
return;
}
catch (Error|RuntimeException localError)
{
throw localError;
}
catch (Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
// 此處由于版面原因,省略 equals()、hashCode()、toString() 三個方法的代碼
// 這 3 個方法的內(nèi)容與 sayHello() 非常相似
static
{
try
{
m3 = Class.forName("org.fenixsoft.def.DynamicProxyTest$IHello").getMethod("sayHello", new Class[0]);
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
return;
}
catch (NoSuchMethodException localNoSuchMethodException)
{
throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
}
catch (ClassNotFoundException localClassNotFoundException)
{
throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
}
}
}
這個代理類的實現(xiàn)代碼也很簡單,它為傳入接口中的每一個方法,以及從 java.lang.Object 中繼承來的 equals()、hashCode()、toString() 方法都生成了對應(yīng)的實現(xiàn),并且統(tǒng)一調(diào)用了 InvocationHandler 對象的 invoke() 方法(代碼中的 “this.h” 就是父類 Proxy 中保存的 InvocationHandler 實例變量)來實現(xiàn)這些方法的內(nèi)容,各個方法的區(qū)別不過是傳入的參數(shù)和 Method 對象有所不同而已,所以無論調(diào)用動態(tài)代理的哪一個方法,實際上都是在執(zhí)行 InvocationHandler.invoke() 中的代理邏輯。
這個例子中并沒有講到 generateProxyClass() 方法具體是如何產(chǎn)生代理類 “$Proxy0.class” 的字節(jié)碼的,大致的生成過程其實就是根據(jù) Class 文件的格式規(guī)范去拼裝字節(jié)碼,但在實際開發(fā)中,以 byte 為單位直接拼裝出字節(jié)碼的應(yīng)用場合很少見,這種生成方式也只能產(chǎn)生一些高度模板化的代碼。對于用戶的程序代碼來說,如果有要大量操作字節(jié)碼的需求,還是使用封裝好的字節(jié)碼類庫比較合適。如果讀者對動態(tài)代理的字節(jié)碼拼裝過程很感興趣,可以在 OpenJDK 的 jdk/src/share/classes/sun/misc 目錄下找到sun.misc.ProxyGenerator 的源碼。
四、retrotranslator跨域JDK版本
一般來說,以 “做項目” 為主的軟件公司比較容易更新技術(shù),在下一個項目中換一個技術(shù)框架、升級到最新的 JDK 版本,甚至把 Java 換成 C#、C++ 來開發(fā)程序都是由可能的。但當(dāng)公司發(fā)展壯大,技術(shù)有所積累,逐漸成為 “做產(chǎn)品” 為主的軟件公司后,自主選擇技術(shù)的權(quán)利就會喪失掉,因為之前所積累的代碼和技術(shù)都是用真金白銀換來的,一個穩(wěn)健的團隊也不會隨意地改變底層的技術(shù)。然而在飛速發(fā)展的程序設(shè)計領(lǐng)域,新技術(shù)總是日新月異、層出不窮,偏偏這些新技術(shù)又如鮮花之于蜜蜂一樣,對程序員散發(fā)著天然的吸引力。
在 Java 世界里,每一次 JDK 大版本的發(fā)布,都伴隨著一場大規(guī)模的技術(shù)革新,而對 Java 程序編寫習(xí)慣改變最大的,無疑是 JDK 1.5 的發(fā)布。自動裝箱、泛型、動態(tài)注解、枚舉、變長參數(shù)、遍歷循環(huán)(foreach 循環(huán))……事實上,在沒有這些語法特性的年代,Java 程序也照樣能寫,但是現(xiàn)在看來,上述每一種語法的改進幾乎都是 “必不可少” 的。就如同習(xí)慣了 24 寸液晶顯示器的程序員,很難習(xí)慣在 15 寸平顯示器上編寫代碼。但假如 “不幸” 因為要保護現(xiàn)有投資、維持程序結(jié)構(gòu)穩(wěn)定等,必須使用 1.5 以前版本的 JDK 呢?我們沒有辦法把 15 寸顯示器變成 24 寸的,但卻可以跨越 JDK 版本之間的溝壑,把 JDK 1.5 中編寫的代碼放到 JDK 1.4 或 1.3 的環(huán)境去部署使用。為了解決這個問題,一種名為 “Java 逆向移植” 的工具(Java Backporting Tools)應(yīng)運而生,Retrotranslator 是這類工具中較出色的一個。
Retrotranslator 的作用是將 JDK 1.5 編譯出來的 Class 文件轉(zhuǎn)變?yōu)榭梢栽?JDK 1.4 或 1.3 上部署的版本,它可以很好地支持自動裝箱、泛型、動態(tài)注解、枚舉、變長參數(shù)、遍歷循環(huán)、靜態(tài)導(dǎo)入這些語法特性,甚至還可以支持 JDK 1.5 中新增的集合改進、并發(fā)包以及對泛型、注解等的反射操作。了解了 Retrotranslator 這種逆向移植工具可以做什么以后,現(xiàn)在關(guān)心的是它是怎樣做到的?
要想知道 Retrotranslator 如何在舊版本 JDK 中模擬新版本 JDK 的功能,首先要弄清楚 JDK 升級中會提供哪些新的功能。JDK 每次升級新增的功能大致可以分為以下 4 類:
在編譯器層面做的改進。如自動裝箱拆箱,實際上就是編譯器在程序中使用到包裝對象的地方自動插入了很多 Integer.valueOf()、Float.valueOf() 之類的代碼;變長參數(shù)在編譯之后就自動轉(zhuǎn)化成一個數(shù)組來完成參數(shù)傳遞;泛型的信息則在編譯階段就已經(jīng)擦除掉了(但是在元數(shù)據(jù)中還保留著),相應(yīng)的地方被編譯器自動插入了類型轉(zhuǎn)換代碼。
對 Java API 的代碼增強。譬如 JDK 1.2 時代引入的 java.util.Collections 等一系列集合類,在 JDK 1.5 時代引入的 java.util.concurrent 并發(fā)包等。
需要在字節(jié)碼中進行支持的改動。如 JDK 1.7 里面新加入的語法特性:動態(tài)語言支持,就需要在虛擬機中新增一條 invokedynamic 字節(jié)碼指令來實現(xiàn)相關(guān)的調(diào)用功能。不過字節(jié)碼指令集一直處于相對比較穩(wěn)定的狀態(tài),這種需要在字節(jié)碼層面直接進行的改動是比較少見的。
虛擬機內(nèi)部的改進。如 JDK 1.5 中實現(xiàn)的 JSR-133 規(guī)范重新定義的 Java 內(nèi)存模型(Java Memory Model,JMM)、CMS 收集器之類的改動,這類改動對于程序員編寫代碼基本是透明的,但會對程序運行時產(chǎn)生影響。
上述 4 類新功能中,Retrotranslator 只能模擬前兩類,對于后面兩類直接在虛擬機內(nèi)部實現(xiàn)的改進,一般所有的逆向移植工具都是無能為力的,至少不能完整地或者再可接受的效率上完成全部模擬,否則虛擬機設(shè)計團隊也沒有必要舍近求遠地改動處于 JDK 底層的虛擬機。在可以模擬的兩類功能中,第二類模擬相對更容易實現(xiàn)一些,如 JDK 1.5 引入的 java.util.concurrent 包,實際是由多線程大師 Doug Lea 開發(fā)的一套并發(fā)包,在 JDK 1.5 出現(xiàn)之前就已經(jīng)存在(那時候名字叫做 dl.util.concurrent,引入 JDK 時由作者和 JDK 開發(fā)團隊共同做了一些改進),所以要在舊的 JDK 中支持這部分功能,以獨立類庫的方式便可實現(xiàn)。Retrotranslator 中附帶了一個名叫 “backport-util-concurrent.jar” 的類庫(由另一個名為 “Backport of JSR 166” 的項目所提供)來代替 JDK 1.5 的并發(fā)包。
至于 JDK 在編譯階段進行處理的那些改進,Retrotranslator 則是使用 ASM 框架直接對字節(jié)碼進行處理。由于組成 Class 文件的字節(jié)碼指令數(shù)量并沒有改變,所以無論是 JDK 1.3、JDK 1.4 還是 JDK 1.5,能用字節(jié)碼表達的語義范圍應(yīng)該是一直的。當(dāng)然,肯定不可能簡單地把 Class 的文件版本號從 49.0 改回 48.0 就能解決問題了,雖然字節(jié)碼指令的數(shù)量沒有變化,但是元數(shù)據(jù)信息和一些語法支持的內(nèi)容還是要做相應(yīng)的修改。以枚舉為例,在 JDK 1.5 中增加了 enum 關(guān)鍵字,但是 Class 文件常量池的 CONSTANT_Class_info 類型常量并沒有發(fā)生任何語義變化,仍然是代表一個類或接口的符號引用,沒有加入枚舉,也沒有增加過 “CONSTANT_Enum_info” 之類的 “枚舉符號引用” 常量。所以使用 enum 關(guān)鍵字定義常量,雖然從 Java 語法上看起來與使用 class 關(guān)鍵字定義類、使用 interface 關(guān)鍵字定義接口是同一層次的,但實際上這是由 Javac 編譯器做出來的假象,從字節(jié)碼的角度來看,枚舉僅僅是一個繼承于 java.lang.Enum、自動生成了 values() 和 valueOf() 方法的普通 Java 類而已。
Retrotranslator 對枚舉所做的主要處理就是把枚舉類的父類從 “java.lang.Enum” 替換位它運行時類庫中包含的 “net.sf.retrotranslator.runtime.java.lang.Enum_”,然后再在類和字段的訪問標(biāo)志中抹去 ACC_ENUM 標(biāo)志位。當(dāng)然,這只是處理的總體思路,具體的實現(xiàn)要比上面說的復(fù)雜得多。可以想象既然兩個父類實現(xiàn)都不一樣,values() 和 valueOf() 的方法自然需要重寫,常量池需要引入大量新的來自父類的符號引用,這些都是實現(xiàn)細節(jié)。圖 9-3 是一個使用 JDK 1.5 編譯的枚舉類與被 Retrotranslator 轉(zhuǎn)換處理后的字節(jié)碼的對比圖。

五、自己手動實現(xiàn)遠程執(zhí)行能力
不知道讀者在做程序維護的時候是否遇到過這類情形:排查問題的過程中,想查看內(nèi)存中的一些參數(shù)值,卻又沒有方法把這些值輸出到界面或日志中,又或者定位到某個緩存數(shù)據(jù)有問題,但缺少緩存的同一管理界面,不得不重啟服務(wù)才能清理這個緩存。類似的需求又一個共同的特點,那就是只要在服務(wù)中執(zhí)行一段程序代碼,就可以定位或排除問題,但就是偏偏找不到可以讓服務(wù)器執(zhí)行臨時代碼的途徑,這時候就會希望 Java 服務(wù)器中也有提供類似 Groovy Console 的功能。
JDK 1.6 之后提供了 Compiler API,可以動態(tài)地編譯 Java 程序,雖然這樣達不到動態(tài)語言的靈活度,但讓服務(wù)器執(zhí)行臨時代碼的需求就可以得到解決了。在 JDK 1.6 之前,也可以通過其他方式來做到,譬如寫一個 JSP 文件上傳到服務(wù)器,然后在瀏覽器中運行它,或者在服務(wù)器端程序中加入一個 BeanShell Script、JavaScript 等的執(zhí)行引擎(如 Mozilla Rhino)去執(zhí)行動態(tài)腳本。在本章的實戰(zhàn)部分,我們將使用前面學(xué)到的關(guān)于類加載及虛擬機執(zhí)行子系統(tǒng)的知識去實現(xiàn)在服務(wù)端執(zhí)行臨時代碼的功能。
目標(biāo)
首先,在實現(xiàn) “在服務(wù)端執(zhí)行臨時代碼” 這個需求之前,先來明確一下本次實戰(zhàn)的具體目標(biāo),我們希望最終的產(chǎn)品是這樣的:
- 不依賴 JDK 版本,能在目前還普遍使用的 JDK 中部署,也就是使用 JDK 1.4 ~ JDK 1.7 都可以運行。
- 不改變原有服務(wù)端程序的部署,不依賴任何第三方類庫。
- 不侵入原有程序,即無須改動原程序的任何代碼,也不會對原有程序的運行帶來任何影響。
考到 BeanShell Script 或 JavaScript 等腳本編寫起來不太方便,“臨時代碼” 需要直接支持 Java 語言。 - “臨時代碼” 應(yīng)當(dāng)具備足夠的自由度,不需要依賴特定的類或?qū)崿F(xiàn)特定的接口。這里寫的是 “不需要” 而不是 “不可以”,當(dāng) “臨時代碼” 需要引用其他類庫時也沒有限制,只要服務(wù)端程序能使用的,臨時代碼應(yīng)當(dāng)都能直接引用。
- “臨時代碼” 的執(zhí)行結(jié)果能返回客戶端,執(zhí)行結(jié)果可以包括程序中輸出的信息及拋出的異常等。
看完上面列出的目標(biāo),你覺得完成這個需求需要做多少工作呢?也許答案比大多數(shù)人所想的都要簡單一些:5 個類,250 行代碼(含注釋),大約一個半小時左右的開發(fā)時間久可以了,現(xiàn)在就開始編寫程序吧!
思路
在程序?qū)崿F(xiàn)的過程中,我們需要解決以下 3 個問題:
- 如何編譯提交到服務(wù)器的 Java 代碼?
- 如何執(zhí)行編譯之后的 Java 代碼?
- 如何收集 Java 代碼的執(zhí)行結(jié)果?
對于第一個問題,我們有兩種思路可以選擇,一種是使用 tools.jar 包(在 Sun JDK/lib 目錄下)中的 com.sun.tools.javac.Main 類來編譯 Java 文件,這其實和使用 javac 命令編譯是一樣的。這種思路的缺點的引入了額外的 JAR 包,而且把程序 “綁死” 在 Sun 的 JDK 上了,要部署到其他公司的 JDK 中還得把 tools.jar 帶上(雖然 JRockit 和 J9 虛擬機也有這個 JAR 包,但它總不是標(biāo)準(zhǔn)所規(guī)定必須存在的)。另外一種思路是直接在客戶端編譯好,把字節(jié)碼而不是 Java 代碼傳到服務(wù)端,這聽起來好像有點投機取巧,一般來說確實不應(yīng)該假定客戶端一定具有編譯代碼的能力,但是既然程序員會寫 Java 代碼去給服務(wù)端排查問題,那么很難想象他的機器上會連編譯 Java 程序的環(huán)境都沒有。
對于第二個問題,簡單地一想:要執(zhí)行編譯后的 Java 代碼,讓類加載器加載這個類生成一個 Class 對象,然后反射調(diào)用一下某個方法就可以了(因為不實現(xiàn)任何接口,我們可以借用一下 Java 中人人皆知的 “main()” 方法)。但我們還應(yīng)該考慮得更周全些:一段程序往往不是編寫、運行一次就能達到效果,同一個類可能要反復(fù)地修改、提交、執(zhí)行。另外,提交上去的類要能訪問服務(wù)端的其他類庫才行。還有,既然提交的是臨時代碼,那提交的 Java 類在執(zhí)行完成后就應(yīng)當(dāng)能卸載和回收。
最后的一個問題,我們想把程序往標(biāo)準(zhǔn)輸出(System.out)和標(biāo)準(zhǔn)錯誤輸出(System.err)中打印的信息收集起來,但標(biāo)準(zhǔn)輸出設(shè)備是整個虛擬機進程全局共享的資源,如果使用 System.setOut()/System.setErr() 方法把輸出流重定向到自己定義的 PrintStream 對象上固然可以收集輸出信息,但也會對原有程序產(chǎn)生影響:會把其他線程向標(biāo)準(zhǔn)輸出中打印的信息也收集了。雖然這些并不是不能解決的問題,不過為了達到完全不影響原程序的目的,我們可以采用另外一種辦法,即直接在執(zhí)行的類中把對 System.out 的符號引用替換為我們準(zhǔn)備的 PrintStream 的符號引用,依賴前面學(xué)習(xí)的只是,做到這一點并不困難。
實現(xiàn)
在程序?qū)崿F(xiàn)部分,我們主要看一下代碼及其注釋。首先看看實現(xiàn)過程中需要用到的 4 個支持類。第一個類用于實現(xiàn) “同一個類的代碼可以被多次加載” 這個需求,具體程序如代碼清單 9-3 所示。
代碼清單 9-3 HotSwapClassLoader 的實現(xiàn)
/**
* 為了多次載入執(zhí)行類而加入的加載器 <br>
* 把 defineClass 方法開放出來,只有外部顯式調(diào)用的時候才會使用到 loadByte 方法
* 由虛擬機調(diào)用時,仍然按照原有的雙親委派規(guī)則使用 loadClass 方法進行類加載
*
*/
public class HotSwapClassLoader extends ClassLoader{
public HotSwapClassLoader() {
super(HotSwapClassLoader.class.getClassLoader());
}
public Class loadByte(byte[] classByte) {
return defineClass(null, classByte, 0, classByte.length);
}
}
HotSwapClassLoader 所做的事情僅僅是公開父類(即 java.lang.ClassLoader) 中的 protected 方法 defineClass(),我們將會使用這個方法把提交執(zhí)行的 Java 類的 byte[] 數(shù)組轉(zhuǎn)變?yōu)?Class 對象。HotSwapClassLoader 中并沒有重寫 loadClass() 或 findClass() 方法,因此如果不算外部手工調(diào)用 loadByte() 方法的話,這個類加載器的類查找范圍與它的父類加載器是完全一致的,在被虛擬機調(diào)用時,它會按照雙親委派模型交給父類加載。構(gòu)造函數(shù)中指定為加載 HotSwapClassLoader 類的類加載器作為父類加載器,這一步是實現(xiàn)提交的執(zhí)行代碼可以訪問服務(wù)端引用類庫的關(guān)鍵,下面我們來看看代碼清單 9-3。
第二個類是實現(xiàn)將 java.lang.System 替換為我們自己定義的 HackSystem 類的過程,它直接修改符合 Class 文件格式的 byte[] 數(shù)組中的常量池部分,將常量池中指定內(nèi)容的 CONSTANT_Utf8_info 常量替換為新的字符串,具體代碼如代碼清單 9-4 所示。ClassModifier 中設(shè)計對 byte[] 數(shù)組操作的部分,主要是將 byte[] 與 int 和 String 互相轉(zhuǎn)換,以及把對 byte[] 數(shù)據(jù)的替換操作封裝在代碼清單 9-5 所示的 ByteUtils 中。
代碼清單 9-4 ClassModifier 的實現(xiàn)
/**
* 修改 Class 文件,暫時只提供修改常量池常量的功能
*
*/
public class ClassModifier {
/**
* Class 文集中常量池的起始偏移
*/
private static final int CONSTANT_POOL_COUNT_INDEX = 8;
/**
* CONSTANT_Utf8_info 常量的 tag 標(biāo)志
*/
private static final int CONSTANT_Utf8_info = 1;
/**
* 常量池中 11 種常量所占的長度,CONSTANT_Utf8_info 型常量除外,因為它不是定長的
*/
private static final int[] CONSTATN_ITEM_LENGTH = { -1, -1, -1, 5, 5, 9, 9,
3, 3, 5, 5, 5, 5 };
private static final int u1 = 1;
private static final int u2 = 2;
private byte[] classByte;
public ClassModifier(byte[] classByte) {
this.classByte = classByte;
}
/**
* 修改常量池 CONSTANT_Utf8_info 常量的內(nèi)容
* @param oldStr 修改前的字符串
* @param newStr 修改后的字符串
* @return 修改結(jié)果
*/
public byte[] modifyUTF8Constant(String oldStr, String newStr) {
int cpc = getConstantPoolCount();
int offset = CONSTANT_POOL_COUNT_INDEX + u2;
for (int i = 0; i < cpc; i++) {
int tag = ByteUtils.bytes2Int(classByte, offset, u1);
if (tag == CONSTANT_Utf8_info) {
int len = ByteUtils.bytes2Int(classByte, offset + u1, u2);
offset += (u1 + u2);
String str = ByteUtils.bytes2String(classByte, offset, len);
if (str.equalsIgnoreCase(oldStr)) {
byte[] strBytes = ByteUtils.string2Bytes(newStr);
byte[] strLen = ByteUtils.int2Bytes(newStr.length(), u2);
classByte = ByteUtils.bytesReplace(classByte, offset - u2, u2, strLen);
classByte = ByteUtils.bytesReplace(classByte, offset, len, strBytes);
return classByte;
} else {
offset += len;
}
} else {
offset += CONSTATN_ITEM_LENGTH[tag];
}
}
return classByte;
}
/**
* 獲取常量池中常量的數(shù)量
* @return 常量池數(shù)量
*/
public int getConstantPoolCount() {
return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2);
}
}
代碼清單 9-5 ByteUtils 的實現(xiàn):
public class ByteUtils {
public static int bytes2Int(byte[] b, int start, int len) {
int sum = 0;
int end = start + len;
for (int i = start; i < end; i++) {
int n = ((int) b[i]) & 0xff;
n <<= (--len) * 8;
sum = n + sum;
}
return sum;
}
public static byte[] int2Bytes(int value, int len) {
byte[] b = new byte[len];
for (int i = 0; i < len; i++) {
b[len - i - 1] = (byte) ((value >> 8 * i) & 0xff);
}
return b;
}
public static String bytes2String(byte[] b, int start, int len) {
return new String(b, start, len);
}
public static byte[] string2Bytes(String str) {
return str.getBytes();
}
public static byte[] bytesReplace(byte[] originalBytes, int offset, int len, byte[] replaceBytes) {
byte[] newBytes = new byte[originalBytes.length + (replaceBytes.length - len)];
System.arraycopy(originalBytes, 0, newBytes, 0, offset);
System.arraycopy(replaceBytes, 0, newBytes, offset, replaceBytes.length);
System.arraycopy(originalBytes, offset + len, newBytes, offset + replaceBytes.
length, originalBytes.length - offset - len);
return newBytes;
}
}
經(jīng)過 ClassModifier 處理后的 byte[] 數(shù)組才會傳給 HotSwapClassLoader.loadByte() 方法進行類加載,byte[] 數(shù)組在這里替換符號引用之后,與客戶端直接在 Java 代碼中引用 HackSystem 類再編譯生成的 Class 是完全一樣的。這樣的實現(xiàn)既避免了客戶端編寫臨時執(zhí)行代碼時要依賴特定的類(不然無法引入 HackSytem),又避免了服務(wù)端修改標(biāo)準(zhǔn)輸出后影響到其他程序的輸出。下面我們來看看代碼清單 9-4 和代碼清單 9-5。
最后一個類類就是前面提到過的用來代替 java.lang.System 的 HackSystem,這個類中的方法看起來不少,但其實除了把 out 和 err 兩個靜態(tài)變量改成使用 ByteArrayOutputStream 作為打印目標(biāo)的同一個 PrintStream 對象,以及增加了讀取、清理 ByteArrayOutputStream 中內(nèi)容的 getBufferString() 和 clearBuffer() 方法外,就再沒有其他新鮮的內(nèi)容了。其余的方法全部來自于 System 類的 public 方法,方法名字、參數(shù)、返回值都完全一樣,并且實現(xiàn)也是直接轉(zhuǎn)調(diào)了 System 類的對應(yīng)方法而已。保留這些方法的目的,是為了在 System 被替換成 HackSystem 之后,執(zhí)行代碼中調(diào)用的 System 的其余方法仍然可以繼續(xù)使用,HackSystem 的實現(xiàn)如代碼清單 9-6 所示。
/**
* 為 JavaClass 劫持 java.lang.System 提供支持
* 除了 out 和 err 外,其余的都直接轉(zhuǎn)發(fā)給 System 處理
*
*/
public class HackSystem {
public final static InputStream in = System.in;
private static ByteArrayOutputStream buffer = new ByteArrayOutputStream();
public final static PrintStream out = new PrintStream(buffer);
public final static PrintStream err = out;
public static String getBufferString() {
return buffer.toString();
}
public static void clearBuffer() {
buffer.reset();
}
public static void setSecurityManager(final SecurityManager s) {
System.setSecurityManager(s);
}
public static SecurityManager getSecurityManager() {
return System.getSecurityManager();
}
public static long currentTimeMillis() {
return System.currentTimeMillis();
}
public static long nanoTime() {
return System.nanoTime();
}
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) {
System.arraycopy(src, srcPos, dest, destPos, length);
}
public static int identityHashCode(Object x) {
return System.identityHashCode(x);
}
// 下面所有的方法都與 java.lang.System 的名稱一樣
// 實現(xiàn)都是字節(jié)轉(zhuǎn)調(diào) System 的對應(yīng)方法
// 因版面原因,省略了其他方法
}
至此,4 個支持類已經(jīng)講解完畢,我們來看看最后一個類 JavaClassExecuter,它是提供給外部調(diào)用的入口,調(diào)用前面幾個支持類組裝邏輯,完成類加載工作。JavaClassExecuter 只有一個 execute() 方法,用輸入的符合 Clas 文件格式的 byte[] 數(shù)組替換 java.lang.System 的符號引用后,使用 HotSwapClassLoader 加載生成一個 Class 對象,由于每次執(zhí)行 execute() 方法都會生成一個新的類加載器實例,因此同一個類可以實現(xiàn)重復(fù)加載。然后,反射調(diào)用這個 Class 對象的 main() 方法,如果期間出現(xiàn)任何異常,將異常信息打印到 HackSystem.out 中,最后把緩沖區(qū)中的信息作為方法的結(jié)果返回。JavaClassExecuter 的實現(xiàn)代碼如代碼清單 9-7 所示。
代碼清單 9-7 JavaClassExecuter 的實現(xiàn):
/**
* JavaClass 執(zhí)行工具
*
*/
public class JavaClassExecuter {
/**
* 執(zhí)行外部傳過來的代表一個 Java 類的 byte 數(shù)組 <br>
* 將輸入類的 byte 數(shù)組中代表 java.lang.System 的 CONSTANT_Utf8_info 常量修改為劫持后的
* HackSystem 類
* 執(zhí)行方法為該類的 static main(String[] args) 方法,輸出結(jié)果為該類向 System.out/err
* 輸出的信息
*
* @param classByte 代表一個 Java 類的 byte 數(shù)組
* @return 執(zhí)行結(jié)果
*/
public static String execute(byte[] classByte) {
HackSystem.clearBuffer();
ClassModifier cm = new ClassModifier(classByte);
byte[] modiBytes = cm.modifyUTF8Constant("java/lang/System",
"org/fenixsoft/classloading/execute/HackSystem");
HotSwapClassLoader loader = new HotSwapClassLoader();
Class clazz = loader.loadByte(modiBytes);
try {
Method method = clazz.getMethod("main", new Class[] {String[].class });
method.invoke(null, new String[] { null });
} catch (Throwable e) {
e.printStackTrace(HackSystem.out);
}
return HackSystem.getBufferString();
}
}
驗證
遠程執(zhí)行功能的編碼到此就完成了,接下來就要檢驗一下我們的勞動成果了。如果只是測試的話,那么可以任意寫一個 Java 類,內(nèi)容無所謂,只要向 System.out 輸出信息即可,取名為 TestClass,同時放到服務(wù)器 C 盤的根目錄中。然后,建立一個 JSP 文件并加入如代碼清單 9-8 所示的內(nèi)容,就可以在瀏覽器中看到這個類的運行結(jié)果了。
代碼清單 9-8 測試 JSP:
<%@ page import="java.lang.*" %>
<%@ page import="java.io.*" %>
<%@ page import="org.fenixsoft.classloading.execute.*" %>
<%
InputStream is = new FileInputStream("c:/TestClass.class");
byte[] b = new byte[is.available()];
is.read(b);
is.close();
out.println("<textarea style='width:1000;height=800'>");
out.println(JavaClassExecuter.excute(b));
out.println("</textarea>");
%>
當(dāng)然,上面的做法只是用于測試和演示,實際使用這個 JavaExecuter 執(zhí)行器的時候,如果還要手工復(fù)制一個 Class 文件到服務(wù)器上就沒有什么意義了。筆者給這個執(zhí)行器寫了一個 “外殼”,是一個 Eclipse 插件,可以把 Java 文件編譯后傳輸?shù)椒?wù)器中,然后把執(zhí)行器的返回結(jié)果輸出到 Eclipse 的 Console 窗口里,這樣就可以在有靈感的時候隨時寫幾行調(diào)試代碼,放到測試環(huán)境的服務(wù)器上立即運行了。雖然實現(xiàn)簡單,但效果很不錯,對調(diào)試問題也非常有用,如圖 9-4 所示。
