ASM简介

ASM是一个通用的Java字节码操作和剖析框架,它能够用来批改现有的类或间接以二进制模式动静生成类。ASM提供了一些常见的字节码转换和剖析算法,从中能够构建定制的简单转换和代码剖析工具。ASM提供了与其余Java字节码框架相似的性能,但侧重于性能。因为它的设计和实现都尽可能小和快,所以它非常适合在动静零碎中应用(当然也能够以动态形式应用,例如在编译器中)。

ASM被用在很多我的项目中,包含如下:

  • OpenJDK,生成lambda调用站点,以及Nashorn编译器;
  • Groovy编译器和Kotlin编译器;
  • Cobertura和Jacoco,以工具化类来度量代码覆盖率;
  • CGLIB,用于动静生成代理类;
  • Gradle,在运行时生成一些类;

更多参考官网:https://asm.ow2.io/

IDE插件

ASM是间接对字节码进行操作,如果不相熟字节码操作汇合的话,写起来会很吃力,所以ASM为支流的IDE专门提供了开发插件BytecodeOutline:

  • IDEA:ASM Bytecode Outline
  • Eclipse:BytecodeOutline

以IDEA为例,只须要对应的类中右击->Show Bytecode outline即可,大抵如下图所示:

面板中蕴含三个页签:

  • Bytecode:类对应的字节码文件;
  • ASMified:利用ASM生成字节码对应的代码;
  • Groovified:类对应的字节码指令;

ASM API

ASM库提供了两个用于生成和转换已编译类的API,一个是外围API,以基于事件的模式来示意类;另一个是树API,以基于对象的模式来示意类;能够比照XML文件解析的形式:SAX模式和DOM模式;外围API对应SAX模式,树API对应DOM模式;每种模式都有本人的优缺点:

  • 基于事件的API要快于基于对象的API,所须要的内存也较少,但在应用基于事件的API时,类转换的实现可能要更难一些;
  • 基于对象的API会把整个类加载到内存中;

ASM库组织在几个包中,这些包散布在几个独自的JAR文件中:

  • org.objectweb.asmorg.objectweb.asm.signature包:定义基于事件的API并提供类解析器和编写器组件,它们蕴含在asm.jar中;
  • org.objectweb.asm.util包:提供基于外围API的各种工具,这些工具可在ASM应用程序的开发和调试过程中应用,蕴含在asm-util.jar中;
  • org.objectweb.asm.commons包:提供了几个有用的预约义类转换器,次要基于外围API,蕴含在asm-commons.jar中;
  • org.objectweb.asm.tree包:定义基于对象的API,并提供用于在基于事件的示意和基于对象的示意之间进行转换的工具,蕴含在asm-tree.jar 中;
  • org.objectweb.asm.tree.analysis包:包提供了一个基于树API的类剖析框架和几个预约义的类分析器,蕴含在asm-analysis.jar中;

外围API

在学习外围API之前,倡议理解一下访问者模式,因为ASM对字节码的操作和剖析都是基于访问者模式来实现的;

访问者模式

访问者模式倡议将新行为放入一个名为访问者的独立类中, 而不是试图将其整合到已有类中。当初, 须要执行操作的原始对象将作为参数被传递给访问者中的办法, 让办法能拜访对象所蕴含的所有必要数据;常见的利用场景:

  • 如果你须要对一个简单对象构造 (例如对象树) 中的所有元素执行某些操作, 可应用访问者模式;
  • 可应用访问者模式来清理辅助行为的业务逻辑;
  • 当某个行为仅在类层次结构中的一些类中有意义, 而在其余类中没有意义时, 可应用该模式;

字节码其实就是一个简单的对象构造,还有像Sharding-Jdbc中对sql的解析也用到访问者模式,能够发现都是一些数据结构比较稳定的数据,固定的语法;

更多参考:访问者模式

访问者模式有两个外围类别离是:独立的访问者、接管访问者事件产生器;对应的ASM外面就是两个外围类:ClassVisitorClassReader,上面别离进行介绍;

ClassVisitor

用于生成和转换编译类的ASM API基于ClassVisitor抽象类,这个类中的每个办法都对应于同名的类文件构造:

public abstract class ClassVisitor {    public ClassVisitor(int api);    public ClassVisitor(int api, ClassVisitor cv);    public void visit(int version, int access, String name,String signature, String superName, String[] interfaces);    public void visitSource(String source, String debug);    public void visitOuterClass(String owner, String name, String desc);    AnnotationVisitor visitAnnotation(String desc, boolean visible);    public void visitAttribute(Attribute attr);    public void visitInnerClass(String name, String outerName,String innerName, int access);    public FieldVisitor visitField(int access, String name, String desc,String signature, Object value);    public MethodVisitor visitMethod(int access, String name, String desc,String signature, String[] exceptions);    void visitEnd();}

