1 概述

ASM是Java中比较流行的用来读写字节码的类库,用来基于字节码层面对代码进行分析和转换。

ASM是一个Java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。

ASM可以直接产生二进制class文件,也可在类被加载入虚拟机之前动态改变类行为, ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能根据要求生成新类。目前许多框架如cglib、Hibernate、 Spring都直接或间接使用ASM操作字节码。

在读写的过程中可以加入自定义的逻辑以增强或修改原来已编译好的字节码,比如CGLIB用它来实现动态代理。ASM被设计用于在运行时对Java类进行生成和转换,当然也包括离线处理。ASM短小精悍、且速度很快,从而避免在运行时动态生成字节码或转换时对程序速度的影响,又因为它体积小巧,可以在很多内存受限的环境中使用。
ASM的主要优势包括如下几个方面:

  1. 它又一个很小,但设计良好并且模块化的API,且易于使用。
  2. 它具有很好的文档,并且还有eclipse插件。
  3. 它支持最新的Java版本。
  4. 它短小精悍、快速、健壮。
  5. 它又一个很大的用户社区,可以给新用户提供支持。
  6. 它的开源许可允许你几乎以任何方式来使用它。
编程模型
  • Core API
    提供了基于事件形式的编程模型。该模型不需要一次性将整个类的结构读取到内存中,因此这种方式更快,需要更少的内存,但这种编程方式难度较大
  • Tree API
    提供了基于树形的编程模型。该模型需要一次性将一个类的完整结构全部读取到内存当中,所以这种方法需要更多的内存,这种编程方式较简单
ASM Core设计

主要有以下几个类、接口(org.objectweb.asm包):

  • ClassReader类:字节码的读取与分析引擎。它采用类似SAX的事件读取机制,每当有事件发生时,调用注册的ClassVisitor、AnnotationVisitor、FieldVisitor、MethodVisitor做相应的处理

  • ClassVisitor接口:该接口中的每个方法,对应了 class 文件中的每一项。定义在读取Class字节码时会触发的事件,如类头解析完成、注解解析、字段解析、方法解析等
    解析器使ClassVisitor访问 JVMS 中定义的Class文件结构。 此类解析ClassFile内容,并为遇到的每个字段,方法和字节码指令调用给定ClassVisitor的适当访问方法。

  • ClassAdapter
    ClassAdapter是ClassVisitor的实现类,实现要变化的功能

  • AnnotationVisitor接口:定义在解析注解时会触发的事件,如解析到一个基本值类型的注解、enum值类型的注解、Array值类型的注解、注解值类型的注解等

  • FieldVisitor接口:定义在解析字段时触发的事件,如解析到字段上的注解、解析到字段相关的属性等。
    MethodVisitor接口:定义在解析方法时触发的事件,如方法上的注解、属性、代码等。

  • ClassWriter类
    它实现了ClassVisitor接口,用于拼接字节码,输出变化后的字节码

  • AnnotationWriter类:它实现了AnnotationVisitor接口,用于拼接注解相关字节码

  • FieldWriter类:它实现了FieldVisitor接口,用于拼接字段相关字节码

  • MethodWriter类:它实现了MethodVisitor接口,用于拼接方法相关字节码。

  • SignatureReader类:对类定义、字段定义、方法定义、本地变量定义的签名的解析。Signature因范型引入,用于存储范型定义时的元数据(因为这些元数据在运行时会被擦除)。

  • SignatureVisitor接口:定义在解析Signature时会触发的事件,如正常的Type参数、类或接口的边界等。
    SignatureWriter类:它实现了SignatureVisitor接口,用于拼接范型相关字节码。

  • Attribute类:字节码中属性的类抽象。

  • ByteVector类:字节码二进制存储的容器。

  • Opcodes接口:字节码指令的一些常量定义。

  • Type类:类型相关的常量定义以及一些基于其上的操作。

类图关系

ClassReader是ASM中最核心的实现,它用于读取并解析Class字节码
ASM 字节码增强框架详解_经验分享

在构建ClassReader实例时,它首先保存字节码二进制数组b,然后创建items数组,数组的长度在字节码数组的第8、9个字节指定(最前面4个字节是魔数CAFEBABE,之后2个字节是次版本号,再后2个字节是主版本号),每个item表示常量池项在字节码数组的偏移量加1(常量池中每个项由1个字节的type和紧跟的字节数组表示,常量池项有12种类型,其中CONSTANT_FieldRef_Info、CONSTANT_MethodRef_Info、CONSTANT_InterfaceMethodRef_Info、CONSTANT_NameAndType_Info包括其类型字节占用5个字节,另外4个字节每2个字节为字段、方法等所在的类、其名称、描述符在当前常量池中CONSTANT_Utf8_Info类型的引用;CONSTANT_Integer_Info、CONSTANT_Float_Info包括其类型字节占用5个字节,另外四个字节为其对应的值;CONSTANT_Class_Info、CONSTANT_String_Info包括其类型字节占用3个字节,另外两个字节为在当前常量池CONSTANT_Utf8_Info项的索引;CONSTANT_Utf8_Info类型第1个字节表示类型,第2、3个字节为该项所表示的字符串的长度);CONSTANT_Double_Info、CONSTANT_Long_Info加类型字节为9个字;maxStringLength表示最长的UTF8类型的常量池项的值,用于决定在解析CONSTANT_Utf8_Info类型项时最大需要的字符数组;header表示常量池之后的字节码的第一个字节。

