前言
今天無意看到美團技術團隊的一篇文章,感覺非常的有意思,所以自己整了一篇文章,一起給大家分享一下:
正文
對於我們Java語言的開發者來說,下面的對話應該很熟悉:
Java的對象行為(方法、函數)是存儲在方法區的。
「方法區中的數據從哪來?」
「方法區中的數據是類加載時從class文件中提取出來的。」
「class文件從哪來?」
「從Java或者其他符合JVM規範的原始碼中編譯而來。」
「原始碼從哪來?」
「廢話,當然是手寫!」
這裡為啥要有這樣的對話?因為這裡討論一個有趣的問題:
在JSP時代,我們都有過這種經歷:修完Java代碼,對於瀏覽器來說,只需要刷新網頁就可以看到效果。按我們正常的思維。對於java程序來說,哪怕是Hello world也需要重新編譯然後重新run。那麼JSP是如何做的類似」熱更新「的呢?
讓我們繼續上面的對話:
「倒著推,手寫沒問題,編譯沒問題,至於加載……有沒有辦法加載一個已經加載過的類呢?如果有的話,我們就能修改字節碼中目標方法所在的區域,然後重新加載這個類,這樣方法區中的對象行為(方法)就被改變了,而且不改變對象的屬性,也不影響已經存在對象的狀態,那麼就可以搞定這個問題了。可是,這豈不是違背了JVM的類加載原理?畢竟我們不想改變ClassLoader。」
「少年,可以去看看java.lang.instrument.Instrumentation。」
java.lang.instrument.Instrumentation
看完文檔之後,我們發現這麼兩個接口:redefineClasses和retransformClasses。一個是重新定義class,一個是修改class。這兩個大同小異,看reDefineClasses的說明:
This method is used to replace the definition of a class without reference to the existing class file bytes, as one might do when recompiling from source for fix-and-continue debugging. Where the existing class file bytes are to be transformed (for example in bytecode instrumentation) retransformClasses should be used.
都是替換已經存在的class文件,redefineClasses是自己提供字節碼文件替換掉已存在的class文件,retransformClasses是在已存在的字節碼文件上修改後再替換之。
當然,運行時直接替換類很不安全。比如新的class文件引用了一個不存在的類,或者把某個類的一個field給刪除了等等,這些情況都會引發異常。所以如文檔中所言,instrument存在諸多的限制:
The redefinition may change method bodies, the constant pool and attributes. The redefinition must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. These restrictions maybe be lifted in future versions. The class file bytes are not checked, verified and installed until after the transformations have been applied, if the resultant bytes are in error this method will throw an exception.
我們能做的基本上也就是簡單修改方法內的一些行為,這對於我們開頭的問題,列印一段日誌來說,已經足夠了。當然,我們除了通過reTransform來列印日誌,還能做很多其他非常有用的事情,這個下文會進行介紹。
那怎麼得到我們需要的class文件呢?一個最簡單的方法,是把修改後的Java文件重新編譯一遍得到class文件,然後調用redefineClasses替換。但是對於沒有(或者拿不到,或者不方便修改)源碼的文件我們應該怎麼辦呢?其實對於JVM來說,不管是Java也好,Scala也好,任何一種符合JVM規範的語言的原始碼,都可以編譯成class文件。JVM的操作對象是class文件,而不是源碼。所以,從這種意義上來講,我們可以說「JVM跟語言無關」。既然如此,不管有沒有源碼,其實我們只需要修改class文件就行了。
直接操作字節碼
Java是軟體開發人員能讀懂的語言,class字節碼是JVM能讀懂的語言,class字節碼最終會被JVM解釋成機器能讀懂的語言。無論哪種語言,都是人創造的。所以,理論上(實際上也確實如此)人能讀懂上述任何一種語言,既然能讀懂,自然能修改。只要我們願意,我們完全可以跳過Java編譯器,直接寫字節碼文件,只不過這並不符合時代的發展罷了,畢竟高級語言設計之始就是為我們人類所服務,其開發效率也比機器語言高很多。
對於人類來說,字節碼文件的可讀性遠遠沒有Java代碼高。儘管如此,還是有一些傑出的程式設計師們創造出了可以用來直接編輯字節碼的框架,提供接口可以讓我們方便地操作字節碼文件,進行注入修改類的方法,動態創造一個新的類等等操作。其中最著名的框架應該就是ASM了,cglib、Spring等框架中對於字節碼的操作就建立在ASM之上。
我們都知道,Spring的AOP是基於動態代理實現的,Spring會在運行時動態創建代理類,代理類中引用被代理類,在被代理的方法執行前後進行一些神秘的操作。那麼,Spring是怎麼在運行時創建代理類的呢?動態代理的美妙之處,就在於我們不必手動為每個需要被代理的類寫代理類代碼,Spring在運行時會根據需要動態地創造出一個類,這裡創造的過程並非通過字符串寫Java文件,然後編譯成class文件,然後加載。Spring會直接「創造」一個class文件,然後加載,創造class文件的工具,就是ASM了。
到這裡,我們知道了用ASM框架直接操作class文件,在類中加一段列印日誌的代碼,然後調用retransformClasses就可以了。