内容能够具备任意长度和复杂性的部件将通过返回辅助访问者类,次要包含:AnnotationVisitorFieldVisitorMethodVisitor;更多能够参考Java 虚拟机标准

以上所有办法都会被事件产生器ClassReader调用,所有办法中的参数都是ClassReader提供的,当然调用每个办法是有程序的:

visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )* ( visitInnerClass | visitField |visitMethod )* visitEnd

首先调用visit,而后是对visitSource 的最多一个调用,接下来是对visitOuterClass 的最多一个调用 , 而后是可按任意程序对 visitAnnotationvisitAttribute的任意多个拜访 , 接下来是可按任意程序对 visitInnerClassvisitFieldvisitMethod 的任意多个调用,最初以一个visitEnd调用完结。

ClassReader

此类次要性能就是读取字节码文件,而后把读取的数据告诉ClassVisitor,字节码文件能够多种形式传入:

  • public ClassReader(final InputStream inputStream):字节流的形式;
  • public ClassReader(final String className):文件全门路;
  • public ClassReader(final byte[] classFile):二进制文件;

常见应用形式如下所示:

ClassReader classReader = new ClassReader("com/zh/asm/TestService");ClassWriter classVisitor = new ClassWriter(ClassWriter.COMPUTE_MAXS);classReader.accept(classVisitor, 0);

ClassReaderaccept办法解决接管一个访问者,还包含另外一个parsingOptions参数,选项包含:

  • SKIP_CODE:跳过已编译代码的拜访(如果您只须要类构造,这可能很有用);
  • SKIP_DEBUG:不拜访调试信息,也不为其创立人工标签;
  • SKIP_FRAMES:跳过堆栈映射帧;
  • EXPAND_FRAMES:解压缩这些帧;

ClassWriter

以上实例中应用了ClassWriter,其继承于ClassVisitor,次要用来生成类,能够独自应用,如下所示:

ClassWriter cw = new ClassWriter(0);cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,"pkg/Comparable", null, "java/lang/Object",new String[]{"pkg/Mesurable"});cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS","I", null, new Integer(-1)).visitEnd();cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL","I", null, new Integer(0)).visitEnd();cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER","I", null, new Integer(1)).visitEnd();cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo","(Ljava/lang/Object;)I", null, null).visitEnd();cw.visitEnd();byte[] b = cw.toByteArray();//输入FileOutputStream fileOutputStream = new FileOutputStream(new File("F:/asm/Comparable.class"));fileOutputStream.write(b);fileOutputStream.close();

以上通过ClassWriter生成一个字节码文件,而后转换成字节数组,最初通过FileOutputStream输入到文件中,反编译后果如下:

package pkg;public interface Comparable extends Mesurable {    int LESS = -1;    int EQUAL = 0;    int GREATER = 1;    int compareTo(Object var1);}

在实例化ClassWriter须要提供一个参数flags,选项包含:

  • COMPUTE_MAXS:将为你计算局部变量与操作数栈局部的大小;还是必须调用 visitMaxs,但能够应用任何参数:它们将被疏忽并从新计算;应用这一选项时,依然必须自行计算这些帧;
  • COMPUTE_FRAMES:一切都是主动计算;不再须要调用 visitFrame,但依然必须调用 visitMaxs(参数将被疏忽并从新计算);
  • 0:不会主动计算任何货色;必须自行计算帧、局部变量与操作数栈的大小;

以上只是对ClassWriter的独自应用,但更有意义的其实是把以上三个外围类整合起来应用,上面重点看看转换操作;

转换操作

在类读取器和类写入器之间引入一个 ClassVisitor,把三者整合起来,大抵代码构造如下所示:

ClassReader classReader = new ClassReader("com/zh/asm/TestService");ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);//解决ClassVisitor classVisitor = new AddFieldAdapter(classWriter...);classReader.accept(classVisitor, 0);

上述代码绝对应的体系结构如下图所示:

这里提供了一个增加属性的适配器,能够重写visitEnd办法,而后写入新的属性,代码如下:

