gradle的很多插件涉及到了字节码修改,内部修改字节码基本都是通过asm来修改,本文章就是通过asm来修改class。本文基于intellij idea来体验的。
下载依赖
首先我创建的工程是一个java工程,然后将项目配置成maven工程:
接着去maven官网找下对应依赖:
然后在工程里面添加依赖:

读取class
- 首先定义一个普通的类:
1
2
3
4
5
6
7
8
9
10
11
|
public class User {
private String name;//Ljava/lang/String表示string类型
private int age;//I表示int类型
private long lang;//在class中J表示long类型
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
|
- 读取到User类对应class的路径
1
2
3
4
5
6
7
8
9
|
public class Utils {
public static String getClassFilePath(Class clazz) {
// file:/Users/xiangcheng/IdeaProject/Asm/target/classes/
String buildDir = clazz.getProtectionDomain().getCodeSource().getLocation().getFile();//获取到文件所在位置
String fileName = clazz.getSimpleName() + ".class";
File file = new File(buildDir + clazz.getPackage().getName().replaceAll("[.]", "/") + "/", fileName);
return file.getAbsolutePath();
}
}
|
- 通过ClassReader获取到class的读取
1
2
3
4
|
Class clazz = User.class;
String clazzFilePath = Utils.getClassFilePath(clazz);
//首先通过class的路径获取到classReader,然后通过classReader的accept方法,传入一个classNode,然后使用classNode获取到方法和属性
ClassReader classReader = new ClassReader(new FileInputStream(clazzFilePath));
|
- 通过classReader获取到classNode
1
2
|
ClassNode classNode = new ClassNode(Opcodes.ASM5);
classReader.accept(classNode, 0);
|
- ClassNode读取方法和属性
1
2
3
4
5
6
7
8
9
10
|
List<MethodNode> methods = classNode.methods;
List<FieldNode> fields = classNode.fields;
System.out.println("methods:");
for (MethodNode methodNode : methods) {
System.out.println(methodNode.name + ", " + methodNode.desc);
}
System.out.println("fields:");
for (FieldNode fieldNode : fields) {
System.out.println(fieldNode.name + ", " + fieldNode.desc);
}
|
获取信息如下:
1
2
3
4
5
6
7
8
|
methods:
<init>, ()V
getName, ()Ljava/lang/String;
getAge, ()I
fields:
name, Ljava/lang/String;
age, I
lang, J
|
我们通过jclasslib验证下:
其中属性的名字,描述符,访问标识都和上面的对应的上。方法也是如此。
上面是通过classNode树形结构访问的,下面通过visit回调的形式访问属性和方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
Class clazz = User.class;
String clazzFilePath = Utils.getClassFilePath(clazz);
ClassReader classReader = new ClassReader(new FileInputStream(clazzFilePath));
//前面介绍的classNode是继承自classVisitor,classNode是把所有的方法和属性全部都读到内存中,然后再遍历,ClassVisitor边读边遍历
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5) {
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
System.out.println("visit field:" + name + " , desc = " + descriptor);
return super.visitField(access, name, descriptor, signature, value);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println("visit method:" + name + " , desc = " + descriptor);
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
};
classReader.accept(classVisitor, 0);
|
通过ClassVisitor也能访问到属性和方法,上面介绍的ClassNode是继承自ClassVisitor,ClassVisitor是边读边遍历,而ClassNode是把方法和属性全部都读到内存中,然后再遍历。
修改class
上面介绍的都是读取class,我们最终的目的是修改class,下面也通过一个🌰来介绍如何修改class,还是先来一个类:
1
2
3
4
5
6
7
|
public class C {
public void m() throws Exception {
Thread.sleep(100);
}
}
|
我们要把它修改成如下:
1
2
3
4
5
6
7
8
9
|
public class C {
public static long timer;
public void m() throws Exception {
timer -= System.currentTimeMillis();
Thread.sleep(100L);
timer += System.currentTimeMillis();
}
}
|
asm修改字节码先通过ClassReader把class信息读取到,然后通过ClassWriter把修改后的class再写会到文件里面,先介绍下通过ClassWriter把class写到文件中:
1
2
3
4
5
6
7
8
9
10
11
12
|
Class clazz = C.class;
String clazzFilePath = Utils.getClassFilePath(clazz);
ClassReader classReader = new ClassReader(new FileInputStream(clazzFilePath));
ClassWriter classWriter = new ClassWriter(0);
classReader.accept(classWriter, 0);
// 写入文件
byte[] bytes = classWriter.toByteArray();
String buildDir = clazz.getProtectionDomain().getCodeSource().getLocation().getFile();
FileOutputStream fos = new FileOutputStream(buildDir + clazz.getPackage().getName().replaceAll("[.]", "/") +"/copyed.class");
fos.write(bytes);
fos.flush();
fos.close();
|
可以看到classReader的accept方法也可以接受一个classWriter对象,它也是继承自classVisitor,然后把它转化成byte数组,最后通过文件输出流把byte数组写会到文件中,整个过程很简单。修改class也是如此把修改后的byte数组也是写会到文件中的。
在上面我们知道classVisitor是读取class的核心类,最终把该classVistor传到accept方法中,而classWriter也需要传到accept方法中。所以看下两者都要使用的话,该怎么处理:
可以看到构造器中可以传入classVisitor,最终里面的所有visit**
方法都是调用了传入进来的classVisitor方法。所以不难看出来这是个代理模式,传入进来的classVisitor是一个被代理对象,该classVisitor是一个代理对象,首先自定义一个classVisitor:
1
2
3
4
5
6
|
public class AddTimerClassVisitor extends ClassVisitor {
public AddTimerClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
}
|
然后看下怎么调用AddTimerClassVisitor
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
Class clazz = C.class;
String clazzFilePath = Utils.getClassFilePath(clazz);
ClassReader classReader = new ClassReader(new FileInputStream(clazzFilePath));
//ClassWriter.COMPUTE_FRAMES表示自动计算方法栈数
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//通过构造一个新的classVisitor,然后将classWriter传入到该classVistor中,实际干活的还是classWriter,AddTimerClassVisitor只是作为一个代理类,进行添加属性
AddTimerClassVisitor addTimerClassVisitor = new AddTimerClassVisitor(Opcodes.ASM5, classWriter);
classReader.accept(addTimerClassVisitor, 0);
// 写入文件
byte[] bytes = classWriter.toByteArray();
FileOutputStream fos = new FileOutputStream(clazzFilePath);
fos.write(bytes);
fos.flush();
fos.close();
|
将ClassWriter传到AddTimerClassVisitor中,可以看出来ClassWriter是被代理的对象,AddTimerClassVisitor是代理对象,在前面我们通过重写visitField访问到属性,通过visitMethod访问到方法,这两个方法是多次被调用,而我们是想添加一个静态的timer属性,和修改方法m,那此时visitField肯定不会被调用到,在class开始访问和结束访问的时候会分别调用visit和visitEnd方法,那添加timer属性可以放到visitEnd中,修改方法可以在visitMethod中通过过滤来达到修改的目的,而最终的字节码照样可以先把结果通过jclasslib来查看:
在class中名字是timer,描述符是J,访问标志是public+static,那么通过asm如何添加该属性:
1
2
3
4
5
6
7
8
9
10
11
|
@Override
public void visitEnd() {
System.out.println("AddTimerClassVisitor visitEnd");
//通过visitField方法添加属性,cv是传进来的classWriter,实际干活的还是classWriter,此处用到了代理模式
FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "timer",
"J", null, null);//visitField方法第一个参数定义访问标志,它是public+static类型,第二个参数是参数的名字,第三个参数是参数的描述符,表示什么类型,long类型用J表示
if (fv != null) {
fv.visitEnd();
}
cv.visitEnd();
}
|
通过cv(传进来的classWriter)来访问属性来添加timer属性,和上面用jclasslib工具查看的结果一样。下面再看下如果修改m方法:
1
2
3
4
5
6
7
8
9
10
|
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println("AddTimerClassVisitor visitMethod");
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if (methodVisitor != null && !name.equals("<init>")) {
NewMethodVisitor newMethodVisitor = new NewMethodVisitor(Opcodes.ASM5, methodVisitor, mOwner);
return newMethodVisitor;
}
return methodVisitor;
}
|
首先是拿到当前的MethodVisitor,然后判断方法不是构造函数,最后放回一个新的NewMethodVisitor,否则返回原来的MethodVisitor,下来来看下NewMethodVisitor如何修改方法m,先看下jclasslib修改后class的方法m:
其实我们通过javap -v com.xc.asm.C来查看字节码:
更多的javap命令如下:
有了上面反编译的字节码后,其实就是asm对应的方法操作了,在NewMethodVisitor中重写visitCode方法,在访问该方法之前添加对应的字节码,重写visitInsn方法,在方法return之前添加对应的字节码:
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
|
public class NewMethodVisitor extends MethodVisitor {
private String mOwner;
public NewMethodVisitor(int api, MethodVisitor methodVisitor, String mOwner) {
super(api, methodVisitor);
this.mOwner = mOwner;
}
@Override
public void visitCode() {
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, mOwner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LSUB);
mv.visitFieldInsn(PUTSTATIC, mOwner, "timer", "J");
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitFieldInsn(GETSTATIC, mOwner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, mOwner, "timer", "J");
}
mv.visitInsn(opcode);
}
}
|
可以结合上面的反编译的字节码,对应着看asm的操作。最后asm操作完后,就是通过输出流把byte数组写会到文件中。
参考:
Android 进阶之路:ASM 修改字节码,这样学就对了!
asm依赖
asm官网
jclasslib