关于后端:Java-字节码操纵框架ASM

6次阅读

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

大家好我是悦, 之前的文章咱们介绍了字节码的基础知识,明天咱们将介绍字节码相干的利用场景,首先要介绍的是如何对字节码做解析和批改,本文将会具体给大家介绍一个工业级字节码操作框架 ASM。

ASM

当咱们须要对一个 class 文件做批改时,咱们能够抉择本人解析这个 class 文件,在合乎 Java 字节码标准的前提下进行字节码革新。如果你写过 class 文件的解析程序,会发现这个过程极其繁琐,更别说进行减少办法等操作了。

ASM 最开始是 2000 年 Eric Bruneton 在 INRIA(法国国立计算机及自动化研究院)读博士期间实现的一个作品。那个时候蕴含 java.lang.reflect.Proxy 包的 JDK 1.3 还没公布,ASM 被作为代码生成器,用来生成动静代理的代理类。通过多年的倒退,ASM 在诸多框架中曾经遍地开花,成为字节码操作畛域事实上的规范。

简略的 API 背地 ASM 主动帮咱们做了很多事件,比方保护常量池的索引,计算最大栈大小 max_stack,局部变量表大小 max_locals 等,除此之外还有上面这些长处:

  • 架构设计精美,使用方便。
  • 更新速度快,反对最新的 Java 版本
  • 速度十分快,在动静代理 class 的生成和 class 的转换时,尽可能确保运行中的利用不会被 ASM 拖慢
  • 十分牢靠、久经考验,曾经有很多驰名的开源框架都在应用,例如 cglib,、mybatis、fastjson
    其它字节码操作框架在操作字节码的过程中生成大量的两头类和对象,消耗大量的内存且运行迟缓,ASM 应用了访问者(Visitor)设计模式,防止了创立和耗费大量的两头变量。

ASM 提供了两种生成和转换类的办法: 基于事件触发的 core API 和基于对象的 Tree API,这两种形式能够用 XML 解析的 SAX 和 DOM 形式来对照。

  • SAX 解析 XML 文件采纳的是事件驱动,它不须要解析残缺个文档,而是一边按内容程序解析文档,如果解析时合乎特定的事件则回调一些函数来处理事件。SAX 运行时是单向的、流式的,解析过的局部无奈在不从新开始的状况下再次读取,ASM 的 Core API 相似于这种形式。
  • DOM 解析形式则会将整个 XML 作为相似树结构的形式读入内存中以便操作及解析,ASM 的 Tree API 相似于这种形式。
    以上面的 XML 文件为例:

    <Order>
      <Customer>Arthur</Customer>
      <Product>
          <Name>Birdsong Clock</Name>
          <Quantity>12</Quantity>
          <Price currency="USD">21.95</Price >
      </Product>
    </Order>

    对应的 SAX 和 DOM 解析形式的如下图所示:

ASM 外围类介绍

ClassReader

它是字节码读取和剖析引擎,帮咱们做了最苦最累的解析二进制的 class 文件字节码的活。采纳相似于 SAX 的事件读取机制,每当有事件产生时,触发相应的 ClassVisitor、MethodVisitor 等做相应的解决。

ClassVisitor

它是一个抽象类,ClassReader 对象创立之后,调用 ClassReader.accept() 办法,传入一个 ClassVisitor 对象。ClassVisitor 在解析字节码的过程中遇到不同的节点时会调用不同的 visit() 办法,比方 visitSource, visitOuterClass, visitAnnotation, visitAttribute, visitInnerClass, visitField, visitMethod 和 visitEnd 办法。在上述 visit 的过程中还会产生一些子过程,比方 visitAnnotation 会触发 AnnotationVisitor 的调用、visitMethod 会触发 MethodVisitor 的调用。正是在这些 visit 的过程中,咱们得以有机会去批改各个子节点的字节码。

ClassVisitor 类中的 visit 办法必须依照以下的程序被调用执行:

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

visit 办法最先被调用,接着调用零次或一次 visitSource 办法,接着调用零次或一次 visitOuterClass 办法,再接下来按任意顺序调用任意屡次 visitAnnotation 和 visitAttribute 办法,再接下来按任意顺序调用任意屡次 visitInnerClass、visitField、visitMethod 办法,visitEnd 最初被调用。

