User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Java   ›   正文
Java

Java—ASM字节码编程

2022-05-04 21:53:39
882  0 1
参考目录 隐藏
1) 认识ASM
2) 例1:HelloWorld
3) 例2:两数之和计算
4) 例3:在原有方法上字节码增强监控耗时
5) 例4:方法加上TryCatch捕获异常并输出

阅读完需:约 16 分钟

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

ASM — Maven

 <!-- ASM -->
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm-commons</artifactId>
            <version>6.2.1</version>
            <exclusions>
                <exclusion>
                    <groupId>org.ow2.asm</groupId>
                    <artifactId>asm-analysis</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.ow2.asm</groupId>
                    <artifactId>asm-tree</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

认识ASM

例1:HelloWorld

首先来看几个简单的例子:

public class HelloWorld {
    public static void main(String[] var0) {
        System.out.println("Hello World");
    }
}

HelloWorld一般我们都是这么写的,但是用 javap -c 反编译一下就会发现完全不一样啦

public class org.itstack.demo.test.HelloWorld {
  public org.itstack.demo.test.HelloWorld();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String Hello World
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}
指令 描述
getstatic 获取静态字段的值
ldc 常量池中的常量值入栈
invokevirtual 运行时方法绑定调用方法
return void函数返回

这几个是上述反编译的字节码指令

当HelloWorld用ASM写出来的时候就会变的非常不一样

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

private static byte[] generate() {
    ClassWriter classWriter = new ClassWriter(0);
    // 定义对象头;版本号、修饰符、全类名、签名、父类、实现的接口
    classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "org/itstack/demo/asm/AsmHelloWorld", null, "java/lang/Object", null);
    // 添加方法;修饰符、方法名、描述符、签名、异常
    MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
    // 执行指令;获取静态属性
    methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
    // 加载常量 load constant
    methodVisitor.visitLdcInsn("Hello World");
    // 调用方法
    methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    // 返回
    methodVisitor.visitInsn(Opcodes.RETURN);
    // 设置操作数栈的深度和局部变量的大小
    methodVisitor.visitMaxs(2, 1);
    // 方法结束
    methodVisitor.visitEnd();
    // 类完成
    classWriter.visitEnd();
    // 生成字节数组
    return classWriter.toByteArray();
}

以上的代码都是来自于 ASM 框架的代码,这里面所有的操作与我们使用使用 javap -c XXX 所反解析出的字节码是一样的,只不过是反过来使用指令来编写代码。

