一起来学字节码插桩:ASM Tree API
文章目录
- 一 ASM介绍
- 二 Tree API
- 2.1、ClassNode
- 2.1.1、accept(ClassVisitor classVisitor)
- 2.1.2、FieldNode
- 2.1.3、MethodNode
- MethodNode.InsnList
- 三 Tree API 实践
- 3.1、统计方法耗时
- 四 参考
一 ASM介绍
ASM是一个通用的Java字节码操作和分析框架。它可用于修改现有类或直接以二进制形式动态生成类。ASM提供了一些常见的字节码转换和分析算法,可以根据这些算法构建定制的复杂转换和代码分析工具。
ASM提供了与其他Java字节码框架类似的功能,但更关注性能。因为它的设计和实现尽可能的小和快,它非常适合在动态系统中使用(但当然也可以以静态的方式使用,例如在编译器中)。关于ASM更多介绍,可以参见 ASM官网。
ASM 从组成结构上可以分成两部分,一部分为Core API,另一部分为Tree API。
Core API包括asm.jar、asm-util.jar和asm-commons.jarTree API包括asm-tree.jar和asm-analysis.jar
本文主要讲解Tree API 的相关使用。
二 Tree API
我们知道Class字节码 是由无符号数和表两种数据结构组成。而 ASM Tree API 可以认为是对上面两种结构进行了进一步封装以简化使用。
使用时首先需要引入 tree api,这里以9.2为例,最新版本请查看官网 :
implementation 'org.ow2.asm:asm-commons:9.2' //Tree API
通过 ./gradlew app:dependencies 查看依赖关系:
+--- org.ow2.asm:asm-commons:9.2
| +--- org.ow2.asm:asm:9.2
| +--- org.ow2.asm:asm-tree:9.2
| | \--- org.ow2.asm:asm:9.2
| \--- org.ow2.asm:asm-analysis:9.2
| \--- org.ow2.asm:asm-tree:9.2 (*)
其中 org.ow2.asm:asm-tree:9.2 的内部类有:

主要用到的类有:
- ClassNode
- FieldNode
- MethodNode
- InsnList
- AbstractInsnNode
- TypeInsnNode
- VarInsnNode
- FieldInsnNode
- MethodInsnNode
- IincInsnNode
- InsnNode
- IntInsnNode
- InvokeDynamicInsnNode
- JumpInsnNode
- LabelNode
- LdcInsnNode
- LookupSwitchInsnNode
- MultiANewArrayInsnNode
- TableSwitchInsnNode
- TryCatchBlockNode
- InsnList
用类图表示为:

整理一下他们之间的关系:
ClassNode包含FieldNode和MethodNode;MethodNode中包含有序指令集合InsnList及其异常处理TryCatchBlockNode;InsnList由很多个有序的单指令AbstractInsnNode组合而成。AbstractInsnNode是一个抽象类,表示字节码指令的节点。一条指令最多只能在一个InsnList中出现一次。AbstractInsnNode的子类实现如下:

