1. 方法監(jiān)控背景
在日常開發(fā)中通常會打印很多的日志,比如方法的出入?yún)?、以及traceid的串聯(lián)等等,其本意是做好鏈路的監(jiān)控和日常問題排查。并且一般為了滿足公司按照不同BU業(yè)務(wù)線隔離的訴求,在日志的輸出上會有一定的格式要求。針對這樣的情況,很顯然通過AOP的方式做成統(tǒng)一的公共模塊即可。如何做到對業(yè)務(wù)應(yīng)用的非侵入性的監(jiān)控以及提供良好的性能,是一個很重要的點(diǎn)。
為了解決這類問題,在實際開發(fā)中,通過【java agent+字節(jié)碼插樁】的方式來完成方法監(jiān)控,agent提供了在業(yè)務(wù)應(yīng)用main執(zhí)行前或執(zhí)行后能夠攔截字節(jié)碼加載的時機(jī),然后通過更改類字節(jié)碼的方式,完成業(yè)務(wù)邏輯的注入,實際上這是AOP思想的一種具體落地方式。字節(jié)碼插樁,在剛開始接觸的時候會覺得是一個很高深以及很進(jìn)階的技術(shù)術(shù)語,實際上僅僅是一個能夠更改class字節(jié)碼的一種手段而已,具體的技術(shù)方案會有很多,比如cglib,javaassit等等。
2. 插樁方案選型
常見的字節(jié)碼操作工具,有JDK proxy、cglib、javaassit以及asm,其中最常用的是javaassit以及asm,關(guān)于這幾種常用工具,查閱了一些博客資料對這幾者的比較總結(jié)如下:
| 內(nèi)容/技術(shù)工具 | asm | javaassit | cglib | jdk |
|---|---|---|---|---|
| 工具背景 | 底層字節(jié)碼框架,操縱的級別是底層JVM的匯編指令級別,要求ASM使用者要對class組織結(jié)構(gòu)和JVM匯編指令有一定的了解 | 是一個開源的分析、編輯和創(chuàng)建Java字節(jié)碼的類庫。是由東京工業(yè)大學(xué)的數(shù)學(xué)和計算機(jī)科學(xué)系的 Shigeru Chiba (千葉 滋)所創(chuàng)建的。它已加入了開放源代碼JBoss 應(yīng)用服務(wù)器項目,通過使用Javassist對字節(jié)碼操作為JBoss實現(xiàn)動態(tài)AOP框架 | 廣泛的被許多AOP的框架使用,例如Spring AOP和dynaop,為他們提供方法的interception(攔截) | jdk動態(tài)代理根據(jù)interface,生成的代理類會被緩存,每個接口只會生成一個代理類 |
| 便捷性 | 需要對字節(jié)碼有了解,開發(fā)難度較大,不容易上手 | 可以直接通過提供的api以java代碼的方式,完成切面邏輯的開發(fā)。操作難度較低,便利性高 | 底層框架使用該工具多,參考案例比較多,學(xué)習(xí)成本中等,也比較容易上手 | 開發(fā)比較容易,但是被代理類需要實現(xiàn)接口的限制,場景具有局限性 |
| 性能 | 高 | 如果直接通過javaassit生成被代理類的字節(jié)碼,其性能和ASM相當(dāng),如果通過MethodHandler生成代理類,速度是很慢,性能較差 |
性能較差 | 性能最差 |
通過對常見字節(jié)碼更改的工具進(jìn)行比較,在不同的業(yè)務(wù)場景下可以選擇合適的工具,比如在不是特別考慮性能損耗的基礎(chǔ)上,可以通過javaassit以及cglib等工具進(jìn)行實踐,而十分關(guān)注性能問題的話,asm則是最合適的工具。而考慮易用性的話,javaassit則是相對合適的工具,可以通過api以java代碼的方式,直接完成業(yè)務(wù)邏輯的注入,使用成本是相當(dāng)?shù)偷摹?/p>
針對工作中的實際場景,由于是業(yè)務(wù)實時鏈路對性能還是有一定的要求,另外結(jié)合其他的考慮最終選用了asm作為字節(jié)碼更改工具。主要實現(xiàn)的功能是對方法級別進(jìn)行服務(wù)監(jiān)控和參數(shù)采集(包含方法的出入?yún)⒁约胺?wù)耗時和異常)、traceid的種入以及對日志標(biāo)準(zhǔn)化(統(tǒng)一按照集團(tuán)要求的日志格式輸出,方便后續(xù)使用ELK工具)。整體的思路如下圖所示:
3. asm介紹
ASM是一個 Java 字節(jié)碼操控框架。它能被用來動態(tài)生成類或者增強(qiáng)既有類的功能。ASM 可以直接產(chǎn)生二進(jìn)制 class 文件,也可以在類被加載入 Java 虛擬機(jī)之前動態(tài)改變類行為。Java class 被存儲在嚴(yán)格格式定義的 .class 文件里,這些類文件擁有足夠的元數(shù)據(jù)來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節(jié)碼(指令)。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據(jù)用戶要求生成新類。
asm包主要包含了這些模塊:
Core:為其他包提供基礎(chǔ)的讀、寫、轉(zhuǎn)化Java字節(jié)碼和定義的API,并且可以生成Java字節(jié)碼和實現(xiàn)大部分字節(jié)碼的轉(zhuǎn)換,在ASM 中通過訪問器模式設(shè)計,來訪問字節(jié)碼。幾個重要的類就在 Core API 中:ClassReader、ClassVisitor 和 ClassWriter 類;
Tree:提供了 Java 字節(jié)碼在內(nèi)存中的表現(xiàn);
Commons:提供了一些常用的簡化字節(jié)碼生成、轉(zhuǎn)換的類和適配器;
Util:包含一些幫助類和簡單的字節(jié)碼修改類,有利于在開發(fā)或者測試中使用;
XML:提供一個適配器將XML和SAX-comliant轉(zhuǎn)化成字節(jié)碼結(jié)構(gòu),可以允許使用XSLT去定義字節(jié)碼轉(zhuǎn)化;
ASM內(nèi)部采用 訪問者模式 將 .class 類文件的內(nèi)容從頭到尾掃描一遍,每次掃描到類文件相應(yīng)的內(nèi)容時,都會調(diào)用ClassVisitor內(nèi)部相應(yīng)的方法。
比如:
- 掃描到類文件時,會回調(diào)
ClassVisitor的visit()方法; - 掃描到類注解時,會回調(diào)
ClassVisitor的visitAnnotation()方法; - 掃描到類成員時,會回調(diào)
ClassVisitor的visitField()方法; - 掃描到類方法時,會回調(diào)
ClassVisitor的visitMethod()方法;
······
掃描到相應(yīng)結(jié)構(gòu)內(nèi)容時,會回調(diào)相應(yīng)方法,該方法會返回一個對應(yīng)的字節(jié)碼操作對象(比如,visitMethod()返回MethodVisitor實例),通過修改這個對象,就可以修改class文件相應(yīng)結(jié)構(gòu)部分內(nèi)容,最后將這個ClassVisitor字節(jié)碼內(nèi)容覆蓋原來.class文件就實現(xiàn)了類文件的代碼切入。
| 字節(jié)碼區(qū)域 | asm接口 |
|---|---|
| Class | ClassVisitor |
| Field | FieldVisitor |
| Method | MethodVisitor |
| Annotation | AnnotationVisitor |
4. 具體實現(xiàn)
4.1 實現(xiàn)結(jié)果
通過使用agent與asm字節(jié)碼插樁完成方法級別業(yè)務(wù)監(jiān)控,可以看下結(jié)果示例。有一個業(yè)務(wù)示例,以test方法為例:
public String test(Long userId) {
long userAge = this.getUserAge();
User user = new User();
user.setAge("20");
user.setName("hello world");
test1((byte) 1, (short) 1, 1, 1, 1, 1, false, 'a');
test2("test2", 2);
test3(user, 10);
test4(user, "test4");
test5((byte) 1, (short) 1, 1, 1, 1, 1, false, 'a');
test6(user, "test6");
test7("test7");
test8(user, "test8");
}
private int getUserAge() {
return 15;
}
通過javaagent配置agent后,在執(zhí)行該方法后會有如下輸出:
[INFO][2021-06-24T14:46:10.969+0800][http-nio-8080-exec-1:AresLogUtil.java:41]
_am||traceid=ac17ea7b60d42a326678246f00000186||spanid=2c04c200030cd61f||cspanid=||type=INPUT||uri=com.example.test.impl.AgentTestServiceImpl.test||proc_time=||params=[{"type":"Ljava/lang/Long;","value":1000}]||errno=I88888||errmsg=ARES正常記錄日志
[INFO][2021-06-24T14:46:10.970+0800][http-nio-8080-exec-1:AresLogUtil.java:41]
_am||traceid=ac17ea7b60d42a326678246f00000186||spanid=2c04c200030cd61f||cspanid=||type=INPUT||uri=com.example.test.impl.AgentTestServiceImpl.getUserAge||proc_time=||params=||errno=I88888||errmsg=ARES正常記錄日志
[INFO][2021-06-24T14:46:10.974+0800][http-nio-8080-exec-1:AresProbeDataProcessor.java:45]
_am||traceid=ac17ea7b60d42a326678246f00000186||spanid=2c04c200030cd61f||{"classNameStr":"com.example.test.impl.AgentTestServiceImpl","methodNameStr":"getUserAge","parametersTypes":[],"returnObjType":"I"}
[INFO][2021-06-24T14:46:10.979+0800][http-nio-8080-exec-1:AresProbeDataProcessor.java:48]
_am||traceid=ac17ea7b60d42a326678246f00000186||spanid=2c04c200030cd61f||{"className":"com.example.test.impl.AgentTestServiceImpl","methodName":"getUserAge","params":[{"type":"I","value":15},{"name":"cost","type":"long","value":4.222047}],"type":"OUTPUT"}
[INFO][2021-06-24T14:46:10.980+0800][http-nio-8080-exec-1:AresLogUtil.java:41]
.......
在日志輸出上主要做了兩件事情:1. 對方法的出入?yún)?、服?wù)耗時、異常都會進(jìn)行日志記錄;2. 在trace串聯(lián)上如果當(dāng)前線程未種入traceid會進(jìn)行補(bǔ)種。有了trace以及服務(wù)完成的上下文,在借助ELK工具就可以高效的完成問題排查以及服務(wù)監(jiān)控,當(dāng)然因為日志量的增加會帶來機(jī)器日志存儲成本,可以通過冷備的方式。技術(shù)方案沒有最好,只有合適。在這種業(yè)務(wù)場景下,通過犧牲空間成本來換取問題排查的高效性以及服務(wù)監(jiān)控帶來的穩(wěn)定性收益。為什么在業(yè)務(wù)方法運(yùn)行時會有這些標(biāo)準(zhǔn)化日志輸出呢?在通過asm插樁后,原業(yè)務(wù)方法的代碼會更改成如下形式:
private int getUserAge() {
long var1 = System.nanoTime();
AresProbeDataProcessor.probeInput("com.example.test.impl.AgentTestServiceImpl", "getUserAge", "[]", (Object[])null, "I");
Integer var3 = 15;
AresProbeDataProcessor.probeOutput("com.example.test.impl.AgentTestServiceImpl", "getUserAge", "[]", "I", var3, var1);
return 15;
}
public void test1(byte var1, short var2, int var3, long var4, float var6, double var7, boolean var9, char var10) {
long var11 = System.nanoTime();
Object[] var13 = new Object[]{var1, var2, var3, var4, var6, var7, var9, var10};
AresProbeDataProcessor.probeInput("com.example.test.impl.AgentTestServiceImpl", "test1", "[\"B\",\"S\",\"I\",\"J\",\"F\",\"D\",\"Z\",\"C\"]", var13, "V");
AresProbeDataProcessor.probeOutput("com.example.test.impl.AgentTestServiceImpl", "test1", "[\"B\",\"S\",\"I\",\"J\",\"F\",\"D\",\"Z\",\"C\"]", "V", (Object)null, var11);
}
從上面插樁后的代碼可以看出,在原來的業(yè)務(wù)方法中,會增加入?yún)⒌牟杉约昂臅r的計算,最后通過AresProbeDataProcessor完成日志輸出,這樣就完成必備信息的采集了。如何對目標(biāo)方法完成業(yè)務(wù)邏輯的注入是一個需要解決的核心問題。
4.2 字節(jié)碼插樁
通過使用premain靜態(tài)加載agent的方式(關(guān)于agent可以查看這篇文章【一文帶你了解agent機(jī)制】),需要通過實現(xiàn)ClassFileTransformer接口的類,通過實現(xiàn)transform方法來完成對原業(yè)務(wù)類的字節(jié)碼的更改。
@Override
public byte[] transform(
ClassLoader loader,
String clazzName,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
try {
// 前置的一些業(yè)務(wù)邏輯判斷(比如類路徑按照參數(shù)配置校驗等等)此處省略
ClassReader classReader = new ClassReader(classfileBuffer);
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new AresClassVisitor(clazzName, classWriter, probeScanner);
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
return classWriter.toByteArray();
} catch (Exception e) {
throw new AresException(AresErrorCodeEnum.AGENT_ERROR.getErrorMsg(), e);
}
}
這里主要是構(gòu)造了ClassVisitor,這是asm框架提供了訪問類字節(jié)碼的能力。
public final class AresClassVisitor extends ClassVisitor {
private final String fullClassName;
private final ProbeScanner probeScanner;
private Boolean isInterface;
private final ClassVisitor cv;
public AresClassVisitor(String clazzName, final ClassVisitor classVisitor, ProbeScanner probeScanner) {
super(Opcodes.ASM5, classVisitor);
this.probeScanner = probeScanner;
this.cv = classVisitor;
if (Objects.isNull(clazzName) || "".equals(clazzName)) {
throw new AresException(AresErrorCodeEnum.CLASSNAME_BLANK.getErrorCode(), AresErrorCodeEnum.CLASSNAME_BLANK.getErrorMsg());
}
this.fullClassName = clazzName.replace("/", ".");
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.isInterface = (access & Opcodes.ACC_INTERFACE) != 0;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if (isExcludeProbe(access, name)) {
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
if (Objects.isNull(mv)) {
return null;
}
return new AresMethodVisitor(mv, access, name, descriptor, fullClassName);
}
private Boolean isExcludeProbe(int access, String name) {
// exclude interface
if (this.isInterface) {
return true;
}
if ((access & Opcodes.ACC_ABSTRACT) != 0
|| (access & Opcodes.ACC_NATIVE) != 0
|| (access & Opcodes.ACC_BRIDGE) != 0
|| (access & Opcodes.ACC_SYNTHETIC) != 0) {
return true;
}
return !this.probeScanner.isNeedProbeByMethodName(name);
}
}
這里主要是判斷當(dāng)前class是否是接口、是否是native方法以及一些業(yè)務(wù)規(guī)則過濾不需要進(jìn)行方法級別的插樁。AresMethodVisitor則是完成對方法出入口插入相應(yīng)的邏輯。主要分為如下幾塊:
-
入口參數(shù)捕獲
protected void onMethodEnter() { this.probeStartTime(); this.probeInputParams(); this.acquireInputParams(); } /** * 入?yún)⒉鍢? */ private void probeInputParams() { int parameterCount = this.paramsTypeList.size(); if (parameterCount <= 0) { return; } // init param array if (parameterCount >= ARRAY_THRESHOLD) { mv.visitVarInsn(Opcodes.BIPUSH, parameterCount); } else { switch (parameterCount) { case 1: mv.visitInsn(Opcodes.ICONST_1); break; case 2: mv.visitInsn(Opcodes.ICONST_2); break; case 3: mv.visitInsn(Opcodes.ICONST_3); break; default: mv.visitInsn(Opcodes.ICONST_0); } } mv.visitTypeInsn(Opcodes.ANEWARRAY, Type.getDescriptor(Object.class)); // local index int localCount = isStaticMethod ? -1 : 0; // assign value to array for (int i = 0; i < parameterCount; i++) { mv.visitInsn(Opcodes.DUP); if (i > LOCAL_INDEX) { mv.visitVarInsn(Opcodes.BIPUSH, i); } else { switch (i) { case 0: mv.visitInsn(Opcodes.ICONST_0); break; case 1: mv.visitInsn(Opcodes.ICONST_1); break; case 2: mv.visitInsn(Opcodes.ICONST_2); break; case 3: mv.visitInsn(Opcodes.ICONST_3); break; case 4: mv.visitInsn(Opcodes.ICONST_4); break; case 5: mv.visitInsn(Opcodes.ICONST_5); break; default: break; } } String type = this.paramsTypeList.get(i); if ("Z".equals(type)) { mv.visitVarInsn(Opcodes.ILOAD, ++localCount); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Boolean.class), "valueOf", "(Z)Ljava/lang/Boolean;", false); } else if ("C".equals(type)) { mv.visitVarInsn(Opcodes.ILOAD, ++localCount); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Character.class), "valueOf", "(C)Ljava/lang/Character;", false); } else if ("B".equals(type)) { mv.visitVarInsn(Opcodes.ILOAD, ++localCount); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Byte.class), "valueOf", "(B)Ljava/lang/Byte;", false); } else if ("S".equals(type)) { mv.visitVarInsn(Opcodes.ILOAD, ++localCount); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Short.class), "valueOf", "(S)Ljava/lang/Short;", false); } else if ("I".equals(type)) { mv.visitVarInsn(Opcodes.ILOAD, ++localCount); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Integer.class), "valueOf", "(I)Ljava/lang/Integer;", false); } else if ("F".equals(type)) { mv.visitVarInsn(Opcodes.FLOAD, ++localCount); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Float.class), "valueOf", "(F)Ljava/lang/Float;", false); } else if ("J".equals(type)) { mv.visitVarInsn(Opcodes.LLOAD, ++localCount); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Long.class), "valueOf", "(J)Ljava/lang/Long;", false); localCount++; } else if ("D".equals(type)) { mv.visitVarInsn(Opcodes.DLOAD, ++localCount); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Double.class), "valueOf", "(D)Ljava/lang/Double;", false); localCount++; } else { mv.visitVarInsn(Opcodes.ALOAD, ++localCount); } mv.visitInsn(Opcodes.AASTORE); } paramsLocal = newLocal(Type.LONG_TYPE); mv.visitVarInsn(Opcodes.ASTORE, paramsLocal); }首先構(gòu)造一個Object數(shù)組去裝載方法的入口參數(shù)。由于JVM中方法的執(zhí)行是一個棧幀結(jié)構(gòu),入口參數(shù)會在局部變量表中,當(dāng)方法正在執(zhí)行時,方法棧棧頂就是當(dāng)前的入?yún)?,通過
ILOAD、FLOAD等相關(guān)指令將棧頂?shù)膮?shù)存入一個局部變量中,隨后放入Object數(shù)組中即可。關(guān)于JVM相關(guān)指令可以看這篇文章JVM指令 -
方法入口植入執(zhí)行開始時間戳
/** * 插入方法開始執(zhí)行時間 */ private void probeStartTime() { mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false); startTimeLocal = newLocal(Type.LONG_TYPE); mv.visitVarInsn(Opcodes.LSTORE, startTimeLocal); }通過
INVOKESTATIC指令調(diào)用system.nanotime方法獲取當(dāng)前開始的時間戳,并放入到局部變量startTimeLocal中,一遍在方法出口能夠獲取到,并計算方法執(zhí)行時長。 -
將采集的參數(shù)發(fā)送到數(shù)據(jù)處理模塊
private void acquireInputParams() { mv.visitLdcInsn(this.className); mv.visitLdcInsn(this.methodName); mv.visitLdcInsn(this.paramTypes); if (this.paramsTypeList.isEmpty()) { mv.visitInsn(Opcodes.ACONST_NULL); } else { mv.visitVarInsn(Opcodes.ALOAD, this.paramsLocal); } mv.visitLdcInsn(this.returnType); mv.visitMethodInsn(INVOKESTATIC, "com/example/am/arch/ares/aspect/AresProbeDataProcessor", "probeInput", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;Ljava/lang/String;)V", false); }通過第1、2步后就能夠采集到入口參數(shù),這樣就可以在原字節(jié)碼中插入調(diào)用
AresProbeDataProcessor的代碼,將這些上下文數(shù)據(jù)發(fā)送出去進(jìn)行標(biāo)準(zhǔn)化處理(AresProbeDataProcessor表示一個數(shù)據(jù)處理模塊,這里只是簡單示意下) -
方法出參數(shù)據(jù)采集
在方法出口捕獲參數(shù)以及處理有異常的情況,大致邏輯如下:
protected void onMethodExit(int opcode) { super.onMethodExit(opcode); if (!this.containsTryCatchBlock) { if (Opcodes.RETURN != opcode) { // 有返回值需要進(jìn)行處理, // 先復(fù)制原來的返回值到棧頂進(jìn)行保存,以免污染原返回值 mv.visitInsn(Opcodes.DUP); } // 處理無try-catch模塊 processMethodExit(opcode); } else { // try-catch-finally 暫時不獲取返回值,只獲取方法時間 processMethodExitWithTryCatchBlock(); } } /** * 處理返回值 * * @param opcode */ private void probeReturnBlock(int opcode) { switch (opcode) { case Opcodes.RETURN: break; // 顯式通過throw拋出的運(yùn)行時異??梢赃M(jìn)行處理 // 隱式在運(yùn)行時排除的異常只能通過添加try-catch進(jìn)行處理 // 這里暫不添加try-catch塊 case Opcodes.ARETURN: case Opcodes.ATHROW: this.returnObjLocal = this.nextLocal; mv.visitVarInsn(Opcodes.ASTORE, this.returnObjLocal); break; case Opcodes.IRETURN: this.returnObjLocal = this.nextLocal; this.handedIntReturnType(); break; case Opcodes.LRETURN: this.returnObjLocal = this.nextLocal; mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Float.class), "valueOf", "(F)Ljava/lang/Float;", false); visitVarInsn(Opcodes.ASTORE, this.returnObjLocal); break; case Opcodes.DRETURN: this.returnObjLocal = this.nextLocal; mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Float.class), "valueOf", "(F)Ljava/lang/Float;", false); visitVarInsn(Opcodes.ASTORE, this.returnObjLocal); break; case Opcodes.FRETURN: this.returnObjLocal = this.nextLocal; mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Float.class), "valueOf", "(F)Ljava/lang/Float;", false); visitVarInsn(Opcodes.ASTORE, this.returnObjLocal); default: break; } } private void processMethodExit(int opcode) { // 捕獲返回值 this.probeReturnBlock(opcode); // 調(diào)用插樁返回值的方法 mv.visitLdcInsn(this.className); mv.visitLdcInsn(this.methodName); mv.visitLdcInsn(this.paramTypes); mv.visitLdcInsn(this.returnType); if (Opcodes.RETURN == opcode) { mv.visitInsn(Opcodes.ACONST_NULL); } else { mv.visitVarInsn(Opcodes.ALOAD, this.returnObjLocal); } mv.visitVarInsn(Opcodes.LLOAD, this.startTimeLocal); mv.visitMethodInsn(INVOKESTATIC, "com/example/am/arch/ares/aspect/AresProbeDataProcessor", "probeOutput", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;J)V", false); } private void processMethodExitWithTryCatchBlock() { mv.visitLdcInsn(this.className); mv.visitLdcInsn(this.methodName); mv.visitLdcInsn(this.paramTypes); mv.visitLdcInsn(this.returnType); mv.visitVarInsn(Opcodes.LLOAD, this.startTimeLocal); mv.visitMethodInsn(INVOKESTATIC, "com/example/am/arch/ares/aspect/AresProbeDataProcessor", "probeCostTime", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;J)V", false); }主要通過
ALOAD將棧頂?shù)姆祷刂蒂x值到局部變量中以便能夠?qū)⒎祷貐?shù)發(fā)送到處理模塊中,有個關(guān)鍵點(diǎn)需要注意的是,為了避免對原方法的返回值直接操作后會”污染“原方法的返回值,影響原方法的邏輯正確性。因此,在對返回值進(jìn)行處理前,對返回值先復(fù)制一份提供給數(shù)據(jù)處理模塊,原業(yè)務(wù)方法返回值不會做任何變更。返回值復(fù)制的指令為mv.visitInsn(Opcodes.DUP);。如果有try-catch-finnaly方法塊的話,在捕獲出參的時候存在一些問題,暫時沒有調(diào)試成功,如果有知道的同學(xué),與我聯(lián)系向你請教。
另外一些注意的點(diǎn)是,由于char/byte/boolean/short對應(yīng)的基本類型在字節(jié)碼中都是用int表示,為了能夠無歧義的拿到字段值,因此只能將這些基本類型統(tǒng)一轉(zhuǎn)換成對應(yīng)的引用類型數(shù)據(jù),具體的轉(zhuǎn)換代入為:
/**
* char/byte/boolean/short/int 在字節(jié)碼中都是使用int來進(jìn)行表示
* 所以需要進(jìn)行區(qū)分
*/
private void handedIntReturnType() {
if ("B".equals(this.returnType)) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Byte.class), "valueOf", "(B)Ljava/lang/Byte;", false);
visitVarInsn(Opcodes.ASTORE, this.returnObjLocal);
return;
}
if ("S".equals(this.returnType)) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Short.class), "valueOf", "(S)Ljava/lang/Short;", false);
visitVarInsn(Opcodes.ASTORE, this.returnObjLocal);
return;
}
if ("C".equals(this.returnType)) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Character.class), "valueOf", "(C)Ljava/lang/Character;", false);
visitVarInsn(Opcodes.ASTORE, this.returnObjLocal);
return;
}
if ("Z".equals(this.returnType)) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Boolean.class), "valueOf", "(Z)Ljava/lang/Boolean;", false);
visitVarInsn(Opcodes.ASTORE, this.returnObjLocal);
return;
}
if ("I".equals(this.returnType)) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Integer.class), "valueOf", "(I)Ljava/lang/Integer;", false);
visitVarInsn(Opcodes.ASTORE, this.returnObjLocal);
}
}
另外需要從描述符提取返回值類型以及入?yún)?shù)類型,這些數(shù)據(jù)構(gòu)成完整的方法上下文數(shù)據(jù)。
5. 總結(jié)
字節(jié)碼插樁本質(zhì)上是一種動態(tài)代理的具體實現(xiàn)方式,而對字節(jié)碼本身進(jìn)行修改完成業(yè)務(wù)邏輯的注入,對業(yè)務(wù)應(yīng)用來說具有非侵入性,并且基于字節(jié)碼的方式也能滿足高性能的線上鏈路的要求。并且在實現(xiàn)過程中會深入到JVM字節(jié)碼指令的實際應(yīng)用也是一件其樂無窮的事情,另外在asm的使用上,可以借助于ASM Bytecode plugin插件可以更加高效的得到待注入的業(yè)務(wù)代碼對應(yīng)的字節(jié)碼指令,同時在分析字節(jié)碼上也借助于jclasslib Bytecode Viewer插件。