類加載及執(zhí)行子系統(tǒng)的案例與實戰(zhàn)

在 Class 文件格式與執(zhí)行引擎這部分中贺归,用戶的程序能直接影響的內容并不太多毡琉,Class 文件以何種格式存儲流济,類型何時加載岸梨、如何連接喂很,以及虛擬機如何執(zhí)行字節(jié)碼指令等都是由虛擬機直接控制的行為,用戶程序無法對其進行改變偿凭。能通過程序進行操作的,主要是字節(jié)碼生成與類加載器這兩部分的功能,但僅僅在如何處理這兩點上寒屯,就已經出現(xiàn)了許多值得欣賞和借鑒的思路,這些思路后來成為了許多常用功能和程序實現(xiàn)的基礎黍少。在本章中寡夹,我們將看一下前面所學的知識在實際開發(fā)之中是如何應用的。
在案例分析部分仍侥,筆者準備了 4 個例子要出,關于類加載器和字節(jié)碼的案例各有兩個。并且這兩個領域的案例中各有一個案例是大多數 Java 開發(fā)人員都使用過的工具或技術农渊,另外一個案例雖然不一定每個人都使用過患蹂,但卻特別精彩地演繹出這個領域中的技術特性。希望這些案例能引起讀者的思考砸紊,并給讀者的日常工作帶來靈感传于。

一、tomcat正統(tǒng)的類加載器架構

主流的 Java Web 服務器醉顽,如 Tomcat沼溜、Jetty、WebLogic游添、WebSphere 或其他筆者沒有列舉的服務器系草,都實現(xiàn)了自己定義的類加載器(一般都不止一個)通熄。因為一個功能健全的 Web 服務器,要解決如下幾個問題:

  • 部署在同一個服務器上的兩個 Web 應用程序所使用的 Java 類庫可以實現(xiàn)相互隔離找都。這是最基本的需求唇辨,兩個不同的應用程序可能會依賴同一個第三方類庫的不同版本,不能要求一個類庫在一個服務器中只有一份能耻,服務器應當保證兩個應用程序的類庫可以互相獨立使用赏枚。
    部署在同一個服務器上的兩個 Web 應用程序所使用的 Java 類庫可以互相共享。這個需求也很常見晓猛,例如饿幅,用戶可能有 10 個使用 Spring 組織的應用程序部署在同一臺服務器上,如果把 10 份 Spring 分別存放在各個應用程序的隔離目錄中戒职,將會是很大的資源浪費——這主要倒不是浪費磁盤空間的問題栗恩,而是指類庫在使用時都要被加載到服務器內存,如果類庫不能共享帕涌,虛擬機的方法區(qū)就會很容易出現(xiàn)過度膨脹的風險摄凡。
  • 服務器需要盡可能地保證自身的安全不受部署的 Web 應用程序影響。目前蚓曼,有許多主流的 Java Web 服務器自身也是使用 Java 語言來實現(xiàn)的亲澡。因此,服務器本身也有類庫依賴的問題纫版,一般來說床绪,基于安全考慮,服務器所使用的類庫應該與應用程序的類庫相互獨立其弊。
    支持 JSP 應用的 Web 服務器癞己,大多數都需要支持 HotSwap 功能。我們知道梭伐,JSP 文件最終要編譯成 Java Class 才能由虛擬機執(zhí)行痹雅,但 JSP 文件由于其純文本存儲的特性,運行時修改的概率遠遠大于第三方類庫或程序自身的 Class 文件糊识。而且 ASP绩社、PHP 和 JSP 這些網頁應用也把修改后無須重啟作為一個很大的 “優(yōu)勢” 來看待,因此 “主流” 的 Web 服務器都會支持 JSP 生成類的熱替換赂苗,當然也有 “非主流” 的愉耙,如運行在生產模式(Producation Mode)下的 WebLogic 服務器默認就不會處理 JSP 文件的變化。

由于存在上述問題拌滋,在部署 Web 應用時朴沿,單獨的一個 ClassPath 就無法滿足需求了,所以各種 Web 服務器都 “不約而同” 地提供了好幾個 ClassPath 路徑供用戶存放第三方類庫,這些路徑一般都以 “l(fā)ib” 或 “classes” 命名赌渣。被放置到不同路徑中的類庫魏铅,具備不同的訪問范圍和服務對象,通常坚芜,每一個目錄都會有一個相對應的自定義類加載器去加載放置在里面的 Java 類庫÷倭悖現(xiàn)在,筆者就以 Tomcat 服務器(注:本案例中選用的是 Tomcat 5.x 服務器的目錄和類加載器結構货岭,在 Tomcat 6.x 的默認配置下,/common疾渴、/server 和 /shared 三個目錄已經合并到一起了)為例千贯,看一看 Tomcat 具體是如何規(guī)劃用戶類庫結構和類加載器的。

  • 在 Tomcat 目錄結構中搞坝,有 3 組目錄(“/common/”搔谴、“/server/” 和 “/shared/”)可以存放 Java 類庫,另外還可以加上 Web 應用程序自身的目錄 “/WEB-INF/”桩撮,一共 4 組敦第,把 Java 類庫放置在這些目錄中的含義分別如下。

放置在 /common 目錄中:類庫可被 Tomcat 和所有的 Web 應用程序共同使用店量。
放置在 /server 目錄中:類庫可被 Tomcat 使用芜果,對所有的 Web 應用程序都不可見。
放置在 /shared 目錄中:類庫可被所有的 Web 應用程序共同使用融师,但對 Tomcat 自己不可見右钾。
放置在 /WebApp/WEB-INF 目錄中:類庫僅僅可以被此 Web 應用程序使用,對 Tomcat 和其他 Web 應用程序都不可見旱爆。
為了支持這套目錄結構舀射,并對目錄里面的類庫進行加載和隔離,Tomcat 自定義了多個類加載器怀伦,這些類加載器按照經典的雙親委派模型來實現(xiàn)脆烟,其關系如圖 9-1 所示。