ASM 开发

IDEA使用插件 asm outline 查看 asm 如何生成该类。
ASM 字节码增强框架详解_经验分享_02

  • 类实现ASM 字节码增强框架详解_经验分享_03
    对应的 asm 编程代码
package asm.com.javaedge.asm;

import java.util.*;

import org.objectweb.asm.*;

public class TestAsmDump implements Opcodes {

    public static byte[] dump() throws Exception {

        ClassWriter cw = new ClassWriter(0);
        FieldVisitor fv;
        MethodVisitor mv;
        AnnotationVisitor av0;

        cw.visit(52, ACC_PUBLIC + ACC_SUPER, "com/javaedge/asm/TestAsm", null, "java/lang/Object", null);

        cw.visitSource("TestAsm.java", null);

        {
            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(7, l0);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            mv.visitInsn(RETURN);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLocalVariable("this", "Lcom/javaedge/asm/TestAsm;", null, l0, l1, 0);
            mv.visitMaxs(1, 1);
            mv.visitEnd();
        }
        {
            mv = cw.visitMethod(ACC_PUBLIC, "m", "()V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            Label l1 = new Label();
            Label l2 = new Label();
            mv.visitTryCatchBlock(l0, l1, l2, "java/lang/InterruptedException");
            Label l3 = new Label();
            mv.visitLabel(l3);
            mv.visitLineNumber(10, l3);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("Hello ASM");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv.visitLabel(l0);
            mv.visitLineNumber(12, l0);
            mv.visitLdcInsn(new Long(100L));
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false);
            mv.visitLabel(l1);
            mv.visitLineNumber(15, l1);
            Label l4 = new Label();
            mv.visitJumpInsn(GOTO, l4);
            mv.visitLabel(l2);
            mv.visitLineNumber(13, l2);
            mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/InterruptedException"});
            mv.visitVarInsn(ASTORE, 1);
            Label l5 = new Label();
            mv.visitLabel(l5);
            mv.visitLineNumber(14, l5);
            mv.visitVarInsn(ALOAD, 1);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/InterruptedException", "printStackTrace", "()V", false);
            mv.visitLabel(l4);
            mv.visitLineNumber(16, l4);
            mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
            mv.visitInsn(RETURN);
            Label l6 = new Label();
            mv.visitLabel(l6);
            mv.visitLocalVariable("e", "Ljava/lang/InterruptedException;", null, l5, l4, 1);
            mv.visitLocalVariable("this", "Lcom/javaedge/asm/TestAsm;", null, l3, l6, 0);
            mv.visitMaxs(2, 2);
            mv.visitEnd();
        }
        cw.visitEnd();

        return cw.toByteArray();
    }
}

看看最简单的统计方法执行时间

            ...
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LSTORE, 1);
            ...
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("Hello ASM");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            mv.visitLabel(l0);
            mv.visitLineNumber(13, l0);
            mv.visitLdcInsn(new Long(100L));
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false);
            mv.visitLabel(l1);
            mv.visitLineNumber(16, l1);
            Label l5 = new Label();
            mv.visitJumpInsn(GOTO, l5);
            mv.visitLabel(l2);
            mv.visitLineNumber(14, l2);
            mv.visitFrame(Opcodes.F_FULL, 2, new Object[]{"com/javaedge/asm/TestAsm", Opcodes.LONG}, 1, new Object[]{"java/lang/InterruptedException"});
            mv.visitVarInsn(ASTORE, 3);
            Label l6 = new Label();
            mv.visitLabel(l6);
            mv.visitLineNumber(15, l6);
            mv.visitVarInsn(ALOAD, 3);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/InterruptedException", "printStackTrace", "()V", false);
            mv.visitLabel(l5);
            mv.visitLineNumber(17, l5);
            mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LSTORE, 3);
            Label l7 = new Label();
            mv.visitLabel(l7);
            mv.visitLineNumber(18, l7);
            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("m\u65b9\u6cd5\u6267\u884c\u65f6\u95f4: ");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(LLOAD, 3);
            mv.visitVarInsn(LLOAD, 1);
            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);
            Label l8 = new Label();
            mv.visitLabel(l8);
            mv.visitLineNumber(19, l8);
            mv.visitInsn(RETURN);
            Label l9 = new Label();
            mv.visitLabel(l9);
            mv.visitLocalVariable("e", "Ljava/lang/InterruptedException;", null, l6, l5, 3);
            mv.visitLocalVariable("this", "Lcom/javaedge/asm/TestAsm;", null, l3, l9, 0);
            mv.visitLocalVariable("start", "J", null, l4, l9, 1);
            mv.visitLocalVariable("end", "J", null, l7, l9, 3);
            mv.visitMaxs(6, 5);
            mv.visitEnd();
        }
        cw.visitEnd();

        return cw.toByteArray();
    }
}

这样我们就知道之后用 ASM 编程该加哪些代码了。