public class AddFieldAdapter extends ClassVisitor {    private int fAcc;    private String fName;    private String fDesc;    //是否曾经有雷同名称的属性    private boolean isFieldPresent;    public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName,                           String fDesc) {        super(ASM4, cv);        this.fAcc = fAcc;        this.fName = fName;        this.fDesc = fDesc;    }    @Override    public FieldVisitor visitField(int access, String name, String desc,                                   String signature, Object value) {        //判断是否有雷同名称的字段,不存在才会在visitEnd中增加        if (name.equals(fName)) {            isFieldPresent = true;        }        return cv.visitField(access, name, desc, signature, value);    }    @Override    public void visitEnd() {        if (!isFieldPresent) {            FieldVisitor fv = cv.visitField(fAcc, fName, fDesc, null, null);            if (fv != null) {                fv.visitEnd();            }        }        cv.visitEnd();    }}

依据ClassVisitor的每个办法被调用的程序,如果类中有多个属性,那么visitField会被调用屡次,每次都会查看要增加的字段是否曾经有了,而后保留在isFieldPresent标识中,这样在拜访最初的visitEnd中判断是否须要增加新属性;

ClassVisitor classVisitor = new AddFieldAdapter(classWriter,ACC_PUBLIC + ACC_FINAL + ACC_STATIC,"id","I");

这里增加了一个public static final int id;能够把字节数组写入class类文件中,而后反编译查看:

public class TestService {    public static final int id;    ......}

工具类

除了下面几个外围类之外,ASM也提供了一些工具类,不便用户应用:

  • Type
    Type对象示意一种 Java类型,既能够由类型描述符结构,也能够由Class对象构建;Type类还蕴含示意基元类型的动态变量;
  • TraceClassVisitor
    扩大了ClassVisitor类,并构建了所拜访类的文本示意;应用TraceClassVisitor以便取得理论生成内容的可读跟踪;
  • CheckClassAdapter
    ClassWriter 类并不会核实对其办法的调用程序是否失当,以及参数是否无效;因而有可能会生成一些被 Java 虚拟机验证器回绝的有效类。为了尽可能提前检测出局部此类谬误,能够应用CheckClassAdapter类 ;
  • ASMifier
    这个类为TraceClassVisitor工具提供了一个可选的后端(默认状况下,它应用一个Textifier后端,产生下面显示的输入类型)。这个后端使TraceClassVisitor类的每个办法打印用于调用它的Java代码。

办法

在介绍下面的ClassVisitor在拜访复杂性的部件将通过返回辅助访问者类,其中包含:AnnotationVisitorFieldVisitorMethodVisitor;在介绍MethodVisitor之前理解一下Java 虚拟机执行模型;

执行模型

每个办法被执行的时候,Java虚拟机都会同步创立一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动静连贯、办法进口等信
息。每一个办法被调用直至执行结束的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程;

  • 局部变量表:蕴含可由其索引以随机程序拜访的变量;
  • 操作数栈:字节码指令用作操作数的值堆栈;

看一个具备3帧的执行栈:

第一帧:蕴含3个局部变量,操作数栈最大值为4,蕴含2个值;

第二帧:蕴含2个局部变量,操作数栈最大值为3,蕴含2个值;

第三帧:蕴含4个局部变量,操作数栈最大值为2,蕴含2个值;

字节代码指令

字节码指令由标识该指令的操作码和固定数量的参数组成:

  • 操作码:是一个无符号字节值,由助记符号标识。例如,操作码值0由助记符NOP设计,并对应于不执行任何操作的指令。
  • 参数:是动态值,确定了准确的指令行为。它们紧跟在操作码之后给出。

字节码指令分为两类:

  • 一小部分指令用于将值从局部变量转移到操作数堆栈;
  • 其余指令只作用于操作数堆栈:它们从堆栈中弹出一些值,依据这些值计算结果,而后将其推回堆栈;

局部变量指令:

  • ILOAD:用于加载一个 boolean、byte、 char、short 或int 局部变量;
  • LLOAD, FLOAD, DLOAD :别离用于加载 long、float 或 double值;
  • ALOAD:用于加载任意非基元值,即对象和数组援用;

操作数栈指令:

  • ISTORE:从操作数栈中弹出一个boolean、byte、 char、short 或int 局部变量值,并将它存储在由其索引i指定的局部变量中;
  • LSTORE,FSTORE,DSTORE:别离弹出 long、float 或 double值;
  • ASTORE:用于弹出任意非基元值;
  • GETFIELDPUTFIELDGETFIELD owner name desc弹出一个对象援用,并推送其name字段的值;
    PUTFIELD owner name desc弹出一个值和一个对象援用,并将该值存储在其name字段中;
    在这两种状况下,对象必须是owner类型,其字段必须是desc类型。GETSTATICPUTSTATIC是相似的指令,然而对于动态字段。
  • INVOKEVIRTUAL、INVOKESTATIC、INVOKESPECIAL、INVOKEINTERFACE、INVOKEDYNAMIC
    INVOKEVIRTUAL owner name desc调用类owner中定义的name办法,其办法描述符为descINVOKESTATIC用于静态方法,INVOKESPECIAL用于公有办法和构造函数,INVOKEINTERFACE用于接口中定义的办法。最初,对于java7类,INVOKEDYNAMIC用于新的动静办法调用机制。

MethodVisitor

用于生成和转换已编译办法的ASM API是基于MethodVisitor抽象类的;它由ClassVisitorvisitMethod办法返回;此类还依据这些指令的参数数量和类型为每个字节码指令类别定义了一个办法;必须按以下顺序调用这些办法:

visitAnnotationDefault? ( visitAnnotation | visitParameterAnnotation | visitAttribute )*( visitCode( visitTryCatchBlock | visitLabel | visitFrame | visitXxx Insn |visitLocalVariable | visitLineNumber )*visitMaxs )?visitEnd

上面看一个对现有办法进行转换实例,给办法增加开始和完结日志;