image.png

灰色背景的 3 個類加載器是 JDK 默認提供的類加載器房待,這 3 個加載器的作用前面已經介紹過了邢羔。而 CommonClassLoader、CatalinaClassLoader吴攒、SharedClassLoader 和 WebAppClassLoader 則是 Tomcat 自己定義的類加載器张抄,它們分別加載 /common/、/server/洼怔、/shared/* 和 /WebApp/WEB-INF/* 中的 Java 類庫署惯。其中 WebApp 類加載器和 Jsp 類加載器通常會存在多個實例,每一個 Web 應用程序對應一個 WebApp 類加載器镣隶,每一個 JSP 文件對應一個 Jsp 類加載器极谊。

從圖 9-1 的委派關系中可以看出诡右,CommonClassLoader 能加載的類都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加載的類則與對方相互隔離轻猖。WebAppClassLoader 可以使用 SharedClassLoader 加載到的類帆吻,但各個 WebAppClassLoader 實例之間相互隔離。而 JasperLoader 的加載范圍僅僅是這個 JSP 文件所編譯出來的那一個 Class咙边,它出現(xiàn)的目的就是為了被丟棄:當服務器檢測到 JSP 文件被修改時猜煮,會替換掉目前的 JasperLoader 的實例,并通過再建立一個新的 Jsp 類加載器來實現(xiàn) JSP 文件的 HotSwap 功能败许。

對于 Tomcat 的 6.x 版本王带,只有指定了 tomcat/conf/catalina.properties 配置文件的 server.loader 和 share.loader 項后才會真正建立 CatalinaClassLoader 和 SharedClassLoader 的實例,否則會用到這兩個類加載器的地方都會用 CommonClassLoader 的實例代替市殷,而默認的配置文件中沒有設置這兩個 loader 項愕撰,所以 Tomcat 6.x 順理成章地把 /common、/server 和 /shared 三個目錄默認合并到一起變成一個 /lib 目錄醋寝,這個目錄里的類庫相當于以前 /common 目錄中類庫的作用搞挣。這是 Tomcat 設計團隊為了簡化大多數的部署場景所做的一項改進,如果默認設置不能滿足需要音羞,用戶可以通過修改配置文件指定 server.loader 和 share.loader 的方式重新啟用 Tomcat 5.x 的加載器架構囱桨。

Tomcat 加載器的實現(xiàn)清晰易懂,并且采用了官方推薦的 “正統(tǒng)” 的使用類加載器的方式黄选。如果讀者閱讀完上面的案例后蝇摸,能完全理解 Tomcat 設計團隊這樣布置加載器架構的用意,那說明已經大致掌握了類加載器 “主流”的使用方式办陷,那么筆者不妨再提一個問題來讓讀者思考一下:前面曾經提到過一個場景貌夕,如果有 10 個 Web 應用程序都是用 Spring 來進行組織和管理的話,可以把 Spring 放到 common 或 shared 目錄下讓這些程序共享民镜。Spring 要對用戶程序的類進行管理啡专,自然要能訪問到用戶程序的類,而用戶的程序顯然是放在 /WebApp/WEB-INF 目錄中的制圈,那么被 CommonClassLoader 或 SharedClassLoader 加載的 Spring 如何訪問并不在其加載范圍的用戶程序呢们童?

二、osgi靈活的類加載器架構

Java 程序社區(qū)中流傳著這么一個觀點:“學習 JEE 規(guī)范鲸鹦,去看 JBoss 源碼慧库;學習類加載器,就去看 OSGi 源碼”馋嗜。盡管 “JEE 規(guī)范” 和 “類加載器的知識” 并不是一個對等的概念齐板,不過,既然這個觀點能在程序員中流傳開來,也從側面說明了 OSGi 對類加載器的運用確實有其獨到之處甘磨。

OSGi(Open Service Gateway Initiative) 是 OSGi 聯(lián)盟(OSGi Alliance)制定的一個基于 Java 語言的動態(tài)模塊化規(guī)范橡羞,這個規(guī)范最初由 Sun、IBM济舆、愛立信等公司聯(lián)合發(fā)起卿泽,目的是使用服務提供商通過住宅網關為各種家用智能設備提供各種服務,后來這個規(guī)范在 Java 的其他技術領域也有相當不錯的發(fā)展滋觉,現(xiàn)在已經成為 Java 世界中 “事實上” 的模塊化標準签夭,并且已經有了 Equinox、Felix 等成熟的實現(xiàn)椎侠。OSGi 在 Java 程序員中最著名的應用案例就是 Eclipse IDE覆致,另外還有許多大型的軟件平臺和中間件服務器都基于或聲明將會基于 OSGi 規(guī)范來實現(xiàn),如 IBM Jazz 平臺肺蔚、GlassFish 服務器、JBoss OSGi 等儡羔。

OSGi 中的每個模塊(稱為 Bundle)與普通的 Java 類庫區(qū)別并不太大宣羊,兩者一般都以 JAR 格式進行封裝,并且內部存儲的都是 Java Package 和 Class汰蜘。但是一個 Bundle 可以聲明它所依賴的 Java Package(通過 Import-Package 描述)仇冯,也可以聲明它允許導出發(fā)布的 Java Package(通過 Export-Package 描述)。在 OSGi 里面族操,Bundle 之間的依賴關系從傳統(tǒng)的上層模塊依賴底層模塊轉變?yōu)槠郊壞K之間的依賴(至少外觀上如此)苛坚,而且類庫的可見性能得到非常精確的控制,一個模塊里只有被 Export 過的 Package 才可能由外界訪問色难,其他的 Package 和 Class 將會隱藏起來泼舱。除了更精確的模塊劃分和可見性控制外,引入 OSGi 的另外一個重要理由是枷莉,基于 OSGi 的程序很可能(只是很可能娇昙,并不是一定會)可以實現(xiàn)模塊級的熱插拔功能,當程序升級更新或調試除錯時笤妙,可以只停用冒掌、重新安裝然后啟用程序的其中一部分,這對企業(yè)級程序開發(fā)來說是一個非常有誘惑力的特性蹲盘。

OSGi 之所以能有上述 “誘人” 的特點股毫,要歸功于它靈活的類加載器架構。OSGi 的 Bundle 類加載器之間只有規(guī)則召衔,沒有固定的委派關系铃诬。例如,某個 Bundle 聲明了一個它依賴的 Package,如果有其他 Bundle 聲明發(fā)布了這個 Package氧急,那么所有對這個 Package 的類加載動作都會委派給發(fā)布它的 Bundle 類加載器去完成颗胡。不涉及某個具體的 Package 時,各個 Bundle 加載器都是平級關系吩坝,只有具體使用某個 Package 和 Class 的時候毒姨,才會根據 Package 導入導出定義來構造 Bundle 間的委派和依賴。

另外钉寝,一個 Bundle 類加載器為其他 Bundle 提供服務時弧呐,會根據 Export-Package 列表嚴格控制訪問范圍。如果一個類存在于 Bundle 的類庫中但是沒有被 Export嵌纲,那么這個 Bundle 的類加載器能找到這個類俘枫,但不會提供給其他 Bundle 使用,而且 OSGi 平臺也不會把其他 Bundle 的類加載請求分配給這個 Bundle 來處理逮走。

我們可以舉一個更具體一些的簡單例子鸠蚪,假設存在 Bundle A、Bundle B师溅、Bundle C 三個模塊茅信,并且這三個 Bundle 定義的依賴關系如下。

Bundle A:聲明發(fā)布了 packageA墓臭,依賴了 java.* 的包蘸鲸。
Bundle B:聲明依賴了 packageA 和 packageC,同時也依賴了 java.* 的包窿锉。
Bundle C:聲明發(fā)布了 packageC酌摇,依賴了 packageA。
那么嗡载,這三個 Bundle 之間的類加載器及父類加載器之間的關系如圖 9-2 所示窑多。


image.png

由于沒有牽扯到具體的 OSGi 實現(xiàn),所以圖 9-2 中的類加載器都沒有指明具體的加載器實現(xiàn)洼滚,只是一個體現(xiàn)了加載器之間關系的概念模型怯伊,并且只是體現(xiàn)了 OSGi 中最簡單的加載器委派關系。一般來說判沟,在 OSGi 中耿芹,加載一個類可能發(fā)生的查找行為和委派關系會比圖 9-2 中顯示的復雜得多,類加載時可能進行的查找規(guī)則如下:

以 java.* 開頭的類挪哄,委派給父類加載器加載吧秕。
否則,委派列表名單內的類迹炼,委派給父類加載器加載砸彬。
否則颠毙,Import 列表中的類,委派給 Export 這個類的 Bundle 的類加載器加載砂碉。
否則蛀蜜,查找當前 Bundle 的 Classpath,使用自己的類加載器加載增蹭。
否則滴某,查找是否在自己的 Fragment Bundle 中,如果是滋迈,則委派給 Fragment Bundle 的類加載器加載霎奢。
否則,查找 Dynamic Import 列表的 Bundle饼灿,委派給對應 Bundle 的類加載器加載幕侠。
否則,類查找失敗碍彭。

從圖 9-2 中還可以看出晤硕,在 OSGi 里面,加載器之間的關系不再是雙親委派模型的屬性結構庇忌,而是已經進一步發(fā)展成了一種更為復雜的窗骑、運行時才能確定的網狀結構。這種網狀的類加載器架構在帶來更好的靈活性的同時漆枚,也可能會產生許多新的隱患。筆者曾經參與過將一個非 OSGi 的大型系統(tǒng)向 Equinox OSGi 平臺遷移的項目抵知,由于歷史原因墙基,代碼模塊之間的的依賴關系錯綜復雜,勉強分離出各個模塊的 Bundle 后刷喜,發(fā)現(xiàn)在高并發(fā)環(huán)境下經常出現(xiàn)死鎖残制。我們很容易就找到了死鎖的原因:如果出現(xiàn)了 Bundle A 依賴于 Bundle B 的 Package B,而 Bundle B 又依賴了 Bundle A 的 Package A掖疮,這兩個 Bundle 進行類加載時就很容易發(fā)生死鎖初茶。具體情況是當 Bundle A 加載 Package B 的類時,首先需要鎖定當前類加載器的實例對象(java.lang.ClassLoader.loadClass() 是一個 synchronized 方法)浊闪,然后把請求委派給 Bundle B 的加載器處理恼布,但如果這時候 Bundle B 也正好想加載 Package A 的類,它也先鎖定自己的加載器再去請求 Bundle A 的加載器處理搁宾,這樣折汞,兩個加載器都在等待對方處理自己的請求,而對方處理完之前自己又一直處于同步鎖定的狀態(tài)盖腿,因此它們就互相死鎖爽待,永遠無法完成加載請求了损同。Equinox 的 Bug List 中有關于這類問題的 Bug,也提供了一個以犧牲性能為代價的解決方案——用戶可以啟用 osgi.classloader.singleThreadLoads 參數來按單線程串行化的方式強制進行類加載器動作鸟款。在 JDK 1.7 中膏燃,為非樹狀繼承關系下的類加載器架構進行了一次專門的升級,目的是從底層避免這類死鎖出現(xiàn)的可能何什。

總體來說组哩,OSGi 描繪了一個很美好的模塊化開發(fā)的目標,而且定義了實現(xiàn)這個目標所需要的各種服務富俄,同時也有成熟框架對其提供實現(xiàn)支持禁炒。對于單個虛擬機下的應用,從開發(fā)初期就建立在 OSGi 是一個很不錯的選擇霍比,這樣便于約束依賴幕袱。但并非所有的應用都適合采用 OSGi 作為基礎架構,OSGi 在提供強大功能的同時悠瞬,也引入了額外的復雜度们豌,帶來了線程死鎖和內存泄露的風險。

三浅妆、字節(jié)碼生成技術與動態(tài)代理的實現(xiàn)

“字節(jié)碼生成” 并不是什么高深的技術望迎,讀者在看到 “字節(jié)碼生成” 這個標題時也不必去向諸如 Javassit、CGLib凌外、ASM 之類的字節(jié)碼類庫辩尊,因為 JDK 里面的 javac 命令就是字節(jié)碼生成技術的 “老祖宗”,并且 javac 也是一個由 Java 語言寫成的程序康辑,它的代碼存放在 OpenJDK 的 langtools/src/share/classes/com/sun/tools/javac 目錄中摄欲。要深入了解字節(jié)碼生成,閱讀 javac 的源碼是個很好的途徑疮薇,不過 javac 對于我們這個例子來說太過龐大了胸墙。在 Java 里面除了 javac 和字節(jié)碼類庫外,使用字節(jié)碼生成的例子還有很多按咒,如 Web 服務器中的 JSP 編譯器迟隅,編譯時植入的 AOP 框架,還有很常用的動態(tài)代理技術励七,甚至在使用反射的時候虛擬機都有可能會在運行時生成字節(jié)碼來提高執(zhí)行速度智袭。我們選擇其中相對簡單的動態(tài)代理來看看字節(jié)碼生成技術是如何影響程序運作的。

相信許多 Java 開發(fā)人員都使用過動態(tài)代理掠抬,即使沒有直接使用過 java.lang.reflect.Proxy 或實現(xiàn)過 java.lang.reflect.InvocationHandler 接口补履,應該也用過 Spring 來做過 Bean 的組織管理。如果使用過 Spring剿另,那大多數情況都會用過動態(tài)代理箫锤,因為如果 Bean 是面向接口編程贬蛙,那么在 Spring 內部都是通過動態(tài)代理的方式來對 Bean 進行增強的。動態(tài)代理中所謂的 “動態(tài)”谚攒,是針對使用 Java 代碼實際編寫了代理類的 “靜態(tài)” 代理而言的阳准,它的優(yōu)勢不在于省去了編寫代理類哪一點工作量,而是實現(xiàn)了可以在原始類和接口還未知的時候馏臭,就確定代理類的代理行為野蝇,當代理類與原始類脫離直接聯(lián)系后,就可以很靈活地重用于不同的應用場景之中括儒。

代碼清單 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();
    }
}

運行結果:

welcome
hello world

上述代碼里固逗,唯一的 “黑匣子” 就是 Proxy.newProxyInstance() 方法浅蚪,除此之外再沒有任何特殊之處。這個方法返回一個實現(xiàn)了 IHello 的接口烫罩,并且代理了 new Hello() 實例行為的對象惜傲。跟蹤這個方法的源碼,可以看到程序進行了驗證贝攒、優(yōu)化盗誊、緩存、同步隘弊、生成字節(jié)碼哈踱、顯式類加載等操作,前面的步驟并不是我們關注的重點长捧,而最后它調用了 sun.misc.ProxyGenerator.generateProxyClass() 方法來完成生成字節(jié)碼的動作,這個方法可以在運行時產生一個描述代理類的字節(jié)碼 byte[] 數組吻贿。如果想看一看這個再運行時產生的代理類中寫了什么串结,可以在main() 方法中加入下面這句:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

加入這句代碼后再次運行程序,磁盤中將會產生一個名為 “$Proxy0.class” 的代理類 Class 文件(注:應該先在【項目目錄】非【ClassPath 目錄】下舅列,建立和包名對應的文件夾肌割,如圖 a 所示),反編譯后可以看見如代碼清單 9-2 所示的內容帐要。

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 個方法的內容與 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() 方法都生成了對應的實現(xiàn)愤炸,并且統(tǒng)一調用了 InvocationHandler 對象的 invoke() 方法(代碼中的 “this.h” 就是父類 Proxy 中保存的 InvocationHandler 實例變量)來實現(xiàn)這些方法的內容,各個方法的區(qū)別不過是傳入的參數和 Method 對象有所不同而已掉奄,所以無論調用動態(tài)代理的哪一個方法规个,實際上都是在執(zhí)行 InvocationHandler.invoke() 中的代理邏輯。

這個例子中并沒有講到 generateProxyClass() 方法具體是如何產生代理類 “$Proxy0.class” 的字節(jié)碼的姓建,大致的生成過程其實就是根據 Class 文件的格式規(guī)范去拼裝字節(jié)碼诞仓,但在實際開發(fā)中,以 byte 為單位直接拼裝出字節(jié)碼的應用場合很少見速兔,這種生成方式也只能產生一些高度模板化的代碼墅拭。對于用戶的程序代碼來說,如果有要大量操作字節(jié)碼的需求憨栽,還是使用封裝好的字節(jié)碼類庫比較合適帜矾。如果讀者對動態(tài)代理的字節(jié)碼拼裝過程很感興趣,可以在 OpenJDK 的 jdk/src/share/classes/sun/misc 目錄下找到sun.misc.ProxyGenerator 的源碼屑柔。

四屡萤、retrotranslator跨域JDK版本

一般來說,以 “做項目” 為主的軟件公司比較容易更新技術掸宛,在下一個項目中換一個技術框架死陆、升級到最新的 JDK 版本狱意,甚至把 Java 換成 C#坚俗、C++ 來開發(fā)程序都是由可能的。但當公司發(fā)展壯大偎行,技術有所積累饰序,逐漸成為 “做產品” 為主的軟件公司后领虹,自主選擇技術的權利就會喪失掉,因為之前所積累的代碼和技術都是用真金白銀換來的求豫,一個穩(wěn)健的團隊也不會隨意地改變底層的技術塌衰。然而在飛速發(fā)展的程序設計領域,新技術總是日新月異蝠嘉、層出不窮最疆,偏偏這些新技術又如鮮花之于蜜蜂一樣,對程序員散發(fā)著天然的吸引力蚤告。

在 Java 世界里努酸,每一次 JDK 大版本的發(fā)布,都伴隨著一場大規(guī)模的技術革新杜恰,而對 Java 程序編寫習慣改變最大的获诈,無疑是 JDK 1.5 的發(fā)布仍源。自動裝箱、泛型烙荷、動態(tài)注解镜会、枚舉、變長參數终抽、遍歷循環(huán)(foreach 循環(huán))……事實上戳表,在沒有這些語法特性的年代,Java 程序也照樣能寫昼伴,但是現(xiàn)在看來匾旭,上述每一種語法的改進幾乎都是 “必不可少” 的。就如同習慣了 24 寸液晶顯示器的程序員圃郊,很難習慣在 15 寸平顯示器上編寫代碼价涝。但假如 “不幸” 因為要保護現(xiàn)有投資、維持程序結構穩(wěn)定等持舆,必須使用 1.5 以前版本的 JDK 呢色瘩?我們沒有辦法把 15 寸顯示器變成 24 寸的,但卻可以跨越 JDK 版本之間的溝壑逸寓,把 JDK 1.5 中編寫的代碼放到 JDK 1.4 或 1.3 的環(huán)境去部署使用居兆。為了解決這個問題,一種名為 “Java 逆向移植” 的工具(Java Backporting Tools)應運而生竹伸,Retrotranslator 是這類工具中較出色的一個泥栖。

Retrotranslator 的作用是將 JDK 1.5 編譯出來的 Class 文件轉變?yōu)榭梢栽?JDK 1.4 或 1.3 上部署的版本,它可以很好地支持自動裝箱勋篓、泛型吧享、動態(tài)注解、枚舉譬嚣、變長參數钢颂、遍歷循環(huán)、靜態(tài)導入這些語法特性拜银,甚至還可以支持 JDK 1.5 中新增的集合改進殊鞭、并發(fā)包以及對泛型、注解等的反射操作盐股。了解了 Retrotranslator 這種逆向移植工具可以做什么以后钱豁,現(xiàn)在關心的是它是怎樣做到的耻卡?

要想知道 Retrotranslator 如何在舊版本 JDK 中模擬新版本 JDK 的功能疯汁,首先要弄清楚 JDK 升級中會提供哪些新的功能。JDK 每次升級新增的功能大致可以分為以下 4 類:

在編譯器層面做的改進卵酪。如自動裝箱拆箱幌蚊,實際上就是編譯器在程序中使用到包裝對象的地方自動插入了很多 Integer.valueOf()谤碳、Float.valueOf() 之類的代碼;變長參數在編譯之后就自動轉化成一個數組來完成參數傳遞溢豆;泛型的信息則在編譯階段就已經擦除掉了(但是在元數據中還保留著)蜒简,相應的地方被編譯器自動插入了類型轉換代碼。
對 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)相關的調用功能。不過字節(jié)碼指令集一直處于相對比較穩(wěn)定的狀態(tài)麸折,這種需要在字節(jié)碼層面直接進行的改動是比較少見的锡凝。
虛擬機內部的改進。如 JDK 1.5 中實現(xiàn)的 JSR-133 規(guī)范重新定義的 Java 內存模型(Java Memory Model垢啼,JMM)窜锯、CMS 收集器之類的改動,這類改動對于程序員編寫代碼基本是透明的芭析,但會對程序運行時產生影響锚扎。
上述 4 類新功能中,Retrotranslator 只能模擬前兩類放刨,對于后面兩類直接在虛擬機內部實現(xiàn)的改進工秩,一般所有的逆向移植工具都是無能為力的,至少不能完整地或者再可接受的效率上完成全部模擬进统,否則虛擬機設計團隊也沒有必要舍近求遠地改動處于 JDK 底層的虛擬機助币。在可以模擬的兩類功能中,第二類模擬相對更容易實現(xiàn)一些螟碎,如 JDK 1.5 引入的 java.util.concurrent 包眉菱,實際是由多線程大師 Doug Lea 開發(fā)的一套并發(fā)包,在 JDK 1.5 出現(xiàn)之前就已經存在(那時候名字叫做 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é)碼指令數量并沒有改變惜姐,所以無論是 JDK 1.3、JDK 1.4 還是 JDK 1.5,能用字節(jié)碼表達的語義范圍應該是一直的歹袁。當然坷衍,肯定不可能簡單地把 Class 的文件版本號從 49.0 改回 48.0 就能解決問題了,雖然字節(jié)碼指令的數量沒有變化条舔,但是元數據信息和一些語法支持的內容還是要做相應的修改枫耳。以枚舉為例,在 JDK 1.5 中增加了 enum 關鍵字孟抗,但是 Class 文件常量池的 CONSTANT_Class_info 類型常量并沒有發(fā)生任何語義變化迁杨,仍然是代表一個類或接口的符號引用,沒有加入枚舉凄硼,也沒有增加過 “CONSTANT_Enum_info” 之類的 “枚舉符號引用” 常量仑最。所以使用 enum 關鍵字定義常量,雖然從 Java 語法上看起來與使用 class 關鍵字定義類帆喇、使用 interface 關鍵字定義接口是同一層次的警医,但實際上這是由 Javac 編譯器做出來的假象,從字節(jié)碼的角度來看坯钦,枚舉僅僅是一個繼承于 java.lang.Enum预皇、自動生成了 values() 和 valueOf() 方法的普通 Java 類而已。

Retrotranslator 對枚舉所做的主要處理就是把枚舉類的父類從 “java.lang.Enum” 替換位它運行時類庫中包含的 “net.sf.retrotranslator.runtime.java.lang.Enum_”婉刀,然后再在類和字段的訪問標志中抹去 ACC_ENUM 標志位吟温。當然,這只是處理的總體思路突颊,具體的實現(xiàn)要比上面說的復雜得多鲁豪。可以想象既然兩個父類實現(xiàn)都不一樣律秃,values() 和 valueOf() 的方法自然需要重寫爬橡,常量池需要引入大量新的來自父類的符號引用,這些都是實現(xiàn)細節(jié)棒动。圖 9-3 是一個使用 JDK 1.5 編譯的枚舉類與被 Retrotranslator 轉換處理后的字節(jié)碼的對比圖糙申。


image.png

五、自己手動實現(xiàn)遠程執(zhí)行能力

不知道讀者在做程序維護的時候是否遇到過這類情形:排查問題的過程中船惨,想查看內存中的一些參數值柜裸,卻又沒有方法把這些值輸出到界面或日志中,又或者定位到某個緩存數據有問題粱锐,但缺少緩存的同一管理界面疙挺,不得不重啟服務才能清理這個緩存。類似的需求又一個共同的特點怜浅,那就是只要在服務中執(zhí)行一段程序代碼铐然,就可以定位或排除問題,但就是偏偏找不到可以讓服務器執(zhí)行臨時代碼的途徑,這時候就會希望 Java 服務器中也有提供類似 Groovy Console 的功能锦爵。

JDK 1.6 之后提供了 Compiler API,可以動態(tài)地編譯 Java 程序奥裸,雖然這樣達不到動態(tài)語言的靈活度险掀,但讓服務器執(zhí)行臨時代碼的需求就可以得到解決了。在 JDK 1.6 之前湾宙,也可以通過其他方式來做到樟氢,譬如寫一個 JSP 文件上傳到服務器,然后在瀏覽器中運行它侠鳄,或者在服務器端程序中加入一個 BeanShell Script埠啃、JavaScript 等的執(zhí)行引擎(如 Mozilla Rhino)去執(zhí)行動態(tài)腳本。在本章的實戰(zhàn)部分伟恶,我們將使用前面學到的關于類加載及虛擬機執(zhí)行子系統(tǒng)的知識去實現(xiàn)在服務端執(zhí)行臨時代碼的功能碴开。

目標

首先,在實現(xiàn) “在服務端執(zhí)行臨時代碼” 這個需求之前博秫,先來明確一下本次實戰(zhàn)的具體目標潦牛,我們希望最終的產品是這樣的:

  1. 不依賴 JDK 版本,能在目前還普遍使用的 JDK 中部署挡育,也就是使用 JDK 1.4 ~ JDK 1.7 都可以運行巴碗。
  2. 不改變原有服務端程序的部署,不依賴任何第三方類庫即寒。
  3. 不侵入原有程序橡淆,即無須改動原程序的任何代碼,也不會對原有程序的運行帶來任何影響母赵。
    考到 BeanShell Script 或 JavaScript 等腳本編寫起來不太方便逸爵,“臨時代碼” 需要直接支持 Java 語言。
  4. “臨時代碼” 應當具備足夠的自由度凹嘲,不需要依賴特定的類或實現(xiàn)特定的接口痊银。這里寫的是 “不需要” 而不是 “不可以”,當 “臨時代碼” 需要引用其他類庫時也沒有限制施绎,只要服務端程序能使用的溯革,臨時代碼應當都能直接引用。
  5. “臨時代碼” 的執(zhí)行結果能返回客戶端谷醉,執(zhí)行結果可以包括程序中輸出的信息及拋出的異常等致稀。

看完上面列出的目標,你覺得完成這個需求需要做多少工作呢俱尼?也許答案比大多數人所想的都要簡單一些:5 個類抖单,250 行代碼(含注釋),大約一個半小時左右的開發(fā)時間久可以了,現(xiàn)在就開始編寫程序吧矛绘!

思路

在程序實現(xiàn)的過程中耍休,我們需要解決以下 3 個問題:

  1. 如何編譯提交到服務器的 Java 代碼?
  2. 如何執(zhí)行編譯之后的 Java 代碼货矮?
  3. 如何收集 Java 代碼的執(zhí)行結果羊精?

對于第一個問題,我們有兩種思路可以選擇囚玫,一種是使用 tools.jar 包(在 Sun JDK/lib 目錄下)中的 com.sun.tools.javac.Main 類來編譯 Java 文件喧锦,這其實和使用 javac 命令編譯是一樣的。這種思路的缺點的引入了額外的 JAR 包抓督,而且把程序 “綁死” 在 Sun 的 JDK 上了燃少,要部署到其他公司的 JDK 中還得把 tools.jar 帶上(雖然 JRockit 和 J9 虛擬機也有這個 JAR 包,但它總不是標準所規(guī)定必須存在的)铃在。另外一種思路是直接在客戶端編譯好阵具,把字節(jié)碼而不是 Java 代碼傳到服務端,這聽起來好像有點投機取巧定铜,一般來說確實不應該假定客戶端一定具有編譯代碼的能力怔昨,但是既然程序員會寫 Java 代碼去給服務端排查問題,那么很難想象他的機器上會連編譯 Java 程序的環(huán)境都沒有宿稀。

對于第二個問題趁舀,簡單地一想:要執(zhí)行編譯后的 Java 代碼,讓類加載器加載這個類生成一個 Class 對象祝沸,然后反射調用一下某個方法就可以了(因為不實現(xiàn)任何接口矮烹,我們可以借用一下 Java 中人人皆知的 “main()” 方法)。但我們還應該考慮得更周全些:一段程序往往不是編寫罩锐、運行一次就能達到效果奉狈,同一個類可能要反復地修改、提交涩惑、執(zhí)行仁期。另外,提交上去的類要能訪問服務端的其他類庫才行竭恬。還有跛蛋,既然提交的是臨時代碼,那提交的 Java 類在執(zhí)行完成后就應當能卸載和回收痊硕。

最后的一個問題赊级,我們想把程序往標準輸出(System.out)和標準錯誤輸出(System.err)中打印的信息收集起來,但標準輸出設備是整個虛擬機進程全局共享的資源岔绸,如果使用 System.setOut()/System.setErr() 方法把輸出流重定向到自己定義的 PrintStream 對象上固然可以收集輸出信息理逊,但也會對原有程序產生影響:會把其他線程向標準輸出中打印的信息也收集了橡伞。雖然這些并不是不能解決的問題,不過為了達到完全不影響原程序的目的晋被,我們可以采用另外一種辦法兑徘,即直接在執(zhí)行的類中把對 System.out 的符號引用替換為我們準備的 PrintStream 的符號引用,依賴前面學習的只是羡洛,做到這一點并不困難挂脑。

實現(xiàn)

在程序實現(xiàn)部分,我們主要看一下代碼及其注釋翘县。首先看看實現(xiàn)過程中需要用到的 4 個支持類。第一個類用于實現(xiàn) “同一個類的代碼可以被多次加載” 這個需求谴分,具體程序如代碼清單 9-3 所示锈麸。

代碼清單 9-3 HotSwapClassLoader 的實現(xiàn)

/**
 * 為了多次載入執(zhí)行類而加入的加載器 <br>
 * 把 defineClass 方法開放出來,只有外部顯式調用的時候才會使用到 loadByte 方法
 * 由虛擬機調用時牺蹄,仍然按照原有的雙親委派規(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[] 數組轉變?yōu)?Class 對象。HotSwapClassLoader 中并沒有重寫 loadClass() 或 findClass() 方法沙兰,因此如果不算外部手工調用 loadByte() 方法的話氓奈,這個類加載器的類查找范圍與它的父類加載器是完全一致的,在被虛擬機調用時鼎天,它會按照雙親委派模型交給父類加載舀奶。構造函數中指定為加載 HotSwapClassLoader 類的類加載器作為父類加載器,這一步是實現(xiàn)提交的執(zhí)行代碼可以訪問服務端引用類庫的關鍵斋射,下面我們來看看代碼清單 9-3育勺。

第二個類是實現(xiàn)將 java.lang.System 替換為我們自己定義的 HackSystem 類的過程,它直接修改符合 Class 文件格式的 byte[] 數組中的常量池部分罗岖,將常量池中指定內容的 CONSTANT_Utf8_info 常量替換為新的字符串涧至,具體代碼如代碼清單 9-4 所示。ClassModifier 中設計對 byte[] 數組操作的部分桑包,主要是將 byte[] 與 int 和 String 互相轉換南蓬,以及把對 byte[] 數據的替換操作封裝在代碼清單 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 標志
     */
    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 常量的內容
     * @param oldStr  修改前的字符串
     * @param newStr  修改后的字符串
     * @return 修改結果
     */
    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;
    }
    
    /**
     * 獲取常量池中常量的數量
     * @return 常量池數量
     */
    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;
    }
}

