关于java:ASM入门篇

81次阅读

共计 11100 个字符,预计需要花费 28 分钟才能阅读完成。

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 源码、架构、算法和面试。

正文完
 0