阅读 310

什么?你还不知道字节码插桩!

更多内容请查看个人博客codercc

1. 方法监控背景

在日常开发中通常会打印很多的日志,比如方法的出入参、以及traceid的串联等等,其本意是做好链路的监控和日常问题排查。并且一般为了满足公司按照不同BU业务线隔离的诉求,在日志的输出上会有一定的格式要求。针对这样的情况,很显然通过AOP的方式做成统一的公共模块即可。如何做到对业务应用的非侵入性的监控以及提供良好的性能,是一个很重要的点。

为了解决这类问题,在实际开发中,通过【java agent+字节码插桩】的方式来完成方法监控,agent提供了在业务应用main执行前或执行后能够拦截字节码加载的时机,然后通过更改类字节码的方式,完成业务逻辑的注入,实际上这是AOP思想的一种具体落地方式。字节码插桩,在刚开始接触的时候会觉得是一个很高深以及很进阶的技术术语,实际上仅仅是一个能够更改class字节码的一种手段而已,具体的技术方案会有很多,比如cglib,javaassit等等。

2. 插桩方案选型

常见的字节码操作工具,有JDK proxy、cglib、javaassit以及asm,其中最常用的是javaassit以及asm,关于这几种常用工具,查阅了一些博客资料对这几者的比较总结如下:

内容/技术工具asmjavaassitcglibjdk
工具背景底层字节码框架,操纵的级别是底层JVM的汇编指令级别,要求ASM使用者要对class组织结构和JVM汇编指令有一定的了解是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架广泛的被许多AOP的框架使用,例如Spring AOP和dynaop,为他们提供方法的interception(拦截)jdk动态代理根据interface,生成的代理类会被缓存,每个接口只会生成一个代理类
便捷性需要对字节码有了解,开发难度较大,不容易上手可以直接通过提供的api以java代码的方式,完成切面逻辑的开发。操作难度较低,便利性高底层框架使用该工具多,参考案例比较多,学习成本中等,也比较容易上手开发比较容易,但是被代理类需要实现接口的限制,场景具有局限性
性能如果直接通过javaassit生成被代理类的字节码,其性能和ASM相当,如果通过MethodHandler生成代理类,速度是很慢,性能较差性能较差性能最差

通过对常见字节码更改的工具进行比较,在不同的业务场景下可以选择合适的工具,比如在不是特别考虑性能损耗的基础上,可以通过javaassit以及cglib等工具进行实践,而十分关注性能问题的话,asm则是最合适的工具。而考虑易用性的话,javaassit则是相对合适的工具,可以通过api以java代码的方式,直接完成业务逻辑的注入,使用成本是相当低的。

针对工作中的实际场景,由于是业务实时链路对性能还是有一定的要求,另外结合其他的考虑最终选用了asm作为字节码更改工具。主要实现的功能是对方法级别进行服务监控和参数采集(包含方法的出入参以及服务耗时和异常)、traceid的种入以及对日志标准化(统一按照集团要求的日志格式输出,方便后续使用ELK工具)。整体的思路如下图所示:

3. asm介绍

ASM是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

asm包主要包含了这些模块:

  1. Core:为其他包提供基础的读、写、转化Java字节码和定义的API,并且可以生成Java字节码和实现大部分字节码的转换,在ASM 中通过访问器模式设计,来访问字节码。几个重要的类就在 Core API 中:ClassReader、ClassVisitor 和 ClassWriter 类;

  2. Tree:提供了 Java 字节码在内存中的表现;

  3. Commons:提供了一些常用的简化字节码生成、转换的类和适配器;

  4. Util:包含一些帮助类和简单的字节码修改类,有利于在开发或者测试中使用;

  5. XML:提供一个适配器将XML和SAX-comliant转化成字节码结构,可以允许使用XSLT去定义字节码转化;

ASM内部采用 访问者模式.class 类文件的内容从头到尾扫描一遍,每次扫描到类文件相应的内容时,都会调用ClassVisitor内部相应的方法。 比如:

  • 扫描到类文件时,会回调ClassVisitorvisit()方法;
  • 扫描到类注解时,会回调ClassVisitorvisitAnnotation()方法;
  • 扫描到类成员时,会回调ClassVisitorvisitField()方法;
  • 扫描到类方法时,会回调ClassVisitorvisitMethod()方法; ······ 扫描到相应结构内容时,会回调相应方法,该方法会返回一个对应的字节码操作对象(比如,visitMethod()返回MethodVisitor实例),通过修改这个对象,就可以修改class文件相应结构部分内容,最后将这个ClassVisitor字节码内容覆盖原来.class文件就实现了类文件的代码切入。
字节码区域asm接口
ClassClassVisitor
FieldFieldVisitor
MethodMethodVisitor
AnnotationAnnotationVisitor

4. 具体实现

4.1 实现结果

通过使用agent与asm字节码插桩完成方法级别业务监控,可以看下结果示例。有一个业务示例,以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后,在执行该方法后会有如下输出:

[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. 对方法的出入参、服务耗时、异常都会进行日志记录;2. 在trace串联上如果当前线程未种入traceid会进行补种。有了trace以及服务完成的上下文,在借助ELK工具就可以高效的完成问题排查以及服务监控,当然因为日志量的增加会带来机器日志存储成本,可以通过冷备的方式。技术方案没有最好,只有合适。在这种业务场景下,通过牺牲空间成本来换取问题排查的高效性以及服务监控带来的稳定性收益。为什么在业务方法运行时会有这些标准化日志输出呢?在通过asm插桩后,原业务方法的代码会更改成如下形式:

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);
}
复制代码