經過 ClassModifier 處理后的 byte[] 數組才會傳給 HotSwapClassLoader.loadByte() 方法進行類加載弱左,byte[] 數組在這里替換符號引用之后蒜焊,與客戶端直接在 Java 代碼中引用 HackSystem 類再編譯生成的 Class 是完全一樣的。這樣的實現(xiàn)既避免了客戶端編寫臨時執(zhí)行代碼時要依賴特定的類(不然無法引入 HackSytem)科贬,又避免了服務端修改標準輸出后影響到其他程序的輸出泳梆。下面我們來看看代碼清單 9-4 和代碼清單 9-5鳖悠。
最后一個類類就是前面提到過的用來代替 java.lang.System 的 HackSystem,這個類中的方法看起來不少优妙,但其實除了把 out 和 err 兩個靜態(tài)變量改成使用 ByteArrayOutputStream 作為打印目標的同一個 PrintStream 對象乘综,以及增加了讀取、清理 ByteArrayOutputStream 中內容的 getBufferString() 和 clearBuffer() 方法外套硼,就再沒有其他新鮮的內容了卡辰。其余的方法全部來自于 System 類的 public 方法,方法名字邪意、參數九妈、返回值都完全一樣,并且實現(xiàn)也是直接轉調了 System 類的對應方法而已雾鬼。保留這些方法的目的萌朱,是為了在 System 被替換成 HackSystem 之后,執(zhí)行代碼中調用的 System 的其余方法仍然可以繼續(xù)使用策菜,HackSystem 的實現(xiàn)如代碼清單 9-6 所示晶疼。