ClassWriter

这个类是 ClassVisitor 抽象类的一个实现类,其之前的每个 ClassVisitor 都可能对原始的字节码做批改,ClassWriter 的 toByteArray 办法则把最终批改的字节码以 byte 数组的模式返回

这三个外围类的关系如下图

一个最简略的用法如上面的代码所示:

public class FooClassVisitor extends ClassVisitor {
    ...
    // visitXXX() 函数
    ...
}

ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(cr,
        ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new FooClassVisitor(cw);
cr.accept(cv, 0);

下面的代码中,ClassReader 负责读取类文件字节数组,accept 调用之后 ClassReader 会把解析字节码过程的事件源源不断的告诉给 ClassVisitor 对象调用不同的 visit 办法,ClassVisitor 能够在这些 visit 办法中对字节码进行批改,ClassWriter 能够生成最终批改过的本人字节码。

ASM 操作字节码案例

接上面咱们用几个简略的例子来演示 ASM 各个外围类操作字节码的案例。

拜访类的办法和字段

ASM 的 visitor 设计模式能够很不便的用来拜访类文件中咱们感兴趣的局部,比方类文件的字段和办法列表,有上面的类:

public class MyMain {
    public int a = 0;
    public int b = 1;
    public void test01() {}
    public void test02() {}
}   

应用 javac 编译为 class 文件,能够用上面的 ASM 代码来输入类的办法和字段列表:

byte[] bytes  = getBytes(); // MyMain.class 文件的字节数组
ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(0);
ClassVisitor cv = new ClassVisitor(ASM5, cw) {
    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {System.out.println("field:" + name);
        return super.visitField(access, name, desc, signature, value);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {System.out.println("method:" + name);
        return super.visitMethod(access, name, desc, signature, exceptions);
    }
};
cr.accept(cv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);

输入后果:

field: a
field: b
method: <init>
method: test01
method: test02

值得注意的是 ClassReader 类 accept 办法的第二个参数 flags,这个参数是一个比特掩码(bit-mask),能够抉择组合的值如下:

  • SKIP_DEBUG:跳过类文件中的调试信息,比方行号信息(LineNumberTable)等
  • SKIP_CODE:跳过办法体中的 Code 属性(办法字节码、异样表等)
  • EXPAND_FRAMES:开展 StackMapTable 属性,
  • SKIP_FRAMES:跳过 StackMapTable 属性
    后面有提到 ClassVisitor 是一个抽象类,咱们能够抉择关怀的事件进行解决,比方例子中的覆写了 visitField 和 visitMethod 办法,仅对字段和办法进行解决,对于不感兴趣的事件能够抉择不覆写或者返回 null 值,这样 ASM 就晓得能够跳过对应的解析事件了。

应用 Tree Api 的形式也能够实现同样的成果

byte[] bytes = getBytes();

ClassReader cr = new ClassReader(bytes);
ClassNode cn = new ClassNode();
cr.accept(cn, ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE);

List<FieldNode> fields = cn.fields;
for (int i = 0; i < fields.size(); i++) {FieldNode fieldNode = fields.get(i);
    System.out.println("field:" + fieldNode.name);
}
List<MethodNode> methods = cn.methods;
for (int i = 0; i < methods.size(); ++i) {MethodNode method = methods.get(i);
    System.out.println("method:" + method.name);
}
ClassWriter cw = new ClassWriter(0);
cr.accept(cn, 0);
byte[] bytesModified = cw.toByteArray();

新增一个字段

在理论字节码转换中,常常会须要给类新增一个字段存储额定的信息,在 ASM 中给类新增一个字段非常简单,以上面的 MyMain 类为例,应用 javac 编译为 class 文件。

public class MyMain {
}

那么问题来了,在 ClassVisitor 的哪个办法外面进行增加字段的操作呢?由后面介绍的调用程序可知,visitField 调用机会只能在 visitInnerClass、visitField、visitMethod、visitEnd 这四种办法中抉择,又因为 visitInnerClass、visitField 不肯定都会被调用到,且它们可能被调用屡次,因而放在 visitEnd 办法中进行解决比拟失当。

应用上面的代码能够给 MyMain 新增一个 String 类型的 xyz 字段。

byte[] bytes = FileUtils.readFileToByteArray(new File("./MyMain.class"));
ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(0);
ClassVisitor cv = new ClassVisitor(ASM5, cw) {
    @Override
    public void visitEnd() {super.visitEnd();
        FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC, "xyz", "Ljava/lang/String;", null, null);
        if (fv != null) fv.visitEnd();}
};
cr.accept(cv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
byte[] bytesModified = cw.toByteArray();
FileUtils.writeByteArrayToFile(new File("./MyMain2.class"), bytesModified);

应用 javap 查看 MyMain2 的字节码,能够看到曾经多了一个类型为 String 的 xyz 变量了。

...
public java.lang.String xyz;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC
...

新增办法

在这个例子中,同样应用 MyMain 类为例,给这个类新增一个 xyz 办法。

public void xyz(int a, String b) {}

新增办法须要调用 visitMethod 办法,依据后面的调用程序来看,同 visitField 一样,visitMethod 调用机会只能在 visitInnerClass、visitField、visitMethod、visitEnd 这四种办法中抉择,这里抉择 visitEnd 办法。依据第一章的内容能够晓得 xyz 办法的签名为 (ILjava/lang/String;)V

byte[] bytes = FileUtils.readFileToByteArray(new File("./MyMain.class"));
ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(0);
ClassVisitor cv = new ClassVisitor(ASM5, cw) {
    @Override
    public void visitEnd() {super.visitEnd();
        MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "xyz", "(ILjava/lang/String;)V", null, null);
        if (mv != null) mv.visitEnd();}
};
cr.accept(cv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
byte[] bytesModified = cw.toByteArray();
FileUtils.writeByteArrayToFile(new File("./MyMain2.class"), bytesModified);

应用 javap 查看生成的 MyMain2 类,确认 xyz 办法曾经生成:

...
public void xyz(int, java.lang.String);
descriptor: (ILjava/lang/String;)V
flags: ACC_PUBLIC
...

移除办法和字段

后面介绍了利用 ASM 给 class 文件新增办法和字段,接下来介绍如何删掉办法和字段,假如有 MyMain 类代码如下,上面介绍如何删掉 abc 字段和 xyz 办法。

public class MyMain {
    private int abc = 0;
    private int def = 0;
    public void foo() {}
    public int xyz(int a, String b) {return 0;}
}

如果如果仔细观察 ClassVisitor 类的 visit 办法,会发现 visitField、visitMethod 等办法是有返回值的,如果这些办法间接返回 null,成果是这些字段、办法从类中被移除。


byte[] bytes = FileUtils.readFileToByteArray(new File("./MyMain.class"));
ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(0);
ClassVisitor cv = new ClassVisitor(ASM5, cw) {
    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {if ("abc".equals(name)) {return null;}
        return super.visitField(access, name, desc, signature, value);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {if ("xyz".equals(name)) {return null;}
        return super.visitMethod(access, name, desc, signature, exceptions);
    }
};

cr.accept(cv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
byte[] bytesModified = cw.toByteArray();
FileUtils.writeByteArrayToFile(new File("./MyMain2.class"), bytesModified);

同样应用 javap 查看 MyMain2 的字节码,能够看到 abc 字段和 xyz 办法曾经被移除,只剩下 def 字段和 foo 办法了。

小结

这篇文章咱们次要解说了 ASM 字节码操作框架,一起来回顾一下要点:

  • 第一,ASM 是一个久经考验的工业级字节码操作框架。
  • 第二,ASM 的三个外围类 ClassReader、ClassVisitor、ClassWriter。ClassReader 对象创立之后,调用 ClassReader.accept() 办法,传入一个 ClassVisitor 对象。ClassVisitor 在解析字节码的过程中遇到不同的节点时会调用不同的 visit() 办法。ClassWriter 负责把最终批改的字节码以 byte 数组的模式返回。

本文由 mdnice 多平台公布

正文完
 0