下面分别来看下每个类的含义。
2.1、ClassNode
ClassNode的注释为A node that represents a class,表示一个类的节点。可以通俗理解为:一个Class类经过Tree API解析后,可以将其转换成一个ClassNode(内部包含类的所有信息)。
public class ClassNode extends ClassVisitor {public int version;public int access;public String name;public String signature;public String superName;public List interfaces;public String outerClass;public String outerMethod;public List attrs;public List innerClasses;public List fields;public List methods;
}
ClassNode继承自ClassVisitor,可以看到ClassNode内部的字段正好对应Class中的所有信息。
| 类型 | 名称 | 说明 |
|---|---|---|
| int | version | jdk版本 |
| int | access | 访问级 |
| String | name | 类名,采用全地址,如java/lang/String |
| String | signature | 签名,通常是null |
| String | superName | 父类类名,采用全地址 |
| List | interfaces | 实现的接口,采用全地址 |
| String | sourceFile | 源文件,可能为null |
| String | sourceDebug | debug源,可能为null |
| String | outerClass | 外部类 |
| String | outerMethod | 外部方法 |
| String | outerMethodDesc | 外部方法描述(包括方法参数、返回值) |
| List | visibleAnnotations | 可见的注解 |
| List | invisibleAnnotations | 不可见的注解 |
| List | attrs | 类的Attribute |
| List | innerClasses | 内部类列表 |
| List | fields | 字段列表 |
| List | methods | 方法列表 |
2.1.1、accept(ClassVisitor classVisitor)
ClassNode#accept(classVisitor) 传入一个ClassVisitor,内部实现如下:
// Makes the given class visitor visit this class.public void accept(final ClassVisitor classVisitor) {// Visit the header.String[] interfacesArray = new String[this.interfaces.size()];//......// Visit the fields.for (int i = 0, n = fields.size(); i < n; ++i) {fields.get(i).accept(classVisitor);}// Visit the methods.for (int i = 0, n = methods.size(); i < n; ++i) {methods.get(i).accept(classVisitor);}classVisitor.visitEnd();}
看下这个方法的注释:Makes the given class visitor visit this class。accept(classVisitor) 可以让传入的classVisitor访问ClassNode中的所有内容。而ClassWriter继承自ClassVisitor,那么就可以将ClassWriter作为入参传入ClassNode中,进而调用classNode.accept(classWriter) 将ClassNode中的数据传入ClassWriter中,最终调用classWriter.toByteArray()将ClassNode转为byte[]。
示例:
//自定义ClassNode类
class AOutLibClassNode(val api: Int, private val classWriter: ClassWriter) : ClassNode(api) {override fun visitEnd() {//允许classWriter访问ClassNode类中的信息accept(classWriter)}
}ClassReader classReader = new ClassReader(xxx.class.getName());
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
//1、将ClassWriter传入ClassNode中
ClassNode classNode = new AOutLibClassNode(BConstant.ASM9, classWriter);
classReader.accept(classNode, ClassReader.EXPAND_FRAMES);//2、ClassNode -> ClassWriter -> bytes[]
bytes[] classNodeToBytes = classWriter.toByteArray();
2.1.2、FieldNode
public class FieldNode extends FieldVisitor {public int access;public String name;public String desc;public String signature;public Object value; //字段的初始化值public List visibleAnnotations;public List invisibleAnnotations;public List visibleTypeAnnotations;public List invisibleTypeAnnotations;public List attrs;//构造方法public FieldNode( final int api, final int access, final String name,final String descriptor, final String signature, final Object value) {super(api);this.access = access;this.name = name;this.desc = descriptor;this.signature = signature;this.value = value;}
}
FieldNode继承自FieldVisitor,可以用于遍历或生成字段,内部变量说明:
| 类型 | 名称 | 说明 |
|---|---|---|
| int | access | 访问级 |
| String | name | 字段名 |
| String | signature | 签名,通常是 null |
| String | desc | 类型描述,例如 Ljava/lang/String、D(double)、F(float)Objectvalue初始值,通常为 null |
| List | visibleAnnotations | 可见的注解 |
| List | invisibleAnnotations | 不可见的注解 |
| List | attrs | 字段的 Attribute |
FieldNode生成代码示例:
ClassNode node = new ClassNode(Opcodes.ASM9);
//public + final + static
int access = Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL| Opcodes.ACC_STATIC;
//long类型,value设置为1
node.fields.add(new FieldNode(access, "timer", "J",null, 1)); //ClassWriter转化为byte[]
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
node.accept(writer);
byte[] byteArr = writer.toByteArray();
执行结果:
public static final long timer = 1;
可以看到生成了新的字段。
2.1.3、MethodNode
public class MethodNode extends MethodVisitor {//1、标识方法的基本属性:方法头public int access;public String name; //方法名public String desc;public String signature;public List exceptions;//2、标识方法的指令集合:方法体public InsnList instructions; //方法中的有序指令集public List tryCatchBlocks;//方法中的try catch异常处理数组public List parameters;public int maxStack; // 栈最大值public int maxLocals; //局部变量的最大值
}
MethodNode用于访问或修改方法。相比于FieldNode的单一,MethodNode内部可能涉及多条指令,方法执行时,也会涉及到局部变量表及操作数栈,具体可参见:Java内存模型。MethodNode 内部变量说明:
| 类型 | 名称 | 说明 |
|---|---|---|
| int | access | 方法访问级 |
| String | name | 方法名 |
| String | desc | 方法描述,其包含方法的返回值、参数 |
| String | signature | 泛型签名,通常是null |
| List | exceptions | 可能返回的异常列表 |
| List | visibleAnnotations | 可见的注解列表 |
| List | invisibleAnnotations | 不可见的注解列表 |
| List | attrs | 方法的Attribute列表 |
| Object | annotation | Default默认的注解 |
| List | visibleParameterAnnotations | 可见的参数注解 |
| List | invisibleParameterAnnotations | 不可见的参数注解列表 |
| InsnList | instructions | Opcode操作码列表 |
| List | tryCatchBlock | try-catch块列表 |
| int | maxStack | 最大操作数栈的深度 |
| int | maxLocals | 最大局部变量表的大小 |
| List | localVariables | 本地(局部)变量节点列表 |
MethodNode 继承自 MethodVisitor,所以内部会有各种 visitxxx() 方法,而在调用MethodNode#visitxxx() 方法后,会将指令存储到instructions (InsnList)中。
MethodNode.InsnList
// InsnList本质上是一个双链表
public class InsnList implements Iterable {private int size; //链表内数据的sizeprivate AbstractInsnNode firstInsn; //链表头指针private AbstractInsnNode lastInsn; //链表尾指针
}
InsnList类实现了Iterable< AbstractInsnNode>接口,内部存储了方法执行时的指令集。其中 AbstractInsnNode 表示单条指令,而InsnList类是一个有序存储AbstractInsnNode指令的双向链表。
当我们想插入操作码指令从而达到修改字节码时,可以对InsnList进行如下操作:
- add(final AbstractInsnNode insnNode):将给定的
insnNode指令添加到InsnList列表的末尾; - add(final InsnList insnList):将一组指令添加到
InsnList列表的末尾; - insert(final AbstractInsnNode insnNode):将给定的
insnNode指令添加到InsnList列表的开头; - insert(final InsnList insnList):将一组指令添加到
InsnList列表的开头; - insert(final AbstractInsnNode previousInsn, final AbstractInsnNode insnNode):将
insnNode指令插入到previousInsn指令之后; - insert(final AbstractInsnNode previousInsn, final InsnList insnList):将
insnList多个指令插入到previousInsn指令之后; - insertBefore(final AbstractInsnNode nextInsn, final AbstractInsnNode insnNode):将
insnNode指令插入到nextInsn之前; - insertBefore(final AbstractInsnNode nextInsn, final InsnList insnList): 将
insnList多个指令插入到nextInsn之前。
继续看AbstractInsnNode:
public abstract class AbstractInsnNode {protected int opcode; //当前指令AbstractInsnNode previousInsn; //指向上一个指令AbstractInsnNode nextInsn; //指向下一个指令int index; //当前指令在InsnList中的索引
}
AbstractInsnNode可以表示的指令集合:
| 类型 | 名称 | 说明 |
|---|---|---|
| FieldInsnNode | GETFIELD、PUTFIELD 等变量操作的字节码 | 操作变量 |
| FrameNode | 栈映射帧 | |
| IincInsnNode | 用于 IINC 变量自加操作 | int var:目标局部变量的位置 int incr: 要增加的数 |
| InsnNode | 无参数值操作的字节码,如 ALOAD_0 | |
| IntInsnNode | 用于 BIPUSH、SIPUSH 和 NEWARRAY 这三个直接操作整数的操作 | int operand:操作的整数值 |
| InvokeDynamicInsnNode | 用于 Java7 新增的 INVOKEDYNAMIC 操作的字节码 | String name:方法名称; String desc:方法描述; Handle bsm:句柄; Object bsmArgs:参数常量 |
| JumpInsnNode | 用于 IFEQ 或 GOTO 等跳转操作 | LabelNode lable:目标lable |
| LabelNode | 用于表示跳转点的 Label 节点 | |
| LdcInsnNode | LDC 用于加载常量池中引用值并进行插入 | Object cst:引用值 |
| LineNumberNode | 表示行号的节点 | int line:行号;LabelNode start:对应的第一个 |
| LabelLookupSwitchInsnNode | 用于实现 LOOKUPSWITCH 操作的字节码 | LabelNode dflt:default 块对应的 LableList keys 键列表;List labels:对应的 Label 节点列表 |
| MethodInsnNode | 用于 INVOKEVIRTUAL 等传统方法调用操作的字节码 | 不适用于 Java7 新增的 INVOKEDYNAMIC,String owner :方法所在的类;String name :方法名称;String desc:方法描述 |
| MultiANewArrayInsnNode | 用于 MULTIANEWARRAY 操作的字节码 | String desc:类型描述;int dims:维数 |
| TableSwitchInsnNode | 用于实现 TABLESWITCH 操作的字节码 | int min:键的最小值;int max:键的最大值;LabelNode dflt:default 块对应的 LableList labels:对应的 Label 节点列表 |
| TypeInsnNode | 用于 NEW、ANEWARRAY 和 CHECKCAST 等类型操作 | String desc:类型;VarInsnNode用于实现 ALOAD、ASTORE 等局部变量操作;int var:局部变量 |
上述的指令按特定顺序存储在InsnList中。当方法执行时,InsnList中的指令集合也会按顺序执行。
三 Tree API 实践
3.1、统计方法耗时
假设要对下面这个类的方法进行耗时统计:
//MethodTimeCostTestJava.java
public class MethodTimeCostTestJava {//static静态方法public static void staticTimeCostMonitor() throws InterruptedException {Thread.sleep(1000);}//非静态方法public void timeCostMonitor() throws InterruptedException {Thread.sleep(1000);}
}
实现:
自定义ClassNode对方法进行插桩操作:
class AOutLibClassNode(val api: Int, private val classWriter: ClassWriter) : ClassNode(api) {private val mThresholdTime = 500companion object {const val owner = "org/ninetripods/lib_bytecode/common/TimeCostUtil"const val descripter = "Lorg/ninetripods/lib_bytecode/common/TimeCostUtil"}override fun visitEnd() {processTimeCost()//允许classWriter访问ClassNode类中的信息accept(classWriter)}private fun processTimeCost(clzName: String? = "", methodName: String? = "", access: Int = 0) {for (methodNode: MethodNode in methods) {if (methodNode.name.equals("") || methodNode.name.equals("")) continueval instructions = methodNode.instructions//方法开头插入val clzName = nameval methodName = methodNode.nameval access = methodNode.accessinstructions.insert(createMethodStartInstructions(clzName, methodName, access))//退出方法之前插入methodNode.instructions.forEach { insnNode ->val opcode = insnNode.opcodeif ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {val endInstructions = createMethodEndInstructions(clzName, methodName, access)methodNode.instructions.insertBefore(insnNode, endInstructions)}}}}/*** 在method中创建入口指令集*/private fun createMethodStartInstructions(clzName: String?,methodName: String?,access: Int,): InsnList {val isStaticMethod = access and Opcodes.ACC_STATIC != 0return InsnList().apply {if (isStaticMethod) {add(FieldInsnNode(Opcodes.GETSTATIC, owner, "INSTANCE", descripter))//操作数栈中传入下面两个参数add(IntInsnNode(Opcodes.SIPUSH, mThresholdTime))add(LdcInsnNode("$clzName&$methodName"))add(MethodInsnNode(Opcodes.INVOKEVIRTUAL,owner,"recordStaticMethodStart","(ILjava/lang/String;)V",false))} else {add(FieldInsnNode(Opcodes.GETSTATIC, owner, "INSTANCE", descripter))//操作数栈中传入对应的三个入参add(IntInsnNode(Opcodes.SIPUSH, mThresholdTime))add(LdcInsnNode("$clzName&$methodName"))add(VarInsnNode(Opcodes.ALOAD, 0))//将上面的三个参数传入下面的方法中add(MethodInsnNode(Opcodes.INVOKEVIRTUAL,owner,"recordMethodStart","(ILjava/lang/String;Ljava/lang/Object;)V",false))}}}/*** 在method中退出时的指令集*/private fun createMethodEndInstructions(clzName: String?,methodName: String?,access: Int,): InsnList {val isStaticMethod = access and Opcodes.ACC_STATIC != 0return InsnList().apply {if (isStaticMethod) {add(FieldInsnNode(Opcodes.GETSTATIC, owner, "INSTANCE", descripter))//调用add(IntInsnNode(Opcodes.SIPUSH, mThresholdTime))add(LdcInsnNode("$clzName&$methodName"))add(MethodInsnNode(Opcodes.INVOKEVIRTUAL,owner,"recordStaticMethodEnd","(ILjava/lang/String;)V",false))} else {add(FieldInsnNode(Opcodes.GETSTATIC, owner, "INSTANCE", descripter))//栈中传入对应的三个入参add(IntInsnNode(Opcodes.SIPUSH, mThresholdTime))add(LdcInsnNode("$clzName&$methodName"))add(VarInsnNode(Opcodes.ALOAD, 0))//将上面的三个参数传入下面的方法中add(MethodInsnNode(Opcodes.INVOKEVIRTUAL,owner,"recordMethodEnd","(ILjava/lang/String;Ljava/lang/Object;)V",false))}}}}
方法耗时处理TimeCostUtil类:
package org.ninetripods.lib_bytecode.common/*** 全局方法耗时Util*/
object TimeCostUtil {private const val TAG = "METHOD_COST"private val staticMethodObj by lazy { StaticMethodObject() }/*** 方法Map,其中key:方法名,value:耗时时间*/private val METHODS_MAP by lazy { ConcurrentHashMap() }/*** 对象方法* @param thresholdTime 阈值* @param methodName 方法名* @param clz 类名*/fun recordMethodStart(thresholdTime: Int, methodName: String, clz: Any?) {try {METHODS_MAP[methodName] = System.currentTimeMillis()} catch (ex: Exception) {ex.printStackTrace()}}/*** 静态方法* @param thresholdTime 阈值* @param methodName 方法名*/fun recordStaticMethodStart(thresholdTime: Int, methodName: String){recordMethodStart(thresholdTime, methodName, staticMethodObj)}/*** 对象方法* @param thresholdTime 阈值时间* @param methodName 方法名* @param clz 类名*/fun recordMethodEnd(thresholdTime: Int, methodName: String, clz: Any?) {Log.e(TAG,"\t methodName=>$methodName thresholdTime=>$thresholdTime method=>recordMethodEnd")synchronized(TimeCostUtil::class.java) {try {if (METHODS_MAP.containsKey(methodName)) {val startTime: Long = METHODS_MAP[methodName] ?: 0Lval costTime = System.currentTimeMillis() - startTimeMETHODS_MAP.remove(methodName)//方法耗时超过了阈值if (costTime >= thresholdTime) {val threadName = Thread.currentThread().nameLog.e(TAG,"\t methodName=>$methodName threadNam=>$threadName thresholdTime=>$thresholdTime costTime=>$costTime")}}} catch (ex: Exception) {ex.printStackTrace()}}}/*** 静态方法* @param thresholdTime 阈值* @param methodName 方法名*/fun recordStaticMethodEnd(thresholdTime: Int, methodName: String) {recordMethodEnd(thresholdTime, methodName, staticMethodObj)}}class StaticMethodObject
然后是执行总入口:
public static void main(String[] args) {try {//使用示例ClassReader classReader = new ClassReader(MethodTimeCostTestJava.class.getName());//ClassWriter.COMPUTE_MAXS 自动计算帧栈信息(操作数栈 & 局部变量表)ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);//-------------------------方式1:ASM Tree API -------------------------ClassNode classNode = new AOutLibClassNode(Opcodes.ASM9, classWriter);//访问者模式:将ClassVisitor传入ClassReader中,从而可以访问ClassReader中的私有信息;类似一个接口回调。classReader.accept(classNode, ClassReader.EXPAND_FRAMES);FileUtil.INSTANCE.byte2File("lib_bytecode/files/MethodTimeCostTestJava.class",classWriter.toByteArray());} catch (IOException e) {e.printStackTrace();}}//将最终修改的字节码写入指定路径
object FileUtil {fun byte2File(outputPath: String, sourceByte: ByteArray) {try {val file = File(outputPath)if (file.exists()) {file.delete()} else {file.parentFile.mkdir()file.createNewFile()}val inputStream = ByteArrayInputStream(sourceByte)val outputStream = FileOutputStream(file)val buffer = ByteArray(1024)var len = 0while (inputStream.read(buffer).apply { len = this } != -1) {outputStream.write(buffer, 0, len)}outputStream.flush()outputStream.close()inputStream.close()} catch (ex: Exception) {ex.printStackTrace()}}
}
执行结果:
public class MethodTimeCostTestJava {public MethodTimeCostTestJava() {}public static void staticTimeCostMonitor() throws InterruptedException {//1TimeCostUtil.INSTANCE.recordStaticMethodStart(500, "org/ninetripods/lib_bytecode/asm/demo/MethodTimeCostTestJava&staticTimeCostMonitor");Thread.sleep(1000L);//2TimeCostUtil.INSTANCE.recordStaticMethodEnd(500, "org/ninetripods/lib_bytecode/asm/demo/MethodTimeCostTestJava&staticTimeCostMonitor");}public void timeCostMonitor() throws InterruptedException {//3TimeCostUtil.INSTANCE.recordMethodStart(500, "org/ninetripods/lib_bytecode/asm/demo/MethodTimeCostTestJava&timeCostMonitor", this);Thread.sleep(1000L);//4TimeCostUtil.INSTANCE.recordMethodEnd(500, "org/ninetripods/lib_bytecode/asm/demo/MethodTimeCostTestJava&timeCostMonitor", this);}
}
可以看到最终生成类的方法中已经包含了我们想要插入的代码,包括静态方法和非静态方法。这里只是简单的写个示例,其中AOutLibClassNode中的写法是参考的 滴滴DoKit 中的代码。
四 参考
【1】Tree API介绍
【2】ASM 修改字节码
【3】Java ASM详解:MethodVisitor和Opcode
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
