什么?你還不知道字節(jié)碼插樁!

更多內(nèi)容請查看個人博客codercc

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包主要包含了這些模塊:

  1. Core:為其他包提供基礎(chǔ)的讀、寫、轉(zhuǎn)化Java字節(jié)碼和定義的API,并且可以生成Java字節(jié)碼和實現(xiàn)大部分字節(jié)碼的轉(zhuǎn)換,在ASM 中通過訪問器模式設(shè)計,來訪問字節(jié)碼。幾個重要的類就在 Core API 中:ClassReader、ClassVisitor 和 ClassWriter 類;

  2. Tree:提供了 Java 字節(jié)碼在內(nèi)存中的表現(xiàn);

  3. Commons:提供了一些常用的簡化字節(jié)碼生成、轉(zhuǎn)換的類和適配器;

  4. Util:包含一些幫助類和簡單的字節(jié)碼修改類,有利于在開發(fā)或者測試中使用;

  5. 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)ClassVisitorvisit()方法;
  • 掃描到類注解時,會回調(diào)ClassVisitorvisitAnnotation()方法;
  • 掃描到類成員時,會回調(diào)ClassVisitorvisitField()方法;
  • 掃描到類方法時,會回調(diào)ClassVisitorvisitMethod()方法;
    ······
    掃描到相應(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)的邏輯。主要分為如下幾塊:

  1. 入口參數(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指令

  2. 方法入口植入執(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í)行時長。

  3. 將采集的參數(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ù)處理模塊,這里只是簡單示意下)

  4. 方法出參數(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插件。

參考資料

  1. asm的介紹、asm api介紹
  2. https://segmentfault.com/a/1190000022622614
  3. https://zhuanlan.zhihu.com/p/126299707
  4. https://www.infoq.cn/article/Living-Matrix-Bytecode-Manipulation
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容