java探针之修改类字节码文件
java探針利用了javaAgent + ASM字節(jié)碼注入工具實(shí)現(xiàn)了動(dòng)態(tài)修改類文件的功能。像skywalking和arthas都使用到了這個(gè)技術(shù)。
具體原理為:
jdk1.5以后引入了javaAgent技術(shù),javaAgent是運(yùn)行方法之前的攔截器。我們利用javaAgent和ASM字節(jié)碼技術(shù),在JVM加載class二進(jìn)制文件的時(shí)候,利用ASM動(dòng)態(tài)的修改加載的class文件,在監(jiān)控的方法前后添加計(jì)時(shí)器功能,用于計(jì)算監(jiān)控方法耗時(shí),同時(shí)將方法耗時(shí)及內(nèi)部調(diào)用情況放入處理器,處理器利用棧先進(jìn)后出的特點(diǎn)對(duì)方法調(diào)用先后順序做處理,當(dāng)一個(gè)請(qǐng)求處理結(jié)束后,將耗時(shí)方法軌跡和入?yún)ap輸出到文件中,然后根據(jù)map中相應(yīng)參數(shù)或耗時(shí)方法軌跡中的關(guān)鍵代碼區(qū)分出我們要抓取的耗時(shí)業(yè)務(wù)。最后將相應(yīng)耗時(shí)軌跡文件取下來(lái),轉(zhuǎn)化為xml格式并進(jìn)行解析,通過(guò)瀏覽器將代碼分層結(jié)構(gòu)展示出來(lái),方便耗時(shí)分析。
上篇我們介紹了JavaAgent的基本使用,下面介紹如何去動(dòng)態(tài)的修改類的字節(jié)碼文件,這個(gè)才是agent實(shí)現(xiàn)更強(qiáng)大功能的核心所在!
Instrumentation接口
Instrumentation接口位于jdk1.6包java.lang.instrument包下,Instrumentation指的是可以獨(dú)立于應(yīng)用程序之外的代理程序,可以用來(lái)監(jiān)控和擴(kuò)展JVM上運(yùn)行的應(yīng)用程序,相當(dāng)于是JVM層面的AOP。
功能:
監(jiān)控和擴(kuò)展JVM上的運(yùn)行程序,它可以替換和修改java類的字節(jié)碼以便采集數(shù)據(jù),用于監(jiān)控,性能統(tǒng)計(jì),覆蓋率分析,事件記錄等。可以用在程序啟動(dòng)時(shí),也可以用于程序運(yùn)行時(shí)動(dòng)態(tài)attach。
比如說(shuō)一個(gè)Java程序在JVM上運(yùn)行,這時(shí)如果需要監(jiān)控JVM的狀態(tài),除了使用JDK自帶的jps等命令之外,就可以通過(guò)instrument來(lái)更直觀的獲取JVM的運(yùn)行情況;
或者一個(gè)Java方法在JVM中執(zhí)行,如果我想獲取這個(gè)方法的執(zhí)行時(shí)間又不想改代碼,常用的做法是通過(guò)Spring的AOP來(lái)實(shí)現(xiàn),而AOP通過(guò)面向切面編程,而instrument是在JVM層面上直接改動(dòng)java方法來(lái)實(shí)現(xiàn)。
要是定義了操作java類的class文件方法,這里又涉及到了ClassFileTransformer接口,這個(gè)接口的作用是改變Class文件的字節(jié)碼,返回新的字節(jié)碼數(shù)組,源碼如下:
public interface ClassFileTransformer{byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException; }ClassFileTransformer接口只有一個(gè)方法,就是改變指定類的Class文件,該接口沒(méi)有默認(rèn)實(shí)現(xiàn),很顯然如果需要改變Class文件的內(nèi)容,需要改成什么樣需要使用者自己來(lái)實(shí)現(xiàn)。
如:
然后在permain或agentmain方法中inst.addTransformer(new MyTransformer());,其他步驟同之前,不再贅述。
Instrumentation接口相當(dāng)于一個(gè)代理,當(dāng)執(zhí)行premain方法時(shí),通過(guò)Instrumentation提供的API可以動(dòng)態(tài)的添加管理JVM加載的Class文件,Instrumentation管理著ClassFileTransformer。
ClassFileTransformer接口可以動(dòng)態(tài)的改變Class文件的字節(jié)碼,在加載字節(jié)碼的時(shí)候可以將字節(jié)碼進(jìn)行動(dòng)態(tài)修改,具體實(shí)現(xiàn)需要自定義實(shí)現(xiàn)類來(lái)實(shí)現(xiàn)ClassFileTransformer接口
Java字節(jié)碼生成框架大致有ASM、Javassist和byte buddy三種
-
ASM框架介紹及使用
ASM是一種Java字節(jié)碼操控框架,能夠以二進(jìn)制形式修改已有的類或是生成類,ASM可以直接生成二進(jìn)制class文件也可以在類被加載入JVM之前動(dòng)態(tài)改變類,只不過(guò)ASM在創(chuàng)建class字節(jié)碼時(shí)說(shuō)底層JVM的匯編指令,需要使用者對(duì)class組織結(jié)構(gòu)和JVM匯編指令有一定的了解。由于Java 類存儲(chǔ)在.class文件中,這些類文件中包含有:類名稱、方法、屬性及字節(jié)碼,ASM從類文件中讀入信息后改變類行為、分析類信息或者直接創(chuàng)建新的類。著名的使用到ASM的案例便是lambda表達(dá)式、CGLIB動(dòng)態(tài)代理類
ASM框架核心類包含
ClassReader:該類用來(lái)解析編譯過(guò)的class字節(jié)碼文件
ClassWriter:該類用來(lái)重新構(gòu)建編譯后的類,比如修改類名、屬性、方法或者根據(jù)要求創(chuàng)建新的字節(jié)碼文件
ClassAdapter:實(shí)現(xiàn)了ClassVisitor接口,將對(duì)它的方法調(diào)用委托給另一個(gè)ClassVisitor對(duì)象 -
Javassist及使用
Javassit相比于ASM要簡(jiǎn)單點(diǎn),Javassit提供了更高級(jí)的API,當(dāng)時(shí)執(zhí)行效率上比ASM要差,因?yàn)锳SM上直接操作的字節(jié)碼。功能和JDK自帶的反射功能類似,但是比反射要強(qiáng)大。Javassist核心類包括ClassPool:
一個(gè)基于HashMap實(shí)現(xiàn)的CtClass對(duì)象容器,key上類名,value上這個(gè)類的CtClass對(duì)象
CtClass:表示一個(gè)類,可以從ClassPool中獲取
CtMethods:表示一個(gè)類的方法
CtFields:表示類中的屬性 -
Byte Buddy及使用
byte buddy是一個(gè)提供了API用于生成任意Java類工具包,可以生成和修改字節(jié)碼。
3. Instrumentation的實(shí)現(xiàn)原理
說(shuō)起Instrumentation的原理,就不得不先提起JVMTI:
JVMTI官網(wǎng)文檔
JVMTI
JVMTI 是JVM Tool Interface 的縮寫,是 JVM 暴露出來(lái)給用戶擴(kuò)展使用的接口集合,JVMTI 是基于事件驅(qū)動(dòng)的,JVM每執(zhí)行一定的邏輯就會(huì)調(diào)用一些事件的回調(diào)接口,這些接口可以給用戶自行擴(kuò)展來(lái)實(shí)現(xiàn)自己的邏輯。JVMTI是實(shí)現(xiàn) Debugger、Profiler、Monitor、Thread Analyser 和coverage analysis等工具的統(tǒng)一基礎(chǔ),在主流 Java 虛擬機(jī)中都有實(shí)現(xiàn)。
JVMTIAgent
JVMTI 是一套本地代碼接口,因此使用 JVMTI 需要我們與 C/C++ 以及 JNI 打交道。事實(shí)上,開(kāi)發(fā)時(shí)一般采用建立一個(gè) Agent 的方式來(lái)使用 JVMTI,它使用 JVMTI 函數(shù),設(shè)置一些回調(diào)函數(shù),并從 Java 虛擬機(jī)中得到當(dāng)前的運(yùn)行態(tài)信息,并作出自己的判斷,最后還可能操作虛擬機(jī)的運(yùn)行態(tài)。把 Agent 編譯成一個(gè)動(dòng)態(tài)鏈接庫(kù)之后,我們就可以在 Java 程序啟動(dòng)的時(shí)候來(lái)加載它(啟動(dòng)加載模式)
主要有三個(gè)函數(shù):
- Agent_OnLoad方法:如果agent是在啟動(dòng)時(shí)加載的,那么在JVM啟動(dòng)過(guò)程中會(huì)執(zhí)行這個(gè)agent里的Agent_OnLoad函數(shù)
- Agent_OnAttach方法:如果agent不是在啟動(dòng)時(shí)加載的,而是attach到目標(biāo)程序上,然后通過(guò)load命令來(lái)加載agent,由ClassFileLoadHook event提供回調(diào),調(diào)用Agent_OnAttach方法
- Agent_OnUnload方法:在agent卸載時(shí)調(diào)用
回到主題,Instrument 就是一種 JVMTIAgent,它實(shí)現(xiàn)了Agent_OnLoad和Agent_OnAttach兩個(gè)方法,也就是在使用時(shí),Instrument既可以在啟動(dòng)時(shí)加載,也可以在運(yùn)行時(shí)動(dòng)態(tài)加載
- 啟動(dòng)時(shí)加載就是在啟動(dòng)時(shí)添加JVM參數(shù):-javaagent:XXXAgent.jar的方式
- 運(yùn)行時(shí)加載是通過(guò)JVM的attach機(jī)制來(lái)實(shí)現(xiàn),通過(guò)發(fā)送load命令來(lái)加載,這種方式明顯更加靈活,對(duì)監(jiān)控目標(biāo)啟動(dòng)也無(wú)限制,arthas的attach就是基于此
通過(guò) VirtualMachine , 可以attach到當(dāng)前指定的jvm pid上,然后 virtualMachine.loadAgent()將編寫好的agent用于監(jiān)控目標(biāo)。
總結(jié):
Instrumentation相當(dāng)于一個(gè)JVM級(jí)別的AOP
Instrumentation在JVM啟動(dòng)的時(shí)候監(jiān)聽(tīng)事件,如類加載事件,JVM觸發(fā)來(lái)指定的事件通過(guò)回調(diào)通知,并創(chuàng)建一個(gè) Instrumentation接口的實(shí)例,然后找到MANIFEST.MF中配置的實(shí)現(xiàn)了premain方法的Class,然后將Instrumentation實(shí)例傳入premain方法中
premain方法會(huì)在main方法之前執(zhí)行,可以添加ClassFileTransfer來(lái)實(shí)現(xiàn)對(duì)Class文件字節(jié)碼的動(dòng)態(tài)修改(并不會(huì)修改Class文件中的字節(jié)碼,而是修改已經(jīng)被JVM加載的字節(jié)碼)
修改字節(jié)碼的技術(shù)可以使用開(kāi)源的 ASM、javassist、byteBuddy等
https://blog.csdn.net/u010862794/article/details/87773434
總結(jié)
以上是生活随笔為你收集整理的java探针之修改类字节码文件的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Spring容器实例bean中的几个关键
- 下一篇: 重要的基础注解@import