/**
 * 為 JavaClass 劫持 java.lang.System 提供支持 
 * 除了 out 和 err 外,其余的都直接轉發(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é)轉調 System 的對應方法 
        // 因版面原因又憨,省略了其他方法
}

至此翠霍,4 個支持類已經講解完畢,我們來看看最后一個類 JavaClassExecuter蠢莺,它是提供給外部調用的入口寒匙,調用前面幾個支持類組裝邏輯,完成類加載工作躏将。JavaClassExecuter 只有一個 execute() 方法蒋情,用輸入的符合 Clas 文件格式的 byte[] 數組替換 java.lang.System 的符號引用后,使用 HotSwapClassLoader 加載生成一個 Class 對象耸携,由于每次執(zhí)行 execute() 方法都會生成一個新的類加載器實例棵癣,因此同一個類可以實現(xiàn)重復加載。然后夺衍,反射調用這個 Class 對象的 main() 方法狈谊,如果期間出現(xiàn)任何異常,將異常信息打印到 HackSystem.out 中沟沙,最后把緩沖區(qū)中的信息作為方法的結果返回河劝。JavaClassExecuter 的實現(xiàn)代碼如代碼清單 9-7 所示。

代碼清單 9-7 JavaClassExecuter 的實現(xiàn):

/**
 * JavaClass 執(zhí)行工具
 *
 */
