本文来自jsonchao的投稿,个人微信:bcce5360
AspectJ 有着一系列弊端:由于其基于规则,所以其切入点相对固定,对于字节码文件的操作自由度以及开发的掌控度就大打折扣。并且,他会额外生成一些包装代码,对性能以及包大小有一定影响。
ASM 基本上可以实现任何对字节码的操作,也就是自由度和开发的掌控度很高。它提供了 访问者模式来访问字节码文件,并且只注入我们想要注入的代码。
ASM 最初起源于一个博士的研究项目,在 2002 年开源,并从 5.x 版本便开始支持 Java 8。并且,ASM 是诸多 JVM 语言钦定的字节码生成库,它在效率与性能方面的优势要远超其它的字节码操作库如 javassist、AspectJ。
思维导图大纲
目录
一、ASM 的优势和逆势
-
1、ASM 的优势
2、ASM 的逆势
二、ASM 的对象模型(ASM Tree API)
-
1、优点
2、缺点
3、获取节点
4、操控操作码
三、ASM 的事件模型(ASM Core API))
-
2、类读取(解析)者 ClassVisitor
3、小结
四、综合实战训练
-
1、使用 ASM Bytecode Outline
2、使用 ASM 编译插桩统计方法耗时
3、全局替换项目中所有的 new Thread
五、总结
一、ASM 的优势和逆势
使用 ASM 操作字节码的优势与逆势都 比较明显,其分别如下所示。
1、ASM 的优势
1)、内存占用很小。
2)、运行速度非常快。
3)、操作灵活:对于字节码的操作非常地灵活,可以进行插入、删除、修改等操作。
4)、想象空间大,能够借用它提升生产力。
5)、丰富的文档与众多社区的支持。
2、ASM 的逆势
上手难度较大,需要对 Java 字节码有比较充分的了解。
对于 ASM 而言,它提供了 两种模型:对象模型和事件模型。
下面,我们就先来讲讲 ASM 的对象模型。
二、ASM 的对象模型(ASM Tree API)
对象模型的 本质 是一个 被封装过后的事件模型,它 使用了树状图的形式来描述一个类,其中包含多个节点,例如方法节点、字段节点等等,而每个节点又有子节点,例如方法节中有操作码子节点 等等。下面我们先来了解下由这种树状图模式实现的对象模型的利弊。
1、优点
1)、适宜处理简单类的修改。
2)、学习成本较低。
3)、代码量较少。
2、缺点
1)、处理大量信息会使代码变得复杂。
2)、代码难以复用。
在对象模型下的 ASM 有 两类操作纬度,分别如下所示:
1)、
获取节点
:获取指定类、字段、方法节点。2)、
操控操作码(针对方法节点)
:获取操作码位置、替换、删除、插入操作码、输出字节码。
下面我们就来分别来了解下 ASM 的这两类操作。
3、获取节点
1)、获取指定类的节点
获取一个类节点的代码如下所示:
ClassNode classNode = new ClassNode();
// 1
ClassReader classReader = new ClassReader(bytes);
// 2
classReader.accept(classNode, 0);
在注释1处,将字节数组传入一个新创建的 ClassReader,这时 ASM 会使用 ClassReader 来解析字节码。接着,在注释2处,ClassReader 在解析完字节码之后便可以通过 accept 方法来将结果写入到一个 ClassNode 对象之中。
那么,一个 ClassNode 具体又包含哪些信息呢?
如下所示:
类节点信息
类型 | 名称 | 说明 |
---|---|---|
int | version | class文件的major版本(编译的java版本) |
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)、获取指定字段的节点
获取一个字段节点的代码如下所示:
for(FieldNode fieldNode : (List)classNode.fields) {
// 1
if(fieldNode.name.equals("password")) {
// 2
fieldNode.access = Opcodes.ACC_PUBLIC;
}
}
字段节点列表 fields 是一个 ArrayList,它储存着类节点的所有字段。在注释1处,我们通过遍历 fields 集合的方式来找到目标字段节点。接着,在注释2处,我们将目标字段节点的访问权限置为 public。
除此之外,我们还可以为类添加需要的字段,代码如下所示:
FieldNode fieldNode = new FieldNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "JsonChao", "I", null, null);
classNode.fields.add(fieldNode);
在上述代码中,我们直接给目标类节点添加了一个 "public static int JsonChao" 的字段,需要注意的是,第三个参数的 "I" 表示的是 int 的类型描述符。
那么,对于一个字段节点,又包含有哪些字段信息呢?
如下所示:
字段信息
类型 | 名称 | 说明 |
---|---|---|
int | access | 访问级 |
String | name | 字段名 |
String | signature | 签名,通常是 null |
String | desc | 类型描述,例如 Ljava/lang/String、D(double)、F(float) |
Object | value | 初始值,通常为 null |
List | visibleAnnotations | 可见的注解 |
List | invisibleAnnotations | 不可见的注解 |
List | attrs | 字段的 Attribute |
接下来,我们看看如何获取一个方法节点。
3、获取指定的方法节点
获取指定的方法节点的代码如下所示:
for(MethodNode methodNode : (List)classNode.methods) {
// 1、判断方法名是否匹配目标方法
if(methodNode.name.equals("getName")) {
// 2、进行操作
}
}
methods 同 fields 一样,也是一个 ArrayList,通过遍历并判断方法名的方式即可匹配到目标方法。
对于一个方法节点来说,它包含有如下信息:
方法节点包含的信息
类型 | 名称 | 说明 |
---|---|---|
int | access | 访问级 |
String | name | 方法名 |
String | desc | 方法描述,其包含方法的返回值和参数 |
String | signature | 签名,通常是null |
List | exceptions | 可能返回的异常列表 |
List | visibleAnnotations | 可见的注解列表 |
List | invisibleAnnotations | 不可见的注解列表 |
List | attrs | 方法的Attribute列表 |
Object | annotationDefault | 默认的注解 |
List[] | visibleParameterAnnotations | |
List[] | invisibleParameterAnnotations | 不可见的参数注解列表 |
InsnList | instructions | 操作码列表 |
List | tryCatchBlocks | try-catch块列表 |
int | maxStack | 最大操作栈的深度 |
int | maxLocals | 最大局部变量区的大小 |
List | localVariables | 本地(局部)变量节点列表 |
4、操控操作码
在操控字节码之前,我们必须先了解下 instructions
,即 操作码列表,它是 方法节点中用于存储操作码的地方,其中 每一个元素都代表一行操作码。
ASM 将一行字节码封装为一个 xxxInsnNode(Insn 表示的是 Instruction 的缩写,即指令/操作码),例如 ALOAD/ARestore 指令被封装入变量操作码节点 VarInsnNode,INVOKEVIRTUAL 指令则会被封入方法操作码节点 MethodInsnNode 之中。
对于所有的指令节点 xxxInsnNode 来说,它们都继承自抽象操作码节点 AbstractInsnNode
。其所有的派生类使用详情如下所示。
所有的指令码节点说明
名称 | 说明 | 参数 |
---|---|---|
FieldInsnNode | 用于 GETFIELD 和 PUTFIELD 之类的字段操作的字节码 | String owner 字段所在的类 String name 字段的名称 String desc 字段的类型 |
FrameNode | 栈映射帧的对应的帧节点 | 待补充 |
IincInsnNode | 用于 IINC 变量自加操作的字节码 | int var:目标局部变量的位置 int incr:要增加的数 |
InsnNode | 一切无参数值操作的字节码,例如 ALOAD_0,DUP(注意不包含 POP) | 无 |
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:对应的第一个 Label |
LookupSwitchInsnNode | 用于实现 LOOKUPSWITCH 操作的字节码 | LabelNode dflt:default 块对应的 Lable List 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 块对应的 Lable List labels:对应的 Label 节点列表 |
TypeInsnNode | 用于实现 NEW、ANEWARRAY 和 CHECKCAST 等类型相关操作的字节码 | String desc:类型 |
VarInsnNode | 用于实现 ALOAD、ASTORE 等局部变量操作的字节码 | int var:局部变量 |