1. Instrumentation介紹
?JVMTI(JVM Tool Interface)是 Java 虛擬機(jī)所提供的 native 編程接口,是 JVMPI(Java Virtual Machine Profiler Interface)和 JVMDI(Java Virtual Machine Debug Interface)的更新版本。JVMTI 提供了一套“代理”程序機(jī)制,可以支持第三方工具程序以代理的方式連接和訪問 JVM嫂粟,并利用 JVMTI 提供的豐富的編程接口线梗,完成很多跟 JVM 相關(guān)的功能。
?Agent 即 JVMTI 的客戶端勤婚,它和執(zhí)行 Java 程序的虛擬機(jī)運(yùn)行在同一個(gè)進(jìn)程上哲鸳。他們通常由另一個(gè)獨(dú)立的進(jìn)程控制臣疑,充當(dāng)這個(gè)獨(dú)立進(jìn)程和當(dāng)前虛擬機(jī)之間的中介,通過調(diào)用 JVMTI 提供的接口和虛擬機(jī)交互徙菠,負(fù)責(zé)獲取并返回當(dāng)前虛擬機(jī)的狀態(tài)或者轉(zhuǎn)發(fā)控制命令讯沈。java.lang.instrument 包的實(shí)現(xiàn),也是基于這種機(jī)制的婿奔。在 Instrumentation 的實(shí)現(xiàn)當(dāng)中缺狠,存在一個(gè) JVMTI 的代理程序,通過調(diào)用 JVMTI 當(dāng)中于 Java 類相關(guān)的函數(shù)來完成Java 類的動(dòng)態(tài)操作萍摊。
?利用 java.lang.instrument 做動(dòng)態(tài) Instrumentation 是 Java SE 5 的新特性挤茄,它把 Java 的 instrument 功能從本地代碼中解放出來,使之可以用 Java 代碼的方式解決問題冰木。使用 Instrumentation驮樊,開發(fā)者可以構(gòu)建一個(gè)獨(dú)立于應(yīng)用程序的代理程序(Agent),用來監(jiān)測(cè)和協(xié)助運(yùn)行在 JVM 上的程序片酝,甚至能夠替換和修改某些類的定義囚衔。有了這樣的功能,開發(fā)者就可以實(shí)現(xiàn)更為靈活的運(yùn)行時(shí)虛擬機(jī)監(jiān)控和 Java 類操作了雕沿,這樣的特性實(shí)際上提供了 一種虛擬機(jī)級(jí)別支持的 AOP 實(shí)現(xiàn)方式练湿,使得開發(fā)者無需對(duì) JDK 做任何升級(jí)和改動(dòng),就可以實(shí)現(xiàn)某些 AOP 的功能了审轮。
?在 Java SE6 里面肥哎,最大的改變是運(yùn)行時(shí)的 Instrumentation 成為可能。在 Java SE 5 中疾渣,Instrument 要求在運(yùn)行前利用命令行參數(shù)或者系統(tǒng)參數(shù)來設(shè)置代理類篡诽,在實(shí)際的運(yùn)行之中,虛擬機(jī)在初始化之時(shí)(在絕大多數(shù)的 Java 類庫被載入之前)榴捡,instrumentation 的設(shè)置已經(jīng)啟動(dòng)杈女,并在虛擬機(jī)中設(shè)置了回調(diào)函數(shù),檢測(cè)特定類的加載情況吊圾,并完成實(shí)際工作达椰。但是在實(shí)際的很多的情況下,我們沒有辦法在虛擬機(jī)啟動(dòng)之時(shí)就為其設(shè)定代理项乒,這樣實(shí)際上限制了 instrument 的應(yīng)用啰劲。而 Java SE 6 的新特性改變了這種情況,通過 Java Tool API 中的 attach 方式檀何,我們可以很方便地 在運(yùn)行過程中動(dòng)態(tài)地設(shè)置加載代理類蝇裤,以達(dá)到 instrumentation 的目的。
?Instrumentation 的最大作用频鉴,就是類定義動(dòng)態(tài)改變和操作栓辜。在 Java SE 5 及其后續(xù)版本當(dāng)中,開發(fā)者可以在一個(gè)普通 Java 程序(帶有 main 函數(shù)的 Java 類)運(yùn)行時(shí)砚殿,通過 -javaagent參數(shù)指定一個(gè)特定的 jar 文件(包含 Instrumentation 代理)來啟動(dòng) Instrumentation 的代理程序啃憎。
2. Transformer
?Transformer是字節(jié)碼轉(zhuǎn)換的接口,Instrumentation是管理Transformer似炎、調(diào)度Transformer進(jìn)行字節(jié)碼轉(zhuǎn)換的門面辛萍。 當(dāng)執(zhí)行Instrumentation的addTransformer、removeTransformer方法時(shí)羡藐,最終是調(diào)用了TransformerManager的addTransformer贩毕、removeTransformer,以此來管理Transformer仆嗦。
?Instrumentation的retransformClasses辉阶、redefineClasses是用于通知TransformerManager調(diào)度字節(jié)碼轉(zhuǎn)換的。除此之外,在調(diào)用ClassLoader.defineClass1()這個(gè)native方法用于進(jìn)行類的定義時(shí)谆甜,也會(huì)通知TransformerManager調(diào)度Transformer來進(jìn)行字節(jié)碼轉(zhuǎn)換垃僚。這三個(gè)字節(jié)碼轉(zhuǎn)換通知時(shí)機(jī)分別稱為:
- 加載類時(shí)(1)
- 重定義類時(shí)(2)
- 重轉(zhuǎn)換類時(shí)(3)
?<b>Transformer可以分為兩類:可重轉(zhuǎn)換的Transformer、不可重轉(zhuǎn)換的Transformer规辱。任何一個(gè)Transformer都可以用于加載類時(shí)谆棺、重定義類時(shí)進(jìn)行轉(zhuǎn)換。如果是可重轉(zhuǎn)換的Transformer罕袋,也可以在重轉(zhuǎn)換時(shí)進(jìn)行轉(zhuǎn)換改淑。對(duì)于所有的注冊(cè)轉(zhuǎn)換器,在發(fā)生類加載時(shí)(1)或者重定義類時(shí)(2)浴讯,會(huì)觸發(fā)轉(zhuǎn)換器的執(zhí)行朵夏。重轉(zhuǎn)換類時(shí)只有可中轉(zhuǎn)換的Transformer會(huì)觸發(fā)。</b>
?當(dāng)存在多個(gè)轉(zhuǎn)換器時(shí)榆纽,轉(zhuǎn)換將由transform調(diào)用鏈組成仰猖。也就是說,一個(gè)transform調(diào)用返回的byte數(shù)組將成為下一個(gè)調(diào)用的輸入掠河。 轉(zhuǎn)換將按以下順序進(jìn)行:
- 不可重轉(zhuǎn)換轉(zhuǎn)換器
- 不可重轉(zhuǎn)換本地(native)轉(zhuǎn)換器
- 可重轉(zhuǎn)換轉(zhuǎn)換器
- 可重轉(zhuǎn)換本地(native)轉(zhuǎn)換器
同樣亮元,在重轉(zhuǎn)換時(shí)(3),不會(huì)調(diào)用不可重轉(zhuǎn)換轉(zhuǎn)換器唠摹,而是重用前一個(gè)轉(zhuǎn)換的結(jié)果爆捞。對(duì)于所有其他情況,調(diào)用此方法勾拉。在每個(gè)這種調(diào)用組中煮甥,轉(zhuǎn)換器將按照注冊(cè)的順序調(diào)用。
?ClassFileTransformer接口只有一個(gè)方法:
byte[] transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
其中classfileBuffer字段為加載的class內(nèi)容的byte數(shù)組藕赞,返回結(jié)果未待初始化的class內(nèi)容的byte數(shù)組成肘。即可以通過該方法修改原class內(nèi)容,返回修改后的內(nèi)容來修改類的行為斧蜕。如果不做任何轉(zhuǎn)換双霍,則要返回null。 如果轉(zhuǎn)換器拋出異常(未捕獲的異常)批销,后續(xù)轉(zhuǎn)換器仍然將被調(diào)用并加載洒闸,仍然將嘗試重定義或重轉(zhuǎn)換。因此均芽,拋出異常與返回 null 的效果相同丘逸。
?請(qǐng)參考https://docs.oracle.com/javase/7/docs/api/java/lang/instrument/ClassFileTransformer.html
2.1. redefineClasses
?使用提供的類文件重新定義提供的一組類。
?該方法用于替換類的定義掀宋,而不引用現(xiàn)有的類文件字節(jié)深纲,就像從源頭進(jìn)行重新編譯以進(jìn)行修復(fù)和繼續(xù)調(diào)試時(shí)一樣仲锄。 在現(xiàn)有的類文件字節(jié)要轉(zhuǎn)換的地方應(yīng)該使用retransformClasses。
?該方法對(duì)一組class進(jìn)行操作湃鹊,以便同時(shí)允許多個(gè)相互依賴的類的更改儒喊,如A類的重新定義可能需要重新定義B類。
?如果重新定義的方法具有活動(dòng)堆棧幀涛舍,則這些活動(dòng)幀將繼續(xù)運(yùn)行原始方法的字節(jié)碼澄惊。 重新定義的方法將做用于新的調(diào)用。
?該方法不會(huì)導(dǎo)致任何初始化富雅,除了在常規(guī)JVM語義下會(huì)發(fā)生。 換句話說肛搬,重新定義一個(gè)類并不會(huì)導(dǎo)致它的初始化器被運(yùn)行没佑。 靜態(tài)變量的值將保持在調(diào)用之前。重新定義的類的實(shí)例不受影響温赔。
?重新定義可能會(huì)改變方法體蛤奢,常量池和屬性。 重定義不能添加陶贼,刪除或重命名字段或方法啤贩,更改方法的簽名或更改繼承。 這些限制可能在將來的版本中解除拜秧。 類文件字節(jié)不會(huì)被檢查痹屹,驗(yàn)證和安裝,直到應(yīng)用轉(zhuǎn)換為止枉氮,如果結(jié)果字節(jié)錯(cuò)誤志衍,則此方法將拋出異常。如果此方法拋出異常聊替,則不會(huì)重新定義任何類楼肪。
?該方法的定義如下:
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException,UnmodifiableClassException
public ClassDefinition(Class<?> theClass,byte[] theClassFile) {
if (theClass == null || theClassFile == null) {
throw new NullPointerException();
}
mClass = theClass;
mClassFile = theClassFile;
}
如上所述,該方法需要指定需要替換的Class以及提供自定義類文件的字節(jié)碼內(nèi)容惹悄,請(qǐng)參考https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html#redefineClasses-java.lang.instrument.ClassDefinition...-
2.2. retransformClasses
?重新轉(zhuǎn)換提供的一組類春叫。
?該方法主要作用于已經(jīng)加載過的class∑郏可以用ClassFileTransformer對(duì)初始化過或者redifine過的class進(jìn)行重新處理暂殖, 無論以前是否發(fā)生轉(zhuǎn)換,此函數(shù)都將重新運(yùn)行轉(zhuǎn)換過程爷速。 轉(zhuǎn)換過程遵循以下步驟:
從初始類文件字節(jié)開始
對(duì)于將canRetransform設(shè)置為false的每個(gè)轉(zhuǎn)換器央星,在上一個(gè)類加載或重定義期間由轉(zhuǎn)換器返回的字節(jié)將被重新用作當(dāng)前轉(zhuǎn)換的輸出,相當(dāng)于當(dāng)前轉(zhuǎn)換器不生效
對(duì)于將canRetransform設(shè)置為true的每個(gè)轉(zhuǎn)換器惫东,將會(huì)在當(dāng)前調(diào)用該轉(zhuǎn)換器
轉(zhuǎn)換后的類文件字節(jié)作為類的新定義安裝
?該方法對(duì)一組class進(jìn)行操作莉给,以便同時(shí)允許多個(gè)相互依賴的類的更改毙石,如A類的重新定義可能需要重新定義B類。
?如果重新定義的方法具有活動(dòng)堆棧幀颓遏,則這些活動(dòng)幀將繼續(xù)運(yùn)行原始方法的字節(jié)碼徐矩。 重新定義的方法將做用于新的調(diào)用。
?該方法不會(huì)導(dǎo)致任何初始化叁幢,除了在常規(guī)JVM語義下會(huì)發(fā)生滤灯。 換句話說,重新定義一個(gè)類并不會(huì)導(dǎo)致它的初始化器被運(yùn)行曼玩。 靜態(tài)變量的值將保持在調(diào)用之前鳞骤。重新定義的類的實(shí)例不受影響
?重新轉(zhuǎn)換可能會(huì)改變方法體,常量池和屬性黍判。 重新傳輸不能添加豫尽,刪除或重命名字段或方法,更改方法的簽名或更改繼承顷帖。 這些限制可能在將來的版本中解除美旧。 類文件字節(jié)不會(huì)被檢查,驗(yàn)證和安裝贬墩,直到應(yīng)用轉(zhuǎn)換為止榴嗅,如果結(jié)果字節(jié)錯(cuò)誤,則此方法將拋出異常陶舞。如果此方法拋出異常嗽测,則不會(huì)重新創(chuàng)建任何類。
?該方法的內(nèi)容如下:
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException
該方法要傳入需要進(jìn)行重轉(zhuǎn)換的類吊说,請(qǐng)參考https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html#retransformClasses-java.lang.Class...-
?需要注意的是论咏,為Agent開啟redefine功能需要在javaagent的MANIFEST.MF里設(shè)置Can-Redefine-Classes:true。為Agent開啟retransform功能需要在javaagent的MANIFEST.MF文件里定義了Can-Retransform-Classes:true颁井。
?介紹完了相關(guān)的內(nèi)容厅贪,下面介紹如何實(shí)現(xiàn)。
3. JDK5 premain方式
?使用premain方式進(jìn)行處理需要如下幾個(gè)步驟
3.1. 提供一個(gè)公共的靜態(tài)方法premain:
//<1>
public static void premain(String agentArgs, Instrumentation inst);
//<2>
public static void premain(String agentArgs);
其中雅宾,<1>的優(yōu)先級(jí)比 <2> 高养涮,將會(huì)被優(yōu)先執(zhí)行(<1>和<2>同時(shí)存在時(shí),<2>被忽略)眉抬。
?正如這個(gè)方法名贯吓,該方法會(huì)先于main方法被執(zhí)行。一般會(huì)在這個(gè)方法中創(chuàng)建一個(gè)代理對(duì)象蜀变,通過參數(shù) inst 的 addTransformer() 方法悄谐,將創(chuàng)建的代理對(duì)象再傳遞給虛擬機(jī)。agentArgs 是 premain 函數(shù)得到的程序參數(shù)库北,隨同 “– javaagent”一起傳入爬舰。與 main 函數(shù)不同的是们陆,這個(gè)參數(shù)是一個(gè)字符串而不是一個(gè)字符串?dāng)?shù)組,如果程序參數(shù)有多個(gè)情屹,程序?qū)⒆孕薪馕鲞@個(gè)字符串坪仇。
3.2. 提供一個(gè)或者多個(gè)ClassFileTransformer實(shí)現(xiàn)類
?上面說過,會(huì)在premain中調(diào)用inst的addTransformer()方法垃你,該方法的入?yún)⒕褪荂lassFileTransformer對(duì)象椅文。
?對(duì)于字節(jié)碼的修改在上一節(jié)已經(jīng)介紹過了,可以有多種方式惜颇。這里使用上一節(jié)的例子皆刺,對(duì)CoreActionImpl類進(jìn)行修改以達(dá)到AOP的效果。代碼如下:
public class PreMainProxyAction implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
if (!className.equals("demo/CoreActionImpl")) {
return classfileBuffer;
}
ASMProxyAction proxyAction = new ASMProxyAction();
byte[] bytes = proxyAction.aop(classfileBuffer);
//這里可以將bytes寫入到文件官还,輸出處理后的calss內(nèi)容
return bytes;
}
public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
inst.addTransformer(new PreMainProxyAction());
}
}
其中ASMProxyAction的內(nèi)容為上一節(jié)ASM例子的內(nèi)容芹橡,只是重新組織了代碼以進(jìn)行復(fù)用,核心內(nèi)容如下:
public byte[] aop(byte[] bytes) {
ClassReader cr = new ClassReader(bytes);
return aop(cr);
}
public byte[] aop(ClassReader cr) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cr.accept(new ClassVisitor(Opcodes.ASM6, cw) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
if (!"say".equals(name)) {
return mv;
}
MethodVisitor aopMV = new MethodVisitor(super.api, mv) {
@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("before core action");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
@Override
public void visitInsn(int opcode) {
if (Opcodes.RETURN == opcode) {
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("after core action");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
super.visitInsn(opcode);
}
};
return aopMV;
}
}, ClassReader.SKIP_DEBUG);
return cw.toByteArray();
}
3.3. jar 文件打包
?將這個(gè) Java 類打包成一個(gè) jar 文件望伦,并在其中的 manifest 屬性當(dāng)中加入” Premain-Class”來指定步驟3.1當(dāng)中編寫的那個(gè)帶有 premain 的 Java 類。
3.4. 運(yùn)行
?用如下方式運(yùn)行帶有 Instrumentation 的 Java 程序:
java -javaagent:jar 文件的位置 [= 傳入 premain 的參數(shù) ]
?按照上面示例代碼注釋的內(nèi)容煎殷,輸出處理過后的字節(jié)碼如下:
public class demo/CoreActionImpl implements demo/Action {
// access flags 0x1
public <init>()V
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
public say()V
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "before core action"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "hello world"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "after core action"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
RETURN
MAXSTACK = 2
MAXLOCALS = 1
}
其實(shí)就是上節(jié)ASM處理過后的結(jié)果屯伞,實(shí)現(xiàn)了AOP。即Instrument提供的premain方法豪直,提供了一個(gè)入口劣摇,可以在main方法執(zhí)行前,修改原class的內(nèi)容弓乙,增加自定義邏輯末融。
?需要指出的是,addTransformer 方法并沒有指明要轉(zhuǎn)換哪個(gè)類暇韧,因而在 transform(Transformer 類中)方法中勾习,程序需要自己判斷當(dāng)前的類是否需要轉(zhuǎn)換,如上面的示例懈玻。
4. JDK6 agentmain方式
?JDK5提供的premain方式只能在應(yīng)用啟動(dòng)前對(duì)class進(jìn)行處理巧婶,JDK6 在此基礎(chǔ)上進(jìn)行了改進(jìn),開發(fā)者可以在 main 函數(shù)開始執(zhí)行以后涂乌,再啟動(dòng)自己的處理程序艺栈。
?使用agentmain方式進(jìn)行處理需要如下幾個(gè)步驟
4.1. 提供一個(gè)公共的靜態(tài)方法agentmain:
//<1>
public static void agentmain (String agentArgs, Instrumentation inst);
//<2>
public static void agentmain (String agentArgs);
其中,<1>的優(yōu)先級(jí)比 <2> 高湾盒,將會(huì)被優(yōu)先執(zhí)行(<1>和<2>同時(shí)存在時(shí)湿右,<2>被忽略)。
4.2. 提供一個(gè)或者多個(gè)ClassFileTransformer實(shí)現(xiàn)類
?方法同3.2一致罚勾。不同的是毅人,由于agentmain方式是在虛擬機(jī)啟動(dòng)后進(jìn)行處理的吭狡,這時(shí)候目標(biāo)class可能已經(jīng)被加載過了,需要重新對(duì)目標(biāo)class進(jìn)行處理堰塌,根據(jù)上面的介紹赵刑,可以調(diào)用retransformClasses方法對(duì)類進(jìn)行重新處理。
4.3. jar 文件打包
?將這個(gè) Java 類打包成一個(gè) jar 文件场刑,并在其中的 manifest 屬性當(dāng)中加入” Agent-Class”來指定步驟4.1當(dāng)中編寫的那個(gè)帶有 agentmain 的 Java 類般此。
4.4. 加載jar包
?同premain不一致的是,agentmain的接入需要外部應(yīng)用顯示觸發(fā)牵现。Java SE 6 當(dāng)中提供的 Attach API铐懊,用來向目標(biāo) JVM attach代理工具程序。需要注意的是瞎疼,Attach API 不是 Java 的標(biāo)準(zhǔn) API科乎,而是 Sun 公司提供的一套擴(kuò)展 API。
?Attach API 很簡(jiǎn)單贼急,只有 2 個(gè)主要的類茅茂,都在 com.sun.tools.attach 包里面: VirtualMachine 代表一個(gè) Java 虛擬機(jī),也就是程序需要監(jiān)控的目標(biāo)虛擬機(jī)太抓,提供了 JVM 枚舉空闲,attach 動(dòng)作和 detach 動(dòng)作(Attach 動(dòng)作的相反行為,從 JVM 上面解除一個(gè)代理)等等 ; VirtualMachineDescriptor 則是一個(gè)描述虛擬機(jī)的容器類走敌,配合 VirtualMachine 類完成各種功能碴倾。
?可用如下的方式將一個(gè)jar包attach到一個(gè)運(yùn)行的虛擬機(jī)上去:
public void start(String processId,String agentArgs, String agentJarPath) throws Exception {
VirtualMachine virtualMachine = null;
try {
virtualMachine = VirtualMachine.attach(processId);
virtualMachine.loadAgent(agentJarPath,agentArgs);
} finally {
if (virtualMachine != null) {
virtualMachine.detach();
}
}
}
其中需要的參數(shù)為:
- processId:目標(biāo)應(yīng)用pid
- agentArgs:傳給agentmain的參數(shù)
- agentJarPath:待加載的jar包
?
更多原創(chuàng)內(nèi)容請(qǐng)搜索微信公眾號(hào):啊駝(doubaotaizi)