  1. 筹备须要被转换的实例,在query办法解决前和解决后增加日志;

    public class TestService {    public void query(int param) {        System.out.println("service handle...");    }}
  2. 重写ClassVisitor中的visitMethod

    public class MyClassVisitor extends ClassVisitor implements Opcodes {    public MyClassVisitor(ClassVisitor cv) {        super(ASM5, cv);    }    @Override    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {        MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature,                exceptions);        if (!name.equals("<init>") && methodVisitor != null) {            methodVisitor = new MyMethodVisitor(methodVisitor);        }        return methodVisitor;    }}

过滤掉<init>办法,其余办法都会被MyMethodVisitor包装,而后重写MethodVisitor的办法;

  1. 重载MethodVisitor

    public class MyMethodVisitor extends MethodVisitor implements Opcodes {    public MyMethodVisitor(MethodVisitor mv) {        super(Opcodes.ASM4, mv);    }    @Override    public void visitCode() {        super.visitCode();        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");        mv.visitLdcInsn("start");        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);    }    @Override    public void visitInsn(int opcode) {        if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)                || opcode == Opcodes.ATHROW) {            //办法在返回之前打印"end"            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");            mv.visitLdcInsn("end");            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);        }        mv.visitInsn(opcode);    }}

visitCode办法拜访之前调用,visitInsn须要判断操作符是不是办法返回,个别办法在返回之前会执行mv.visitInsn(RETURN)操作,这时候能够通过opcode来判断;

  1. 查看生成的新的字节码文件

    public class TestService {    public TestService() {    }    public void query(int var1) {        System.out.println("start");        System.out.println("service handle...");        System.out.println("end");    }}

工具类

在办法上面也同样提供了一些工具类:

  • LocalVariablesSorter:此办法适配器将一个办法中应用的局部变量依照它们在这个办法中的呈现程序从新进行编号,同时能够应用 newLocal 办法创立一个新的局部变量;
  • AdviceAdapter:此办法适配器是一个抽象类,可用于在办法的结尾以及任何RETURNATHROW指令之前插入代码;其次要长处是它也实用于构造函数,其中代码不能仅插入构造函数的结尾,而是在调用超级构造函数之后插入。

应用场景

ASM被用在很多我的项目中,这里介绍两种常见的应用场景:AOP和代替反射;

AOP

面向切面编程,在程序开发中次要用来解决一些零碎层面上的问题,比方日志,事务,权限期待;其中关键技术就是代理,代理包含动静代理和动态代理,实现的形式也有多种:

  • AspectJ:属于动态织入,原理是动态代理;
  • JDK动静代理:JDK动静代理两个外围类:ProxyInvocationHandler
  • Cglib动静代理:封装了ASM,能够再运行期动静生成新的Class;性能上比JDK动静代理更弱小;

其中的Cglib动静代理形式就依赖ASM,下面的实例中咱们也看到了ASM的字节码加强性能;

代替反射

FastJson以速度快著称,其中有一项就是应用ASM代替了Java反射;另外还有ReflectASM包专门用来代替Java反射;

ReflectASM 是一个十分小的 Java 类库,通过代码生成来提供高性能的反射解决,主动为 get/set 字段提供拜访类,拜访类应用字节码操作而不是 Java 的反射技术,因而十分快。

看一段ReflectASM简略应用形式:

TestBean testBean = new TestBean(1, "zhaohui", 18);MethodAccess methodAccess = MethodAccess.get(TestBean.class);String[] mns = methodAccess.getMethodNames();for (int i = 0; i < mns.length; i++) {    System.out.println(methodAccess.invoke(testBean, mns[i]));}

这里失常打印TestBean中的属性值,为什么速度快,因为外部会通过ASM生成一个长期的TestBeanMethodAccess,外部重写了invoke办法,反编译之后如下所示:

public Object invoke(Object var1, int var2, Object... var3) {        TestBean var4 = (TestBean)var1;        switch(var2) {        case 0:            return var4.getName();        case 1:            return var4.getId();        case 2:            return var4.getAge();        default:            throw new IllegalArgumentException("Method not found: " + var2);        } }

能够发现invoke外面其实就是一般的调用,速度必定比应用java反射快。

参考文档

asm4-guide.pdf

ASM4手册中文版

感激关注

能够关注微信公众号「回滚吧代码」,第一工夫浏览,文章继续更新;专一Java源码、架构、算法和面试。