public class JavaClassExecuter {
 
    /**
     * 執(zhí)行外部傳過來的代表一個 Java 類的 byte 數組 <br>
     * 將輸入類的 byte 數組中代表 java.lang.System 的 CONSTANT_Utf8_info 常量修改為劫持后的 
     * HackSystem 類
     * 執(zhí)行方法為該類的 static main(String[] args) 方法矛紫,輸出結果為該類向 System.out/err
     * 輸出的信息
     * 
     * @param classByte  代表一個 Java 類的 byte 數組
     * @return  執(zhí)行結果
     */
    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 類务甥,內容無所謂牡辽,只要向 System.out 輸出信息即可,取名為 TestClass敞临,同時放到服務器 C 盤的根目錄中态辛。然后,建立一個 JSP 文件并加入如代碼清單 9-8 所示的內容挺尿,就可以在瀏覽器中看到這個類的運行結果了奏黑。

代碼清單 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>");
%>

當然,上面的做法只是用于測試和演示编矾,實際使用這個 JavaExecuter 執(zhí)行器的時候熟史,如果還要手工復制一個 Class 文件到服務器上就沒有什么意義了。筆者給這個執(zhí)行器寫了一個 “外殼”窄俏,是一個 Eclipse 插件蹂匹,可以把 Java 文件編譯后傳輸到服務器中,然后把執(zhí)行器的返回結果輸出到 Eclipse 的 Console 窗口里裆操,這樣就可以在有靈感的時候隨時寫幾行調試代碼怒详,放到測試環(huán)境的服務器上立即運行了炉媒。雖然實現(xiàn)簡單踪区,但效果很不錯,對調試問題也非常有用吊骤,如圖 9-4 所示缎岗。


image.png
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市白粉,隨后出現(xiàn)的幾起案子传泊,更是在濱河造成了極大的恐慌,老刑警劉巖鸭巴,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件眷细,死亡現(xiàn)場離奇詭異,居然都是意外死亡鹃祖,警方通過查閱死者的電腦和手機溪椎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恬口,“玉大人校读,你說我怎么就攤上這事∽婺埽” “怎么了歉秫?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長养铸。 經常有香客問我雁芙,道長轧膘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任却特,我火速辦了婚禮扶供,結果婚禮上,老公的妹妹穿的比我還像新娘裂明。我一直安慰自己椿浓,他們只是感情好,可當我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布闽晦。 她就那樣靜靜地躺著扳碍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪仙蛉。 梳的紋絲不亂的頭發(fā)上笋敞,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機與錄音荠瘪,去河邊找鬼夯巷。 笑死,一個胖子當著我的面吹牛哀墓,可吹牛的內容都是我干的趁餐。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼篮绰,長吁一口氣:“原來是場噩夢啊……” “哼后雷!你這毒婦竟也來了?” 一聲冷哼從身側響起吠各,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤臀突,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后贾漏,有當地人在樹林里發(fā)現(xiàn)了一具尸體候学,經...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年纵散,在試婚紗的時候發(fā)現(xiàn)自己被綠了梳码。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡困食,死狀恐怖边翁,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情硕盹,我是刑警寧澤符匾,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站瘩例,受9級特大地震影響啊胶,放射性物質發(fā)生泄漏甸各。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一焰坪、第九天 我趴在偏房一處隱蔽的房頂上張望趣倾。 院中可真熱鬧,春花似錦某饰、人聲如沸儒恋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诫尽。三九已至,卻和暖如春炬守,著一層夾襖步出監(jiān)牢的瞬間牧嫉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工减途, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留酣藻,地道東北人。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓鳍置,卻偏偏與公主長得像辽剧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子墓捻,可洞房花燭夜當晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內容