从上面插桩后的代码可以看出,在原来的业务方法中,会增加入参的采集以及耗时的计算,最后通过AresProbeDataProcessor完成日志输出,这样就完成必备信息的采集了。如何对目标方法完成业务逻辑的注入是一个需要解决的核心问题。

4.2 字节码插桩

通过使用premain静态加载agent的方式(关于agent可以查看这篇文章【一文带你了解agent机制】),需要通过实现ClassFileTransformer接口的类,通过实现transform方法来完成对原业务类的字节码的更改。

@Override
public byte[] transform(
        ClassLoader loader,
        String clazzName,
        Class<?> classBeingRedefined,
        ProtectionDomain protectionDomain,
        byte[] classfileBuffer) {
    try {
        // 前置的一些业务逻辑判断(比如类路径按照参数配置校验等等)此处省略
        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);
    }
}
复制代码

这里主要是构造了ClassVisitor,这是asm框架提供了访问类字节码的能力。

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);
    }
}
复制代码

这里主要是判断当前class是否是接口、是否是native方法以及一些业务规则过滤不需要进行方法级别的插桩。AresMethodVisitor则是完成对方法出入口插入相应的逻辑。主要分为如下几块:

  1. 入口参数捕获

    protected void onMethodEnter() {
        this.probeStartTime();
        this.probeInputParams();
        this.acquireInputParams();
    }
    
    /**
         * 入参插桩
         */
        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);
        }
    复制代码

    首先构造一个Object数组去装载方法的入口参数。由于JVM中方法的执行是一个栈帧结构,入口参数会在局部变量表中,当方法正在执行时,方法栈栈顶就是当前的入参,通过ILOAD、FLOAD等相关指令将栈顶的参数存入一个局部变量中,随后放入Object数组中即可。关于JVM相关指令可以看这篇文章JVM指令

  2. 方法入口植入执行开始时间戳

    /**
     * 插入方法开始执行时间
     */
    private void probeStartTime() {
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
        startTimeLocal = newLocal(Type.LONG_TYPE);
        mv.visitVarInsn(Opcodes.LSTORE, startTimeLocal);
    }
    复制代码

    通过INVOKESTATIC指令调用system.nanotime方法获取当前开始的时间戳,并放入到局部变量startTimeLocal中,一遍在方法出口能够获取到,并计算方法执行时长。

  3. 将采集的参数发送到数据处理模块

    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步后就能够采集到入口参数,这样就可以在原字节码中插入调用AresProbeDataProcessor的代码,将这些上下文数据发送出去进行标准化处理(AresProbeDataProcessor表示一个数据处理模块,这里只是简单示意下)

  4. 方法出参数据采集

    在方法出口捕获参数以及处理有异常的情况,大致逻辑如下:

    protected void onMethodExit(int opcode) {
        super.onMethodExit(opcode);
        if (!this.containsTryCatchBlock) {
            if (Opcodes.RETURN != opcode) {
                // 有返回值需要进行处理,
                // 先复制原来的返回值到栈顶进行保存,以免污染原返回值
                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抛出的运行时异常可以进行处理
                // 隐式在运行时排除的异常只能通过添加try-catch进行处理
                // 这里暂不添加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);
            // 调用插桩返回值的方法
            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将栈顶的返回值赋值到局部变量中以便能够将返回参数发送到处理模块中,有个关键点需要注意的是,为了避免对原方法的返回值直接操作后会”污染“原方法的返回值,影响原方法的逻辑正确性。因此,在对返回值进行处理前,对返回值先复制一份提供给数据处理模块,原业务方法返回值不会做任何变更。返回值复制的指令为 mv.visitInsn(Opcodes.DUP);。如果有try-catch-finnaly方法块的话,在捕获出参的时候存在一些问题,暂时没有调试成功,如果有知道的同学,与我联系向你请教。

另外一些注意的点是,由于char/byte/boolean/short对应的基本类型在字节码中都是用int表示,为了能够无歧义的拿到字段值,因此只能将这些基本类型统一转换成对应的引用类型数据,具体的转换代入为:

/**
 * char/byte/boolean/short/int 在字节码中都是使用int来进行表示
 * 所以需要进行区分
 */
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);
    }
}
复制代码

另外需要从描述符提取返回值类型以及入参参数类型,这些数据构成完整的方法上下文数据。

5. 总结

字节码插桩本质上是一种动态代理的具体实现方式,而对字节码本身进行修改完成业务逻辑的注入,对业务应用来说具有非侵入性,并且基于字节码的方式也能满足高性能的线上链路的要求。并且在实现过程中会深入到JVM字节码指令的实际应用也是一件其乐无穷的事情,另外在asm的使用上,可以借助于ASM Bytecode plugin插件可以更加高效的得到待注入的业务代码对应的字节码指令,同时在分析字节码上也借助于jclasslib Bytecode Viewer插件。

参考资料

  1. asm的介绍asm api介绍
  2. segmentfault.com/a/119000002…
  3. zhuanlan.zhihu.com/p/126299707
  4. www.infoq.cn/article/Liv…
文章分类
后端
文章标签