一 ASM介绍
ASM
是一个通用的Java字节码操作和分析框架
。它可用于修改现有类
或直接以二进制形式动态生成类
。ASM
提供了一些常见的字节码转换和分析算法,可以根据这些算法构建定制的复杂转换和代码分析工具。
ASM
提供了与其他Java字节码框架
类似的功能,但更关注性能
。因为它的设计和实现尽可能的小和快
,它非常适合在动态系统中使用(但当然也可以以静态的方式使用,例如在编译器中)。关于ASM
更多介绍,可以参见 ASM官网。
ASM
从组成结构上可以分成两部分,一部分为Core API
,另一部分为Tree API
。
Core API
包括asm.jar
、asm-util.jar
和asm-commons.jar
Tree 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
用类图表示为:
整理一下他们之间的关系:
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<String> interfaces;
public String outerClass;
public String outerMethod;
public List<Attribute> attrs;
public List<InnerClassNode> innerClasses;
public List<FieldNode> fields;
public List<MethodNode> 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<AnnotationNode> visibleAnnotations;
public List<AnnotationNode> invisibleAnnotations;
public List<TypeAnnotationNode> visibleTypeAnnotations;
public List<TypeAnnotationNode> invisibleTypeAnnotations;
public List<Attribute> 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<String> exceptions;
//2、标识方法的指令集合:方法体
public InsnList instructions; //方法中的有序指令集
public List<TryCatchBlockNode> tryCatchBlocks;//方法中的try catch异常处理数组
public List<ParameterNode> 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<AbstractInsnNode> {
private int size; //链表内数据的size
private 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 = 500
companion 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("<init>") || methodNode.name.equals("<clinit>")) continue
val instructions = methodNode.instructions
//方法开头插入
val clzName = name
val methodName = methodNode.name
val access = methodNode.access
instructions.insert(createMethodStartInstructions(clzName, methodName, access))
//退出方法之前插入
methodNode.instructions.forEach { insnNode ->
val opcode = insnNode.opcode
if ((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 != 0
return 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 != 0
return 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<String, Long>() }
/**
* 对象方法
* @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] ?: 0L
val costTime = System.currentTimeMillis() - startTime
METHODS_MAP.remove(methodName)
//方法耗时超过了阈值
if (costTime >= thresholdTime) {
val threadName = Thread.currentThread().name
Log.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 = 0
while (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 {
//1
TimeCostUtil.INSTANCE.recordStaticMethodStart(500, "org/ninetripods/lib_bytecode/asm/demo/MethodTimeCostTestJava&staticTimeCostMonitor");
Thread.sleep(1000L);
//2
TimeCostUtil.INSTANCE.recordStaticMethodEnd(500, "org/ninetripods/lib_bytecode/asm/demo/MethodTimeCostTestJava&staticTimeCostMonitor");
}
public void timeCostMonitor() throws InterruptedException {
//3
TimeCostUtil.INSTANCE.recordMethodStart(500, "org/ninetripods/lib_bytecode/asm/demo/MethodTimeCostTestJava&timeCostMonitor", this);
Thread.sleep(1000L);
//4
TimeCostUtil.INSTANCE.recordMethodEnd(500, "org/ninetripods/lib_bytecode/asm/demo/MethodTimeCostTestJava&timeCostMonitor", this);
}
}
可以看到最终生成类的方法中已经包含了我们想要插入的代码,包括静态方法和非静态方法。这里只是简单的写个示例,其中AOutLibClassNode
中的写法是参考的 滴滴DoKit 中的代码。
四 参考
【1】Tree API介绍\
【2】ASM 修改字节码\
【3】Java ASM详解:MethodVisitor和Opcode