这里有几个比较关键的内容:

  1. 定义一个类的生成 ClassWriter
  2. 设定版本、修饰符、全类名、签名、父类、实现的接口,其实也就是那句;public class HelloWorld
  3. 接下来开始创建方法,方法同样需要设定;修饰符、方法名、描述符等。这里面有几个固定标识;
    1. 类型描述符
      • | Java 类型 | 类型描述符 | |:—|:—| | boolean | Z | | char | C | | byte | B | | short | S | | int | I | | float | F | | long | J | | double | D | | Object | Ljava/lang/Object; | | int[] | [I | | Object[][] | [[Ljava/lang/Object; |
    2. 方法描述符
      • | 源文件中的方法声明 | 方法描述符 | |:—|:—| | void m(int i, float f) | (IF)V | | int m(Object o) | (Ljava/lang/Object;)I | | int[] m(int i, String s) | (ILjava/lang/String;)[I | | Object m(int[] i) | ([I)Ljava/lang/Object; |
    3. ([Ljava/lang/String;)V == void main(String[] args)
  4. 执行指令;获取静态属性。主要是获得 System.out
  5. 加载常量 load constant,输出我们的HelloWorld methodVisitor.visitLdcInsn("Hello World");
  6. 最后是调用输出方法并设置空返回,同时在结尾要设置操作数栈的深度和局部变量的大小

但是单纯的写指令很令人很费解,难以下手所以有一些好用的工具可以帮助我们查看代码的字节码。

当我们开发的时候只要对着这个字节码来编写ASM,基本问题都可以解决。

例2:两数之和计算

我们的目标方法

public class demo {

    public static void main(String[] args) {
        int sum = new demo().sum(1, 1);
        System.out.println(sum);
    }

    public int sum(int i, int m) {
        return i + m;
    }

}

ASM编写的内容


    public static byte[] generate2(){
        ClassWriter classWriter = new ClassWriter(0);

        MethodVisitor methodVisitor_init = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        methodVisitor_init.visitCode();
        methodVisitor_init.visitVarInsn(Opcodes.ALOAD, 0);
        methodVisitor_init.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
        methodVisitor_init.visitInsn(Opcodes.RETURN);
        methodVisitor_init.visitMaxs(1, 1);
        methodVisitor_init.visitEnd();

        classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "com/enmalvi/ASM/demoauto", null, "java/lang/Object", null);

        MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);

        methodVisitor.visitTypeInsn(Opcodes.NEW, "com/enmalvi/ASM/demoauto");
        methodVisitor.visitInsn(Opcodes.DUP);
        methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/enmalvi/ASM/demoauto","<init>","()V");
        methodVisitor.visitInsn(Opcodes.ICONST_1);
        methodVisitor.visitInsn(Opcodes.ICONST_1);

        methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/enmalvi/ASM/demoauto", "sum", "(II)I");
        methodVisitor.visitVarInsn(Opcodes.ISTORE,1);
        methodVisitor.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");
        methodVisitor.visitVarInsn(Opcodes.ILOAD,1);
        methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/io/PrintStream","println","(I)V");
        methodVisitor.visitInsn(Opcodes.RETURN);
        methodVisitor.visitMaxs(3,2);
        methodVisitor.visitEnd();

        // 添加方法;修饰符、方法名、描述符、签名、异常
        MethodVisitor methodVisitor_sum = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "sum", "(II)I", null, null);
        methodVisitor_sum.visitVarInsn(Opcodes.ILOAD, 1);
        methodVisitor_sum.visitVarInsn(Opcodes.ILOAD, 2);
        methodVisitor_sum.visitInsn(Opcodes.IADD);
        // 返回
        methodVisitor_sum.visitInsn(Opcodes.IRETURN);
        // 设置操作数栈的深度和局部变量的大小
        methodVisitor_sum.visitMaxs(2, 3);
        methodVisitor_sum.visitEnd();

        // 类完成
        classWriter.visitEnd();
        // 生成字节数组
        return classWriter.toByteArray();

    }


    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Class<?> clazz = new generate().defineClass("com.enmalvi.ASM.demoauto", generate2(), 0, generate2().length);
        // 反射获取 main 方法
        Method main = clazz.getMethod("main", String[].class);
        // 调用 main 方法
        main.invoke(null, new Object[]{new String[]{}});
    }

看似很难很复杂,其实都是模版化的套路

  1. ClassWriter 创建一个类
  2. MethodVisitor 生成一个方法的访问中者,写入方法的内容最后methodVisitor.visitEnd()结束方法执行闭环,最后classWriter.visitEnd()类完成创建完成,classWriter.toByteArray()生成字节数组。
  3. 通过反射调用创建的类里的方法即可

其中的methodVisitor也是分好几段来创建的和插件展示的字节码是一致的,而ClassWriter只创建一个即可。

例3:在原有方法上字节码增强监控耗时

原有的方法:

public class MyMethod {

    public String queryUserInfo(String uid) {
        System.out.println("xxxx");
        System.out.println("xxxx");
        System.out.println("xxxx");
        System.out.println("xxxx");
        return uid;
    }

}

目标实现的方法:

public class ApiTest {

    public String queryUserInfo(String uid) {
        long var2 = System.nanoTime();
        System.out.println("xxxx");
        System.out.println("xxxx");
        System.out.println("xxxx");
        System.out.println("xxxx");
        System.out.println("方法执行耗时(纳秒)->queryUserInfo:" + (System.nanoTime() - var2));
        return uid;
    }

}

ASM编写

public class TestMonitor extends ClassLoader {

    public static void main(String[] args) throws IOException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {

        // 读取类
        ClassReader cr = new ClassReader(MyMethod.class.getName());
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);

        // 初始化
        {
            MethodVisitor methodVisitor = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
            methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
            methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            methodVisitor.visitInsn(Opcodes.RETURN);
            methodVisitor.visitMaxs(1, 1);
            methodVisitor.visitEnd();
        }

        // 插入信息
        ClassVisitor cv = new ProfilingClassAdapter(cw, MyMethod.class.getSimpleName());
        // 组合字节码信息
        cr.accept(cv, ClassReader.EXPAND_FRAMES);

        byte[] bytes = cw.toByteArray();
        outputClazz(bytes);

        Class<?> clazz = new TestMonitor().defineClass("com.enmalvi.ASM.ASM02.MyMethod", bytes, 0, bytes.length);
        Method queryUserInfo = clazz.getMethod("queryUserInfo", String.class);
        Object obj = queryUserInfo.invoke(clazz.newInstance(), "10001");
        System.out.println("测试结果:" + obj);

    }

    static class ProfilingClassAdapter extends ClassVisitor {

        public ProfilingClassAdapter(final ClassVisitor cv, String innerClassName) {
            super(ASM5, cv);
        }

        // 关键是这里 插入自己想要的内容
        @Override
        public MethodVisitor visitMethod(int access,
                                         String name,
                                         String desc,
                                         String signature,
                                         String[] exceptions) {
            System.out.println("access:" + access);
            System.out.println("name:" + name);
            System.out.println("desc:" + desc);

            if (!"queryUserInfo".equals(name)) return null;

            // 基本固定写法
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
            
            return new ProfilingMethodVisitor(mv, access, name, desc);
        }

    }

    static class ProfilingMethodVisitor extends AdviceAdapter {

        private String methodName = "";

        protected ProfilingMethodVisitor(MethodVisitor methodVisitor, int access, String name, String descriptor) {
            super(ASM5, methodVisitor, access, name, descriptor);
            this.methodName = name;
        }

        // 在方法输入
        @Override
        protected void onMethodEnter() {
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
            mv.visitVarInsn(LSTORE, 2);
            mv.visitVarInsn(ALOAD, 1);
        }

        // 关于方法退出
        @Override
        protected void onMethodExit(int opcode) {
            if ((IRETURN <= opcode && opcode <= RETURN) || opcode == ATHROW) {
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");

                mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
                mv.visitInsn(DUP);
                mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
                mv.visitLdcInsn("方法执行耗时(纳秒)->" + methodName+":");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
                mv.visitVarInsn(LLOAD, 2);
                mv.visitInsn(LSUB);

                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

            }
        }
    }

    private static void outputClazz(byte[] bytes) {
        // 输出类字节码
        FileOutputStream out = null;
        try {
            String pathName = TestMonitor.class.getResource("/").getPath() + "AsmTestMonitor.class";
            out = new FileOutputStream(new File(pathName));
            System.out.println("ASM类输出路径:" + pathName);
            out.write(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != out) try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}
  • 整体的代码块有点大,我们可以分为块来看,如下;
    1. ClassReader cr = new ClassReader(MyMethod.class.getName()); 读取原有类,也是字节码增强的开始
    2. ClassVisitor cv = new ProfilingClassAdapter(cw, MyMethod.class.getSimpleName()); 开始增强字节码
    3. onMethodEnter,onMethodExit,在方法进入和方法退出时添加耗时执行的代码。

结果:

access:1
name:<init>
desc:()V
access:1
name:queryUserInfo
desc:(Ljava/lang/String;)Ljava/lang/String;
ASM类输出路径:/Users/xujiahui/Me/IDEA/enmalvi/case-study/target/classes/AsmTestMonitor.class
xxxx
xxxx
xxxx
xxxx
方法执行耗时(纳秒)->queryUserInfo:44083
测试结果:10001

例4:方法加上TryCatch捕获异常并输出

在字节码增强方面有三个框架;ASM、Javassist、ByteCode,各有优缺点按需选择。

通过 ASM 字节码增强技术,使用指令码将方法修改为我们想要的效果。这部分原本需要使用 JavaAgent技术,在工程启动加载时候进行修改字节码。这里为了将关于字节码核心内容展示出来,通过加载类名称获取字节码进行修改。

这是修改之前的方法

public Integer strToNumber(String str) {
    return Integer.parseInt(str);
}

这是修改之后的方法

    public Integer strToNumber(String str) throws JsonProcessingException {
        try {
            Integer var2 = Integer.parseInt(str);
            point("com.enmalvi.ASM.ASM03.strToNumber", (Object)var2);
            return var2;
        } catch (Exception var3) {
            point("com.enmalvi.ASM.ASM03.strToNumber", (Throwable)var3);
            throw var3;
        }
    }

从修改前到修改后,可以看到。有如下几点修改;

  1. 返回值赋值给新的参数,并做了输出
  2. 把方法包裹在一个 TryCatch 中,并将异常也做了输出

ASM编写

public class MethodTest extends ClassLoader {

    // 测试的方法
    public Integer strToNumber(String str) {
        return Integer.parseInt(str);
    }

    public static void main(String[] args) throws Exception {
        byte[] bytes = new MethodTest().getBytes(MethodTest.class.getName());
        // 输出方法
//        outputClazz(bytes, MethodTest.class.getSimpleName());

        // 测试方法
        Class<?> clazz = new MethodTest().defineClass("com.enmalvi.ASM.ASM03.MethodTest", bytes, 0, bytes.length);
        Method queryUserInfo = clazz.getMethod("strToNumber", String.class);

        // 正确入参
        Object obj01 = queryUserInfo.invoke(clazz.newInstance(), "123");
        System.out.println("01 测试结果:" + obj01);

        // 异常入参
        Object obj02 = queryUserInfo.invoke(clazz.newInstance(), "abc");
        System.out.println("02 测试结果:" + obj02);
    }

    /**
     * 字节码增强获取新的字节码
     */
    private byte[] getBytes(String className) throws IOException {

        /**
         * 首先他会分别创建 ClassReader、ClassWriter,用于对类的加载和写入,这里的加载方式在构造方法中也提供的比较丰富。
         * 可以通过类名、字节码或者流的方式进行处理。
         */
        ClassReader cr = new ClassReader(className);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        cr.accept(new ClassVisitor(ASM5, cw) {

            /**
             * 接下来是对方法的访问 MethodVisitor ,基本所有使用 ASM 技术的监控系统,都会在这里来实现字节码的注入。
             * @param access
             * @param name
             * @param descriptor
             * @param signature
             * @param exceptions
             * @return
             */
            @Override
            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {

                // 方法过滤
                if (!"strToNumber".equals(name)) {
                    return super.visitMethod(access, name, descriptor, signature, exceptions);
                }

                MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);

                return new AdviceAdapter(ASM5, mv, access, name, descriptor) {

                    private Label from = new Label(),
                            to = new Label(),
                            target = new Label();

                    /**
                     * onMethodEnter 方法进入时设置一些基本内容,比如当前纳秒用于后续监控方法的执行耗时。
                     * 还有就是一些 Try 块的开始。
                     */
                    @Override
                    protected void onMethodEnter() {
                        //标志:try块开始位置
                        visitLabel(from);
                        visitTryCatchBlock(from,
                                to,
                                target,
                                "java/lang/Exception");
                    }

                    /**
                     * visitMaxs 这个是在方法结束前,用于添加 Catch 块。到这也就可以将整个方法进行包裹起来了。
                     * @param maxStack
                     * @param maxLocals
                     */
                    @Override
                    public void visitMaxs(int maxStack, int maxLocals) {

                        //标志:try块结束
                        mv.visitLabel(to);
                        //标志:catch块开始位置
                        mv.visitLabel(target);

                        mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Exception"});

                        // 异常信息保存到局部变量
                        int local = newLocal(Type.LONG_TYPE);
                        mv.visitVarInsn(ASTORE, local);

                        // 输出信息
                        mv.visitLdcInsn(className + "." + name);  // 类名.方法名
                        mv.visitVarInsn(ALOAD, local);
                        mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(MethodTest.class), "point", "(Ljava/lang/String;Ljava/lang/Throwable;)V", false);

                        // 抛出异常
                        mv.visitVarInsn(ALOAD, local);
                        mv.visitInsn(ATHROW);

                        super.visitMaxs(maxStack, maxLocals);
                    }

                    /**
                     * onMethodExit 最后是这个方法退出时,用于 RETURN 之前,可以注入结尾的字节码加强,比如调用外部方法输出监控信息。
                     * @param opcode
                     */
                    @Override
                    protected void onMethodExit(int opcode) {
                        // this.nextLocal,获取局部变量的索引值。这个值就让局部变量最后的值,也就是存放 ARETURN 的值(ARETURN,是返回对象类型,如果是返回 int 则需要使用 IRETURN)。
                        if ((IRETURN <= opcode && opcode <= RETURN) || opcode == ATHROW) {
                            int nextLocal = this.nextLocal;
                            mv.visitVarInsn(ASTORE, nextLocal); // 将栈顶引用类型值保存到局部变量indexbyte中。
                            mv.visitVarInsn(ALOAD, nextLocal);  // 从局部变量indexbyte中装载引用类型值入栈。

                            mv.visitLdcInsn(className + "." + name);  // 类名.方法名
                            mv.visitVarInsn(ALOAD, nextLocal); // ALOAD,将异常信息加载到操作数栈用,用于输出。
                            // INVOKESTATIC,调用静态方法。调用方法除了这个指令外还有;invokespecial、invokevirtual、invokeinterface。
                            mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(MethodTest.class), "point", "(Ljava/lang/String;Ljava/lang/Object;)V", false);
                        }
                    }
                };
            }
        }, ClassReader.EXPAND_FRAMES);

        return cw.toByteArray();
    }

    /**
     * 输出字节码
     *
     * @param bytes     字节码
     * @param className 类名称
     */
    private static void outputClazz(byte[] bytes, String className) {
        // 输出类字节码
        FileOutputStream out = null;
        try {
            String pathName = MethodTest.class.getResource("/").getPath() + className + "SQM.class";
            out = new FileOutputStream(new File(pathName));
            System.out.println("ASM字节码增强后类输出路径:" + pathName + "\r\n");
            out.write(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != out) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void point(String methodName, Throwable throwable) {
        System.out.println("系统监控 :: [方法名称:" + methodName + " 异常信息:" + throwable.getMessage() + "]\r\n");
    }

    public static void point(String methodName, Object response) throws JsonProcessingException {
        System.out.println("系统监控 :: [方法名称:" + methodName + " 输出信息:" + new ObjectMapper().writeValueAsString(response) + "]\r\n");
    }

}

以上这段代码就是 ASM 用于处理字节码增强的模版代码块。首先他会分别创建 ClassReader、ClassWriter,用于对类的加载和写入,这里的加载方式在构造方法中也提供的比较丰富。可以通过类名、字节码或者流的方式进行处理。

接下来是对方法的访问 MethodVisitor ,基本所有使用 ASM 技术的监控系统,都会在这里来实现字节码的注入。这里面目前用到了三个方法的,如下;

  1. onMethodEnter 方法进入时设置一些基本内容,比如当前纳秒用于后续监控方法的执行耗时。还有就是一些 Try 块的开始。
  2. visitMaxs 这个是在方法结束前,用于添加 Catch 块。到这也就可以将整个方法进行包裹起来了。
  3. onMethodExit 最后是这个方法退出时,用于 RETURN 之前,可以注入结尾的字节码加强,比如调用外部方法输出监控信息。

基本上所有的 ASM 字节码增强操作,都离不开这三个方法。

结果:

系统监控 :: [方法名称:com.enmalvi.ASM.ASM03.MethodTest.strToNumber 输出信息:123]

01 测试结果:123
系统监控 :: [方法名称:com.enmalvi.ASM.ASM03.MethodTest.strToNumber 异常信息:For input string: "abc"]

我们常用的非入侵的监控系统,全链路监控,以及一些反射框架中,其实都用到了 ASM,只是还没有注意到而已。

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

1 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 593828浏览

随机文章
SpringSecurity—两种资源放行策略
4年前
MyBatis笔记2—HelloWorld
5年前
RabbitMQ—简单队列模式
5年前
Java—设计模式Builder模式
5年前
Kotlin-协程(专)—Channel 篇(三十九)
4年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 593828 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付