0%

ASM字节码插桩

ASM是干什么的?

ASM 是一个 Java 字节码操控框架。

ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。

Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。

ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

ASM的应用场景有AOP(CGLIB就是基于ASM)、热部署、修改其他jar包中的类等。

用ASM修改类的好处?

  • 类的修改是硬编码在新生成的类文件内部的,没有反射带来性能上的付出。
  • 越过Java常规语法限制,做出代码编写无法实现的事情。

字节码操纵操作工具很多,ASM有什么优势?

常见的字节码操作工具有:

  • ASM
  • JavaAssist
  • AspectJ(基于BCEL)
  • CGLIB(基于ASM)
  • ByteBuddy(基于ASM)

ASM优势:

  • 体积小
  • 性能高

ASM劣势:

  • 需要熟悉字节码原理,API易用性低

JavaAssist优势:

  • 不需要熟悉字节码原理,API易用性高

JavaAssist劣势:

  • 体积大
  • 性能差

所以ASM适用于对性能和体积敏感的场景。

参考:

Visitor模式怎么理解?

把可变的和不变的分离。

具体而言,被访问者是不变的,而访问者是可变的。举个例子来说,我是不变的,而不同的人看我会有不同的眼光,这个看我的眼光是可变的。

访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。

访问者模式适用于数据结构相对稳定,算法又易变化的系统。
因为访问者模式使得算法操作增加变得容易。

若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。

访问者模式的优点:

增加操作很容易,因为增加操作意味着增加新的访问者。访问者模式将有关行为集中到一个访问者对象中,其改变不影响系统数据结构。

访问者模式的缺点:

增加新的数据结构很困难。

简单来说,访问者模式就是一种分离对象数据结构与行为的方法,通过这种分离,可达到为一个被访问者动态添加新的操作而无需做其它的修改的效果。

参考:

ASM为什么用Visitor模式?

.class 文件的结构是固定的,主要有常量池、字段表、方法表、属性表等内容,通过使用访问者模式在扫描 .class 文件中各个表的内容时,就可以修改这些内容了。

ASM的思想是什么?

ClassReader 的 accept 方法中传进来了一个参数ClassVisitor。在内部,ClassVisitor会不断的读取ClassReader的二进制byte[],然后在解析后通过参数classVisitor的抽象visitXXX方法将属性全部转发出去。

参考:

ASM API

分为核心API和树形API

核心API

ASM Core API可以类比解析XML文件中的SAX方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用Core API。在Core API中有以下几个关键类:

  • ClassReader:用于读取已经编译好的.class文件。
  • ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。
  • 各种Visitor类:如上所述,CoreAPI根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的Visitor,比如用于访问方法的MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的AnnotationVisitor等。为了实现AOP,重点要使用的是MethodVisitor。

树形API

ASM Tree API可以类比解析XML文件中的DOM方式,把整个类的结构读取到内存中,缺点是消耗内存多,但是编程比较简单。TreeApi不同于CoreAPI,TreeAPI通过各种Node类来映射字节码的各个区域,类比DOM节点,就可以很好地理解这种编程方式。

Intellij Idea 中 ASM Bytecode Outline 插件

可以把java代码转为ASM框架的代码。

参考:

ASM如何使用?

ClassWriter是ClassVistor的实现类。

处理逻辑都写自定义ClassVisitor里。

模板的拦截代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
ClassReader classReader = new ClassReader("meituan/bytecode/asm/Base");
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);

//处理
ClassVisitor classVisitor = new MyClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
byte[] data = classWriter.toByteArray();

//输出
File f = new File("/classes/meituan/bytecode/asm/Base.class");
FileOutputStream fout = new FileOutputStream(f);
fout.write(data);
fout.close();

代码示例:在一个方法前后分别插入方法

例如有一个类

1
2
3
4
5
6
7
public class TestBean {
public void halloAop() {
AopInterceptor.beforeInvoke();
System.out.println("Hello Aop");
AopInterceptor.afterInvoke();
}
}

要插入AopInterceptor的beforeInvoke()和afterInvoke()在TestBean的halloAop()的执行前后。

1
2
3
4
5
6
7
8
9
public class AopInterceptor {
public static void beforeInvoke() {
System.out.println("before");
};

public static void afterInvoke() {
System.out.println("after");
};
}

