java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java 字节码操纵框架ASM

Java字节码操纵框架ASM图文实例详解

作者:悦

这篇文章主要为大家介绍了Java字节码操纵框架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 等,除此之外还有下面这些优点:

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),可以选择组合的值如下:

使用 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 字节码操作框架,一起来回顾一下要点:

以上就是Java字节码操纵框架ASM图文实例详解的详细内容,更多关于Java 字节码操纵框架ASM的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文