在Class文件格式與執行引擎這部分中,用戶的程序能直接影響的內容並不太多, Class文件以何種格式存儲,類型什麼時候加載、如何鏈接,以及虛擬機如何執行字節碼指令等都是由虛擬機直接控制的行爲,用戶程序沒法對其進行改變。能經過程序進行操做的,主要是字節碼生成與類加載器這兩部分的功能,但僅僅在如何處理這兩點上,就已經出現了許多值得欣賞和借鑑的思路,這些思路後來成爲了許多經常使用功能和程序實現的基礎。java
主流的Java Web服務器 ,如Tomcat、Jetty、WebLogic、WebSphere或其餘筆者沒有列舉的服務器,都實現了本身定義的類加載器(通常都不止一個)。由於一個功能健全的Web服務器 ,要解決以下幾個問題:程序員
因爲存在上述問題,在部署Web應用時 ,單獨的一個ClassPath就沒法知足需求了,因此各類Web服務器都「不約而同」地提供了好幾個ClassPath路徑供用戶存放第三方類庫,這些路徑通常都以「lib」或「classes」命名。被放置到不一樣路徑中的類庫,具有不一樣的訪問範圍和服務對象,一般,每個目錄都會有一個相應的自定義類加載器去加載放置在裏面的Java類庫。 如今 ,筆者就以Tomcat服務器爲例,看一看Tomcat具體是如何規劃用戶類庫結構和類加載器的。apache
在Tomcat目錄結構中,有3組目錄(「/common/*」、「/server/*」和「/shared/*」)能夠存放Java類庫,另外還能夠加上Web應用程序自身的目錄「/WEB-INF/*」 ,一共4組 ,把Java類庫放
置在這些目錄中的含義分別以下。編程
爲了支持這套目錄結構,並對目錄裏面的類庫進行加載和隔離,Tomcat自定義了多個類加載器,這些類加載器按照經典的雙親委派模型來實現,其關係如圖9-1所示。數組
灰色背景的3個類加載器是JDK默認提供的類加載器,這3個加載器的做用在第7章中已經介紹過了。而CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader則是Tomcat本身定義的類加載器,它們分別加載/common/*、/server/*、 /shared/*和/WebApp/WEB-INF/*中的Java類庫。其中WebApp類加載器和Jsp類加載器一般會存在多個實例,每個Web應用程序對應一個WebApp類加載器,每個JSP文件對應一個Jsp類加載器。瀏覽器
從圖9-1的委派關係能夠看出,CommonClassLoader能加載的類均可以被Catalina ClassLoader和SharedClassLoader使用 ,而CatalinaClassLoader和SharedClassLoader本身能加載的類則與對方相互隔離。WebAppClassLoader可使用SharedClassLoader加載到的類,但各個WebAppClassLoader實例之間相互隔離。而JasperLoader的加載範圍僅僅是這個JSP文件所編譯出來的那一個Class , 它出現的目的就是爲了被丟棄:當服務器檢測到JSP文件被修改
時 ,會替掉目前的JasperLoader的實例,並經過再創建一個新的Jsp類加載器來實現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
Tomcat加載器的實現清晰易懂,而且採用了官方推薦的「正統」的使用類加載器的方式。 若是讀者閱讀完上面的案例後,能徹底理解Tomcat設計團隊這樣佈置加載器架構的用意,那說明已經大體掌握了類加載器「主流」的使用方式,那麼筆者不妨再提一個問題讓讀者思考一下 :前面曾經提到過一個場景,若是有10個Web應用程序都是用Spring來進行組織和管理的話 ,能夠把Spring放到Common或Shared目錄下讓這些程序共享。Spring要對用戶程序的類進行管理 ,天然要能訪問到用戶程序的類,而用戶的程序顯然是放在/WebApp/WEB-INF目錄中的 ,那麼被CommonClassLoader或SharedClassLoader加載的Spring如何訪問棄不在其加載範圍內的用戶程序呢?若是讀過本書第7章的相關內容,相信讀者能夠很容易地回答這個問題。安全
Tomcat是Apache基金會中的一款開源的Java Web服務器 ,主頁地址爲: http://tomcat.apache.org。本案例中選用的是Tomcat 5.x服務器的目錄和類加載器結構,在Tomcat6.x的默認配置下,/common、/server和/shared三個目錄已經合併到一塊兒了。服務器
Java程序社區中流傳着這麼一個觀點:「學習JEE規 範 ,去看JBoss源 碼 ;學習類加載器, 就去看OSGi源碼」。儘管「JEE規範」和「類加載器的知識」並非一個對等的概念,不 過 ,既然 這個觀點能在程序員中流傳開來,也從側面說明了OSGi對類加載器的運用確實有其獨到之 處。
Java程序社區中流傳着這麼一個觀點:「學習JEE規範 ,去看JBoss源碼;學習類加載器, 就去看OSGi源碼」。儘管「JEE規範」和「類加載器的知識」並非一個對等的概念,不過 ,既然這個觀點能在程序員中流傳開來,也從側面說明了OSGi對類加載器的運用確實有其獨到之處。
OSGi ( Open Service Gateway Initiative ) 是OSGi聯盟 ( OSGi Alliance ) 制定的一個基於Java語言的動態模塊化規範,這個規範最初由Sun、IBM、爰立信等公司聯合發起,目的是使服務提供商經過住宅網關爲各類家用智能設備提供各類服務,後來這個規範在Java的其餘技術領域也有至關不錯的發展,如今已經成爲Java世界中「事實上」的模塊化標準,而且已經有了Equinox、Felix等成熟的實現。OSGi在Java程序員中最著名的應用案例就是Eclipse IDE,另 外還有許多大型的軟件平臺和中間件服務器都基於或聲明將會基於OSGi規範來實現,如IBM Jazz平臺、GlassFish服務器、jBossOSGi等。
OSGi中的每一個模塊(稱爲Bundle)與普通的Java類庫區別並不太大,二者通常都以JAR格式進行封裝,而且內部存儲的都是Java Package和Class。可是一個Bundle能夠聲明它所依賴的Java Package(經過Import-Packagel描述),也能夠聲明它容許導出發佈的Java Package(經過Export-Package描述)。在OSGi裏面,Bundle之間的依賴關係從傳統的上層模塊依賴底層模塊轉變爲平級模塊之間的依賴(至少外觀上如此),並且類庫的可見性能獲得很是精確的控制,一個模塊裏只有被Export過的Package纔可能由外界訪問,其餘的Package和Class將會隱藏起來。除了更精確的模塊劃分和可見性控制外,**引入OSGi的另一個重要理
由是**,基於OSGi的程序極可能(只是極可能,並非必定會)能夠實現模塊級的熱插拔功能 ,當程序升級更新或調試除錯時,能夠只停用、從新安裝而後啓用程序的其中一部分,這對企業級程序開發來講是一個很是有誘惑力的特性。
OSGi之因此能有上述「誘人」的特色,要歸功於它靈活的類加載器架構。OSGi的Bundle類加載器之間只有規則,沒有固定的委派關係。例如 ,某個Bundle聲明瞭一個它依賴的Package,若是有其餘Bundle聲明發布了這個Package,那麼全部對這個Package的類加載動做都會委派給發佈它的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之間的類加載器及父類加載器之間的關係如圖9-2所示。
因爲沒有牽扯到具體的OSGi實現 ,因此圖9-2中的類加載器都沒有指明具體的加載器實現,只是一個體現了加載器之間關係的概念模型,而且只是體現了OSGi中最簡單的加載器委派關係。通常來講,在OSGi中,加載一個類可能發生的查找行爲和委派關係會比圖9-2中 顯示的複雜得多,類加載時可能進行的查找規則以下:
從圖9-2中還能夠看出,在OSGi裏面,加載器之間的關係再也不是雙親委派模型的樹形結構 ,而是已經進一步發展成了一種更爲複雜的、運行時才能肯定的網狀結構。這種網狀的類加載器架構在帶來更好的靈活性的同時,也可能會產生許多新的隱患。筆者曾經參與過將一個非OSGi的大型系統向Equinox OSGi平臺遷移的項目,因爲歷史緣由,代碼模塊之間的依賴關係錯綜複雜,勉強分離出各個模塊的Bundle後 ,發如今高併發環境下常常出現死鎖。咱們很容易地找到了死鎖的緣由:若是出現了Bundle A依賴Bundle B的Package B , 而Bundle B又依賴了Bundle A的Package A , 這兩個Bundle進行類加載時就很容易發生死鎖。具體狀況是當 Bundle A加載Package B的類時,首先須要鎖定當前類加載器的實例對象
(java.lang.ClassLoader.loadClass()是一個synchronized方法),而後把請求委派給Bundle B的加載器處理,若是這時候Bundle B也正好想加載Package A的類,它也先鎖定本身的加載器再去請求Bundle A的加載器處理,這樣 ,兩個加載器都在等待對方處理本身的請求,而對方處理完以前本身又一直處於同步鎖定的狀態,所以它們就互相死鎖,永遠沒法完成加載請求了。Equinox的Bug List中有關於這類問題的Bug, 也提供了一個以犧牲性能爲代價的解決方案—–用戶能夠用osgi.classloader.singleThreadLoads參數來按單線程串行化的方式強制進行類加載動做。在JDK 1.7中 ,爲非樹狀繼承關係下的類加載器架構進行了一次專門的升級,目的是從底層避免這類死鎖出現的可能。
整體來講,OSGi描繪了 一個很美好的模塊化開發的目標,並且定義了實現這個目標所須要的各類服務,同時也有成熟框架對其提供實現支持。對於單個虛擬機下的應用,從開發初期就創建在OSGi上是一個很不錯的選擇,這樣便於約束依賴。但並不是全部的應用都適合採用OSGi做爲基礎架構,OSGi在提供強大功能的同時,也引入了額外的複雜度,帶來了線程死鎖和內存泄漏的風險。
「字節碼生成」並非什麼高深的技術,讀者在看到「字節碼生成」這個標題時也先沒必要去想諸如Javassist、CGLib、ASM之類的字節碼類庫,由於JDK裏面的javac命令就是字節碼生成技術的「老祖宗」 ,而且javac也是一個由Java語言寫成的程序,它的代碼存放在OpenJDK的langtools/src/share/classes/com/sun/tools/javac目錄中。要深刻了解字節碼生成,閱讀javac的源碼是個很好的途徑,不過javac對於咱們這個例子來講太過龐大了。在Java裏面除了javac和字節碼類庫外,使用字節碼生成的例子還有不少,如Web服務器中的JSP編譯器 ,編譯時植入的AOP框架 ,還有很經常使用的動態代理技術,甚至在使用反射的時候虛擬機都有可能會在運行時生成字節碼來提升執行速度。咱們選擇其中相對簡單的動態代理來看看字節碼生成技術是如何影響程序運做的。
相信許多Java開發人員都使用過動態代理,即便沒有直接使用過Java.lang.reflect.Proxy或實現過java.lang.reflect.InvocationHandler接口 ,應該也用過Spring來作過Bean的組織管理。若是使用過Spring , 那大多數狀況都會用過動態代理,由於若是Bean是面向接口編程,那麼在 Spring內部都是經過動態代理的方式來對Bean進行加強的。動態代理中所謂的「動態」,是針對使用Java代碼實際編寫了代理類的「靜態」代理而言的,它的優點不在於省去了編寫代理類那一點工做量,而是實現了能夠在原始類和接口還未知的時候,就肯定代理類的代理行爲, 當代理類與原始類脫離直接聯繫後,就能夠很靈活地重用於不一樣的應用場景之中。
代碼清單9-1演示了一個最簡單的動態代理的用法,原始的邏輯是打印一句「hello world」 ,代理類的邏輯是在原始類方法執行前打印一句「welcome」。咱們先看一下代碼,而後再分析JDK是如何作到的。
代碼清單9-1 動態代理的簡單示例
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) { IHello hello = (IHello) new DynamicProxy().bind(new Hello()); hello.sayHello(); } }
運行結果以下:
welcome
hello world
上述代碼裏,惟一的「黑釐子」就是Proxy.newProxyInstance()方法,除此以外再沒有任何特殊之處。這個方法返回一個實現了IHello的接口,而且代理了new Hello()實例行爲的對象。跟蹤這個方法的源碼,能夠看到程序進行了驗證、優化、緩存、同步、生成字節碼、顯式類加載等操做,前面的步驟並非咱們關注的重點,而最後它調用了sun.misc.ProxyGenerator.generateProxyClass()方法來完成生成字節碼的動做,這個方法能夠在運行時產生一個描述代理類的字節碼byte []數組。若是想看一看這個在運行時產生的代理類中寫了些什麼 ,能夠在main()方法中加入下面這句:
System.getProperties().put ("sun.misc.ProxyGenerator.saveGeneratedFiles" ,"true")
加入這句代碼後再次運行程序,磁盤中將會產生一個名爲「$Proxy0.class」的代理類Class文件 ,反編譯後能夠看見如代碼清單9-2所示的內容。
加入這句代碼後再次運行程序,磁盤中將會產生一個名爲「$Proxy0.class」的代理類Class文件 ,反編譯後能夠看見如代碼清單9-2所示的內容。
代碼清單9-2 反編譯的動態代理類的代碼
package org.fenixsoft.bytecode; 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) throws { super(paramInvocationHandler); } public final void sayHello() throws { try { this.h.invoke(this, m3, null); return; } catch (RuntimeException localRuntimeException) { throw localRuntimeException; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } // 此處因爲版面緣由,省略equals()、hashCode()、toString()三個方法的代碼 // 這3個方法的內容與sayHello()很是類似。 static { try { m3 = Class.forName("org.fenixsoft.bytecode.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()); } } }
這個代理類的實現代碼也很簡單,它爲傳入接口中的每個方法,以及從 java.lang.Object中繼承來的equals()、hashCode()、toString()方法都生成了對應的實現 ,而且統一調用了InvocationHandler對象的invoke()方法(代碼中的「this.h」就是父類Proxy中保存的InvocationHandler實例變量)來實現這些方法的內容,各個方法的區別不過是傳入的參數和Method對象有所不一樣而已,因此不管調用動態代理的哪個方法,實際上都是在執行InvocationHandler.invoke()中的代理邏輯。
這個例子中並無講到generateProxyClass()方法具體是如何產生代理
類「$Proxy0.class」的字節碼的,大體的生成過程其實就是根據Class文件的格式規範去拼裝字節碼 ,但在實際開發中,以byte爲單位直接拼裝出字節碼的應用場合不多見,這種生成方式也只能產生一些高度模板化的代碼。對於用戶的程序代碼來講,若是有要大量操做字節碼的需求,仍是使用封裝好的字節碼類庫比較合適。若是讀者對動態代理的字節碼拼裝過程很感興趣 ,能夠在OpenJDK的jdk/src/share/classes/sun/misc目錄下找到sun.misc.ProxyGenerator的源
碼。
通常來講,以「作項目」爲主的軟件公司比較容易更新技術,在下一個項目中換一個技術框架、升級到最新的JDK版本 ,甚至把Java換成C#、C++來開發程序都是有可能的。但當公司發展壯大,技術有所積累,逐漸成爲以「作產品」爲主的軟件公司後,自主選擇技術的權利就會喪失掉,由於以前所積累的代碼和技術都是用真金白銀換來的,一個穩健的團隊也不會隨意地改變底層的技術。然而在飛速發展的程序設計領域,新技術老是突飛猛進、層出不窮 ,恰恰這些新技術又如鮮花之於蜜蜂同樣,對程序員散發着自然的吸引力。
在Java世界裏,每一次JDK大版本的發佈,都伴隨着一場大規模的技術革新,而對Java程序編寫習慣改變最大的,無疑是JDK 1.5的發佈。自動裝箱、泛型、動態註解、枚舉、變長參數、遍歷循環(foreach循環)……事實上,在沒有這些語法特性的年代,Java程序也照樣能寫,可是如今看來,上述每一種語法的改進幾乎都是「必不可少」的。就如同習慣了24寸 液晶顯示器的程序員,很難習慣在15寸純平顯示器上編寫代碼。但假如「不幸」由於要保護現有投資、維持程序結構穩定等,必須使用1.5之前版本的JDK呢 ?咱們沒有辦法把15寸顯示器變成24寸的,但卻能夠跨越JDK版本之間的溝壑,把JDK 1.5中編寫的代碼放到JDK 1.4或1.3 的環境中去部署使用。爲了解決這個問題,一種名爲「Java逆向移植」的工具( Java Backporting Tools ) 應運而生,Retrotranslator是這類工具中較出色的一個。
Retrotranslator的做用是將JDK 1.5編譯出來的Class文件轉變爲能夠在JDK 1.4或1.3上部署的版本 ,它能夠很好地支持自動裝箱、泛型、動態註解、枚舉、變長參數、遍歷循環、靜態導入這些語法特性,甚至還能夠支持JDK 1.5中新增的集合改進、併發包以及對泛型、註解等的反射操做。瞭解了Retrotranslator這種逆向移植工具能夠作什麼之後,如今關心的是它是怎樣作到的?
要想知道Retrotranslator如何在舊版本JDK中模擬新版本JDK的功能 ,首先要弄清楚JDK 升級中會提供哪些新的功能。JDK每次升級新增的功能大體能夠分爲如下4類 :
上述4類新功能中,Retrotranslator只能模擬前兩類,對於後面兩類直接在虛擬機內部實現的改進,通常全部的逆向移植工具都是無能爲力的,至少不能完整地或者在可接受的效率上完成所有模擬,不然虛擬機設計團隊也沒有必要捨近求遠地改動處於JDK底層的虛擬機。 在能夠模擬的兩類功能中,第二類模擬相對更容易實現一些,如JDK 1.5引入的 java.util.concurrent包 ,實際是由多線程大師Doug Lea開發的一套併發包,在JDK 1.5出現以前就已經存在(那時候名字叫作dl.util.concurrent, 引入JDK時由做者和JDK開發團隊共同作了一些改進),因此要在舊的JDK中支持這部分功能,以獨立類庫的方式即可實現。 Retrotranslator中附帶了一個名叫「backport-util-concurrent.jar」的類庫來代替JDK 1.5的併發包。
至於JDK在編譯階段進行處理的那些改進,Retrotranslator則是使用ASM框架直接對字節碼進行處理。因爲組成Class文件的字節碼指令數量並無改變,因此不管是JDK 1.三、 JDK 1.4仍是JDK 1.5,能用字節碼錶達的語義範圍應該是一致的。固然,確定不可能簡單地把Class的文件版本號從49.0改回48.0就能解決問題了,雖然字節碼指令的數量沒有變化,可是元數據信息和一些語法支持的內容仍是要作相應的修改。以枚舉爲例,在JDK 1.5中增長了enum關鍵字 ,可是Class文件常量池的CONSTANT_Class_info類型常量並無發生任何語義變化 ,仍然是表明一個類或接口的符號引用,沒有加入枚舉,也沒有增長 過「CONSTANT_Enum_info」之類的「枚舉符號引用」常量。因此使用enum關鍵字定義常量,雖然從Java語法上看起來與使用class關鍵字定義類、使用interface關鍵字定義接口是同一層次的 ,但實際上這是由Javac編譯器作出來的假象,從字節碼的角度來看,枚舉僅僅是一個繼承於java.lang.Enum、自動生成了values()和valueOf()方法的普通類而已。
Retrotranslator對枚舉所作的主要處理就是把枚舉類的父類從「java.lang.Enum」替換爲它運行時類庫中包含的「net.sf.retrotranslator.runtime.java.lang.Enum_」,而後再在類和字段的訪問標誌中抹去ACC_ENUM標誌位。固然 ,這只是處理的整體思路,具體的實現要比上面說的複雜得多。能夠想象既然兩個父類實現都不同, values ( ) 和valueOf( )的方法天然須要重寫 ,常量池須要引入大量新的來自父類的符號引用,這些都是實現細節。圖9-3是一個使用JDK 1.5編譯的枚舉類與被Retrotranslator轉換處理後的字節碼的對比圖。
不知道讀者在作程序維護的時候是否遇到過這類情形:排查問題的過程當中,想查看內存中的一些參數值,卻又沒有方法把這些值輸出到界面或日誌中,又或者定位到某個緩存數據有問題 ,但缺乏緩存的統一管理界面,不得不重啓服務才能清理這個緩存。相似的需求有一個共同的特色,那就是隻要在服務中執行一段程序代碼,就能夠定位或排除問題,但就是恰恰找不到可讓服務器執行臨時代碼的途徑,這時候就會但願Java服務器中也有提供相似Groovy Console的功能。
JDK 1.6以後提供了Compiler API , 能夠動態地編譯Java程序,雖然這樣達不到動態語言的靈活度,但讓服務器執行臨時代碼的需求就能夠獲得解決了。在JDK 1.6以前 ,也能夠經過其餘方式來作到,譬如寫一個JSP文件上傳到服務器,而後在瀏覽器中運行它,或者在服務端程序中加入一個BeanShell Script、JavaScript等的執行引擎(如Mozilla Rhino)去執行動態腳本。在本章的實戰部分,咱們將使用前面學到的關於類加載及虛擬機執行子系統的知識去實如今服務端執行臨時代碼的功能。
首先 ,在實現「在服務端執行臨時代碼」這個需求以前,先來明確一下本次實戰的具體目標 ,咱們但願最終的產品是這樣的:
看完上面列出的目標,你以爲完成這個需求須要作多少工做呢?也許答案比大多數人所想的都要簡單一些:5個類 ,250行代碼(含註釋),大約一個半小時左右的開發時間就能夠了 ,如今就開始編寫程序吧!
在程序實現的過程當中,咱們須要解決如下3個問題:
對於第一個問題,咱們有兩種思路能夠選擇,一種是使用tools.jar包 (在SunJDK/lib目錄下)中的com.sun.tools.javac.Main類來編譯Java文件,這其實和使用Javac命令編譯是同樣的。 這種思路的缺點是引入了額外的JAR包 ,並且把程序「綁死」在Sun的JDK上了,要部署到其餘公司的JDK中還得把tools.jar帶上(雖然JRockit和J9虛擬機也有這個JAR包,但它總不是標準所規定必須存在的)。另一種思路是直接在客戶端編譯好,把字節碼而不是Java代碼傳到服務端,這聽起來好像有點投機取巧,通常來講確實不該該假定客戶端必定具備編譯代碼的能力,可是既然程序員會寫Java代碼去給服務端排查問題,那麼很難想象他的機器上會連編譯Java程序的環境都沒有。
對於第二個問題,簡單地一想:要執行編譯後的Java代碼,讓類加載器加載這個類生成一個Class對象 ,而後反射調用一下某個方法就能夠了(由於不實現任何接口,咱們能夠借用一下Java中人人皆知的「main() 」方法)。但咱們還應該考慮得更周全些:一段程序每每不是編寫、運行一次就能達到效果,同一個類可能要反覆地修改、提交、執行。另外,提交上去的類要能訪問服務端的其餘類庫才行。還有 ,既然提交的是臨時代碼,那提交的Java類在執行完後就應當能卸載和回收。
最後的一個問題,咱們想把程序往標準輸出(Systemout)和標準錯誤輸出( Systemerr ) 中打印的信息收集起來,但標準輸出設備是整個虛擬機進程全局共享的資源,如桌使用System.setOut()/System.setErr()方法把輸出流重定向到本身定義的PrintStream對象上當然能夠收集輸出信息,但也會對原有程序產生影響:會把其餘線程向標準輸出中打印的信息也收集了。雖然這些並非不能解決的問題,不過爲了達到徹底不影響原程序的目的 ,咱們能夠採用另一種辦法,即直接在執行的類中把對System.out的符號引用替換爲咱們準備的PnntStream的符號引用,依賴前面學習的知識,作到這一點並不困難。
在程序實現部分,咱們主要看一下代碼及其註釋。首先看看實現過程當中須要用到的4個支持類。第一個類用於實現「同一個類的代碼能夠被屢次加載」這個需求,即用於解決【目標】中列舉的第2個問題的HotSwapClassLoader,具體程序如代碼清單9-3所示。
代碼清單9-3 HotSwapClassLoader的實現
/** * 爲了屢次載入執行類而加入的加載器<br> * 把defineClass方法開放出來,只有外部顯式調用的時候纔會使用到loadByte方法 * 由虛擬機調用時,仍然按照原有的雙親委派規則使用loadClass方法進行類加載 * * @author zzm */ 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() ,咱們將會使用這個方法把提交執行的Java類的byte[]數組轉變爲Class對象。HotSwapClassLoader中並無重寫loadClass() 或findClass() 方法 ,所以若是不算外部手工調用loadByte() 方法的話,這個類加載器的類查找範圍與它的父類加載器是徹底一致的,在被虛擬機調用時,它會按照雙親委派模型交給父類加載。構造函數中指定爲加載HotSwapClassLoader類的類加載器也爲父類加載器,這一步是實現提交的執行代碼能夠訪問服務端引用類庫的關鍵,下面咱們來看看代碼清單9-3。
第二個類是實現將java.lang.System替換爲咱們本身定義的HackSystem類的過程,它直接修改符合Class文件格式的byte[]數組中的常量池部分,將常量池中指定內容的 CONSTANT_UtfB_info常量替換爲新的字符串,具體代碼如代碼清單9-4所示。 ClassModifier中涉及對byte[]數組操做的部分,主要是將byte[]與int和String互相轉換,以及把對byte[]數據的替換操做封裝在代碼清單9-5所示的ByteUtils中。
代碼清單9-4 ClassModifier的實現
/** * 修改Class文件,暫時只提供修改常量池常量的功能 * @author zzm */ 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[] CONSTANT_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 += CONSTANT_ITEM_LENGTH[tag]; } } return classByte; } /** * 獲取常量池中常量的數量 * @return 常量池數量 */ public int getConstantPoolCount() { return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2); } }
代碼清單9-5 ByteUtils的實現
/** * Bytes數組處理工具 * @author */ 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是徹底同樣的。這樣的實現既避免了客戶端編寫臨時執行代碼時要依賴特定的類(否則沒法引入HackSystem) ,又避免了服務端修改標準輸出後影響到其餘程序的 輸出。下面咱們來看看代碼清單9-4和代碼清單9-5。
/** * 爲JavaClass劫持java.lang.System提供支持 * 除了out和err外,其他的都直接轉發給System處理 * * @author zzm */ 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 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的名稱同樣 // 實現都是字節轉調System的對應方法 // 因版面緣由,省略了其餘方法 }
至此, 4個支持類已經講解完畢,咱們來看看最後一個類JavaClassExecuter , 它是提供給外部調用的入口,調用前面幾個支持類組裝邏輯,完成類加載工做。JavaClassExecuter只有一個execute()方法,用輸入的符合Class文件格式的byte[]數組替換java.lang.System的符號引用後,使用HotSwapClassLoader加載生成一個Class對象,因爲每次執行execute()方法都會生成一個新的類加載器實例,所以同一個類能夠實現重複加載。而後,反射調用這個Class對象的main()方法,若是期間出現任何異常,將異常信息打印到HackSystemout中,最後把緩衝區中的信息、做爲方法的結果返回。JavaClassExecuter的實現代碼如代運清單9- 7所示。
代碼清單9-7 JavaClassExecuter的實現
/** * JavaClass執行工具 * * @author zzm */ public class JavaClassExecuter { /** * 執行外部傳過來的表明一個Java類的Byte數組<br> * 將輸入類的byte數組中表明java.lang.System的CONSTANT_Utf8_info常量修改成劫持後的HackSystem類 * 執行方法爲該類的static main(String[] args)方法,輸出結果爲該類向System.out/err輸出的信息 * @param classByte 表明一個Java類的Byte數組 * @return 執行結果 */ 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(); } }
遠程執行功能的編碼到此就完成了,接下來就要檢驗一下咱們的勞動成果了。若是隻是測試的話,那麼能夠任意寫一個Java類 ,內容無所謂,只要向System.out輸出信息便可,取名爲TestClass, 同時放到服務器C盤的根目錄中。而後,創建一個JSP文件並加入如代碼清單9- 8所示的內容,就能夠在瀏覽器中看到這個類的運行結果了。
<%@ 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.execute(b)); out.println("</textarea>"); %>
固然 ,上面的作法只是用於測試和演示,實際使用這個JavaExecuter執行器的時候,若是還要手工複製一個Class文件到服務器上就沒有什麼意義了。筆者給這個執行器寫了一個「外殼」,是一個Eclipse插件 ,能夠把Java文件編譯後傳輸到服務器中,而後把執行器的返回結果輸到Eclipse的Console窗口裏,這樣就能夠在有靈感的時候隨時寫幾行調試代碼, 放到測試環境的服務器上當即運行了。雖然實現簡單,但效果很不錯,對調試問題也很是有用 ,如圖9-4所示。