在自定义ClassVisitor的visitMethod方法中拦截halloAop方法,对不是halloAop的方法返回null表示不处理,对halloAop的拦截处理交给AopMethod类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class AopClassAdapter extends ClassVisitor {
public AopClassAdapter(int api, ClassVisitor cv) {
super(api, cv);
}

public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
if ("<init>".equals(name))
return null;//放弃原有类中所有构造方法
if (!name.equals("halloAop"))
return null;// 只对halloAop方法执行代理
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
return new AopMethod(this.api, mv);
}
}

MethodVisitor中

visitCode方法,它会在ASM开始访问某一个方法的Code区时被调用,重写visitCode方法,将AOP中的前置逻辑就放在这里。

每当ASM访问到无参数指令时,都会调用MyMethodVisitor中的visitInsn方法。我们判断了当前指令是否为无参数的“return”指令,如果是就在它的前面添加一些指令,也就是将AOP的后置逻辑放在该方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class AopMethod extends MethodVisitor implements Opcodes {
public AopMethod(int api, MethodVisitor mv) {
super(api, mv);
}

public void visitCode() {
super.visitCode();
this.visitMethodInsn(INVOKESTATIC,"org/more/test/asm/AopInterceptor", "beforeInvoke", "()V");
}

public void visitInsn(int opcode) {
if (opcode == RETURN) {//在返回之前安插after 代码。
mv.visitMethodInsn(INVOKESTATIC, "org/more/test/asm/AopInterceptor", "afterInvoke", "()V");
}
super.visitInsn(opcode);
}
}

参考:

invokevirtual指令执行方法后,方法的返回值存放在哪?

存放在栈帧的操作栈中。

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。

举个例子,例如整数加法的字节码指令iadd,这条指令在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会把这两个int值出栈并相加,然后将相加的结果重新入栈。

参考

局部变量表的执行过程?

参考:

  • 虚拟机字节码执行引擎
  • 深入理解Java虚拟机(第2版)第8章 虚拟机字节码执行引擎
    • 8.2.1 局部变量表
    • 8.4.3 基于栈的解释器的执行过程

MethodNode有什么作用?

可以获取方法体内部的字节码指令等方法的一切信息

MethodNode有什么使用场景?

微信Android客户端卡顿检测工具:Matrix-Android-TraceCanary

判断一个方法是空方法?

遍历字节码指令,没有有效的字节码指令就是空

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
private boolean isEmptyMethod() {  
ListIterator<AbstractInsnNode> iterator = instructions.iterator();
while (iterator.hasNext()) {
AbstractInsnNode insnNode = iterator.next();
int opcode = insnNode.getOpcode();
if (-1 == opcode) {
continue;
} else {
return false;
}
}
return true;
}

判断扫描的函数是否只含有 PUT/READ FIELD 等简单的指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39


private boolean isGetSetMethod() {
int ignoreCount = 0;
ListIterator<AbstractInsnNode> iterator = instructions.iterator();
while (iterator.hasNext()) {
AbstractInsnNode insnNode = iterator.next();
int opcode = insnNode.getOpcode();
if (-1 == opcode) {
continue;
}

if (opcode != Opcodes.GETFIELD
&& opcode != Opcodes.GETSTATIC
&& opcode != Opcodes.H_GETFIELD
&& opcode != Opcodes.H_GETSTATIC
&& opcode != Opcodes.RETURN
&& opcode != Opcodes.ARETURN
&& opcode != Opcodes.DRETURN
&& opcode != Opcodes.FRETURN
&& opcode != Opcodes.LRETURN
&& opcode != Opcodes.IRETURN
&& opcode != Opcodes.PUTFIELD
&& opcode != Opcodes.PUTSTATIC
&& opcode != Opcodes.H_PUTFIELD
&& opcode != Opcodes.H_PUTSTATIC
&& opcode > Opcodes.SALOAD) {
if (isConstructor && opcode == Opcodes.INVOKESPECIAL) {
ignoreCount++;
if (ignoreCount > 1) {
return false;
}
continue;
}
return false;
}
}
return true;
}

判断一个方法是不是仅调用另外一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
private boolean isSingleMethod() {
ListIterator<AbstractInsnNode> iterator = instructions.iterator();
while (iterator.hasNext()) {
AbstractInsnNode insnNode = iterator.next();
int opcode = insnNode.getOpcode();
if (-1 == opcode) {
continue;
} else if (Opcodes.INVOKEVIRTUAL <= opcode && opcode <= Opcodes.INVOKEDYNAMIC) {
return false;
}
}
return true;
}

参考资料