一、前言
本篇我们将介绍如何用ASM工具对代码进行插入和修改。先说下我们这章节的任务,如下:
对已有的Class文件的方法里插入方法执行时间的统计
二、加载Class文件
想要修改一个Class文件的内容,那肯定要先把Class文件加载进内存,不然你修改个&…(%¥。正好,在ASM工具里,有一个ClassReader类就可以帮助我们读取Class文件。接下来让我们看看如何使用
2.1 准备一个Class文件
准备一个Class文件,你可以先写一个Java文件,通过编译器编译或者自己用javac命令都可以。
2.2 读取Class文件
直接上代码(具体路径看自己Class文件所在)
public class ASMLoadDemo {
public static void main(String[] args) throws Exception {
insertCodeByLoadClass("D:\\20210426\\code\\JavaProjec\\JMMDemo\\out\\production\\JMMDemo\\asm\\InjectTest.class");
}
/**
* 加载Class插入代码
*
* @param path
*/
public static void insertCodeByLoadClass(String path) {
try {
//1.获取Class文件
FileInputStream fis = new FileInputStream(path);
//2.读取Class文件
ClassReader classReader = new ClassReader(fis);
//3.准备
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//4.重点!! 开始插桩 对代码进行扫描,进行代码插入。
classReader.accept(new MyClassVisitor(Opcodes.ASM9, classWriter), ClassReader.EXPAND_FRAMES);
//5.插桩结束 输出字节码
byte[] bytes = classWriter.toByteArray();
//6.保存文件
FileOutputStream fos = new FileOutputStream("src/asm/InjectTest2.class");
fos.write(bytes);
fos.close();
fis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
MyClassVisitor代码
/**
* 用来观察(扫描)类信息,包括变量、方法
*/
static class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(int api) {
super(api);
}
public MyClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
/**
* 当有一个方法,就执行这个回调一次,类中有多个方法,这里就会执行多次
*/
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println("读到一个方法visitMethod: " + name);
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MyMethodVisitor(api, methodVisitor, access, name, descriptor);
}
}
MyMethodVisitor代码
/**
* 用来观察(扫描)方法里的代码
*/
static class MyMethodVisitor extends AdviceAdapter {
protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
int startTimeIndex;//startTime在局部变量表的位置
int endTimeIndex;//endTime在局部变量表的位置
/**
* 方法进入的时候调用
*/
@Override
protected void onMethodEnter() {
super.onMethodEnter();
//调用System.currentTimeMillis方法,调用完后将返回值推到栈顶
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
//获取当前局部变量表可插入的位置,不能乱传,不然会影响下一个值的索引获取,这里你传DOUBLE_TYPE或者LONG_TYPE,都是一样的
startTimeIndex = newLocal(Type.LONG_TYPE);
//把栈顶的值(也就是System.currentTimeMillis的返回值)保存到局部变量表startTimeIndex位置
storeLocal(startTimeIndex);
System.out.println("onMethodEnter getName: " + getName() + " startIndex " + startTimeIndex);
}
/**
* 当方法退出的时候调用
*/
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
//调用System.currentTimeMillis方法,调用完后将返回值推到栈顶
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
//获取当前局部变量表可插入的位置
endTimeIndex = newLocal(Type.LONG_TYPE);
//把栈顶的值(也就是System.currentTimeMillis的返回值)保存到局部变量表endTimeIndex位置
storeLocal(endTimeIndex);
//调用System.out,调用完后栈顶会有一个
getStatic(Type.getType("Ljava/lang/System;"), "out", Type.getType("Ljava/io/PrintStream;"));
//new 一个 StringBuilder();并将其引用值压入栈顶
newInstance(Type.getType("Ljava/lang/StringBuilder;"));
//复制栈顶数值并将复制值压入栈顶(对象StringBuilder的引用)
dup();
//调用StringBuilder的构造方法
invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"), new Method("<init>", "()V"));
//将"execute time=" 从常量池中推送至栈顶
visitLdcInsn("execute time=");
//调用StringBuilder的append方法,将栈顶的值添加进StringBuilder
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
//把局部变量表中的endTimeIndex位置元素,压入栈顶
loadLocal(endTimeIndex);
//把局部变量表中的startTimeIndex位置元素,压入栈顶
loadLocal(startTimeIndex);
//将栈顶两元素进行相减,把结果再压入栈顶
math(SUB, Type.LONG_TYPE);
//调用append方法,将栈顶值添加
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("append", "(J)Ljava/lang/StringBuilder;"));
//将" ms" 从常量池中推送至栈顶
visitLdcInsn(" ms");
//调用StringBuilder的append方法,将栈顶的值添加进StringBuilder
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
//调用StringBuilder的toString方法,调用完后,将结果推到栈顶
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("toString", "()Ljava/lang/String;"));
//调用println,打印栈顶的值
invokeVirtual(Type.getType("Ljava/io/PrintStream;"), new Method("println", "(Ljava/lang/String;)V"));
}
}
先看看效果
可以看到,每个方法都插入方法执行时间的统计(如何优化的问题留到最后后面🤭)。
看完结果,我们先抛开MyClassVisitor和MyMethodVisitor流程很简单,总共就三步
- 读取Class文件
- 使用ClassVisitor类对Class文件进行代码扫描,这一步就可以进行代码的修改和插入了。
- 代码修改插入结束后,自然就可以重新输出Class文件,这里我们给Class改个名为InjectTest2,方便观察。
可以看到,最重要的其实就是第二步,对字节码进行插桩(你也可以叫代码观察,代码扫描,你喜欢就行😀),如果你有第一节ASM如何生成代码的经验,上面的代码唯一不懂的也就是MyClassVisitor,其实这就一个对类里面代码进行扫描的类。相应的,还会MethodVisitor,FiledVisitor,知道了ClassVisitor是什么意思后,这两个也不难理解。
接下来让我们一起看看ClassVisitor和MethodVisitor的用法解释。如果知道的朋友,可以直接跳到优化章节了。
三、ClassVisitor
看名字就能看出来,这是一个对Class文件进行观察(扫描)的工具类。因为它是一个抽象类,所以我们只能对其进行继承重写。
并且ClassVisitor所有的调用都是由ClassReader来进行回调,就是我们前面accept方法。而这个方法里,执行了对ClassVisitor源码扫描的回调,截图如下:
接下来我们看看ClassVisitor的构造方法
3.1 构造方法
protected ClassVisitor(final int api, final ClassVisitor classVisitor)
参数解释
- api:代表是ASM API的版本 ,必须是如下的版本号,默认填最新(ASM9)即可。
注意:编写代码和代码读取的版本号最好保持一致,不然可能会有一些兼容性的错误,例如下面,一些API版本的判断。
- classVisitor:传一个ClassVisitor的子类,对字节码修改的内容进行保存。
上面这个解释可能会让你感到不可思议,我们可以先看我们上面代码这个参数传的是ClassWriter的实例,也就是说ClassWriter继承于ClassVisitor。其实也不难理解,这里ClassWriter的作用就是保存我们修改的信息,然后重新输出字节码,因为ClassVisitor就是一个纯粹的代码扫描类(可以自行去看他的源码,没有保存修改代码的地方)。那么这个字段的作用其实就是用来对字节码修改进行保存的,我们简单来看下ClassVisitor某个方法的回调。例如:visitMethod方法,因为我们就是在这里进行字节码修改的
public MethodVisitor visitMethod(
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions) {
if (cv != null) {
return cv.visitMethod(access, name, descriptor, signature, exceptions);
}
return null;
}
可以看到,方法最终的调用都给了cv,而这个cv就是我们传进来的ClassWriter 。不仅是这个方法,在ClassVisitor中,除了构造方法外,其他所有的方法最终的执行都是给了cv,也就是说,ClassVisitor就是一个代理类。
接下来介绍几个常用回调方法。
3.2 visitMethod
作用:回调Class中的每一个方法
源代码如下:
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
参数的解释这里就不过说明了,在上章节中有具体说明,这里说下如何使用来修改字节码。
其实也很简单,固定用法,删除某个方法
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println("读到Class的一个方法visitMethod: " + name);
if (name.equals("test")){
//删除test方法
return null;
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
想对方法体修改:
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MyMethodVisitor(api, methodVisitor, access, name, descriptor);
为什么需要拿到super.visitMethod呢?
因为这个拿到的methodVisitor不就是我们传入的ClassWriter的MethodVisitor,不拿到它,最后怎么把代码合成一份。
3.3 visitField
作用:回调Class中的每一个变量
使用如下,删除/修改一个变量
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
System.out.println("读到Class的一个变量visitField: " + name);
if (name.equals("a")){
//删除a变量
return null;
}
if (name.equals("b")){
//修改b变量名词为d,值为10
return super.visitField(access, "d", descriptor, signature, 10);
}
return super.visitField(access, name, descriptor, signature, value);
}
添加一个变量
@Override
public void visitEnd() {
super.visitEnd();
FieldVisitor fv = cv.visitField(Opcodes.ACC_PRIVATE, "e", "I", null, null);
if (fv != null) {
fv.visitEnd();
}
}
3.4 visitAnnotation
作用:回调Class上的每一个注解
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
return super.visitAnnotation(descriptor, visible);
}
参数解释:
- descriptor:注解的描述符
- visible:如果注解是运行时(也就是注解上的@Retention(RetentionPolicy.RUNTIME))返回true,否则就是false
四、MethodVisitor
类似于ClassVisitor,MethodVisitor是对方法进行扫描,包括方法上的注解,方法内的所有代码。
在本文中,我们使用的是继承于AdviceAdapter的类,其实它最终也是继承于MethodVisitor,相比于MethodVisitor,它有更多封装好的功能,方便我们对代码进行查看修改(具体可以看源码),所以这里主要介绍AdviceAdapter的用法。
当然,你也可以直接继承MethodVisitor使用。
4.1 onMethodEnter
作用:方法进入时调用,可以在方法执行前插入代码
用法:根据JVM指令,使用下列指令进行代码插件修改
4.2 onMethodExit
作用:方法退出时调用,可以在方法执行后插入代码
用法:根据JVM指令,使用下列指令进行代码插件修改
4.3 invokeStatic
作用:调用一个静态方法,也就是static。实际调用的还是MethodVisitor的visitMethodInsn
例如:Java代码,调用系统静态方法
long start = System.currentTimeMillis();
对应的JVM字节码
INVOKESTATIC java/lang/System.currentTimeMillis ()J
LSTORE 1
注意1:如果调用的方法有返回值,会自动压入栈顶!!
注意2:Long和Double类型的值保存进局部变量表会占用2个位置,也就是两个slot,一个slot是32位。slot是局部变量表的单位
用法如下:
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
int startIndex = newLocal(Type.LONG_TYPE);
storeLocal(startIndex);
//上面两句等同于下面这一句,不推荐下面这种
storeLocal(1)
注意1:这里newLocal只是为了方便类统计局部变量表的索引,传Type_LONG_TYPE和随便一个值他的返回值都是一样的,影响的只会是下一个调用的人,所以说要传正确的值类型。
同理,invokeConstructor,invokeVirtual,invokeDynamic,invokeInterface调用的还是MethodVisitor的visitMethodInsn。
4.4 newLocal
作用:创建一个局部变量的类型,类会自动加上类型的Size,返回值是局部变量表里的索引。
用法如下:
newLocal(Type.LONG_TYPE);
提示:如果此时局部变量表是空的,第一次调用上面语句后,会返回0的索引,下一次再调用上面语句,就是返回2的索引,因为Long占用两个slot。
4.5 storeLocal
作用:将栈顶的值保存到局部变量表的指令,实际调用的还是MethodVisitor的visitVarInsn,最好就是搭配newLocal方法使用
例如上面的invokeStatic 例子,当调用完inkeStatic命令完,栈顶有值,就可以执行storeLocal命令。如下:
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
int startIndex = newLocal(Type.LONG_TYPE);
storeLocal(startIndex);
提示:也不一定要调用newLocal,不过就需要你手动计算索引,填入正确的索引
4.6 push
作用:将给定的值压入栈顶,一般要搭配storeLocal使用,实际调用的还是MethodVisitor的visitInsn
例如:Java代码,创建一个变量
String a = "hhhh";
long b = 2L;
double c = 2.22D;
对应的JVM字节码
L0
LINENUMBER 6 L0
LDC "hhhh"
ASTORE 1
L1
LINENUMBER 7 L1
LDC 2
LSTORE 2
L2
LINENUMBER 8 L2
LDC 2.22
DSTORE 4
用法如下:
push("hhhh");
storeLocal(newLocal(Type.CHAR_TYPE));
push(2L);
storeLocal(newLocal(Type.LONG_TYPE));
push(2.22);
storeLocal(newLocal(Type.LONG_TYPE));
问题:我试过push(1),但是写入Class里的内容却是Boolean,没搞懂,有知道的小伙伴欢迎评论。
4.7 getStatic
作用:获取一个静态的实例变量,实际调用的还是MethodVisitor的visitFieldInsn
例如:
getStatic(Type.getType("Ljava/lang/System;"), "out", Type.getType("Ljava/io/PrintStream;"));
同理,getFiled就是获取非静态的实例变量,也就是获取类的变量。
相应的,putStatic和putFiled就是给静态实例变量赋值和非静态变量赋值。
提示:不是直接调用就能赋值的,赋值前栈顶需要有对应的变量和值。
4.8 newInstance
作用:创建一个对象,并将其引用值压入栈顶,实际调用的还是MethodVisitor的visitTypeInsn
4.9 dup
作用:从栈顶的对象赋值一个引用,并将其引用值压入栈顶,实际调用的还是MethodVisitor的visitInsn
例如下面代码:
StringBuilder stringBuilder = new StringBuilder();
用法:
//new 一个 StringBuilder();并将其引用值压入栈顶
newInstance(Type.getType("Ljava/lang/StringBuilder;"));
//复制栈顶元素(对象StringBuilder的引用)
dup();
4.10 visitLdcInsn
作用:将传入的值从常量池中推送至栈顶
4.11 loadLocal
作用:把局部变量表中的元素,压入栈顶
4.12 math
作用:将栈顶的两个元素进行计算(例如:加减乘除等),并把结果值重新压入栈顶。
五、FieldVisitor
类似于ClassVisitor,FieldVisitor是对变量的信息进行扫描,包括变量上的注解,变量的参数。
5.1 visitAnnotation
作用:回调变量上的每一个注解
源代码:
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible)
参数解释:
- descriptor:注解的描述符
- visible:如果注解是运行时(也就是注解上的@Retention(RetentionPolicy.RUNTIME))返回true,否则就是false
六、代码优化
可以看到,生成出来的Class文件对每个方法都进行了代码插入,这很明显不是我们需要的。解决方法如下:
第一种,进行方法过滤
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println("读到Class的一个方法visitMethod: " + name);
if (!name.equals("test")) {
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MyMethodVisitor(api, methodVisitor, access, name, descriptor);
}
这种虽然很方便,但是好像也太Low了。我们再来看第二种
第二种,注解方式
很简单,创建一个注解(@ASMTarget),然后在对应需要插入代码的方法上写上我们注解。例如:
ASMTarget代码
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.FIELD,ElementType.ANNOTATION_TYPE})
public @interface ASMTarget {
}
提前在需要插入代码的方法上写上@ASMTarget
@ASMTarget
public static void main(String[] args) throws InterruptedException {
Thread.sleep(1234);
}
然后我们看看MethodVisitor代码要如何修改,如下::
boolean needInsertCode = false;//标记是否需要插入代码
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if ("Lasm/ASMTarget;".equals(descriptor)) {
needInsertCode = true;
}
return super.visitAnnotation(descriptor, visible);
}
然后在onMethodEnter和onMethodExit进行拦截即可,代码如下。
@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (!needInsertCode) {
return;
}
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
if (!needInsertCode) {
return;
}
}
代码地址:JMMDemo: 一些关于JMM,javassist,asm学习和一些案例。。。。。。。。。。。。。。。。。。。 - Gitee.com