这篇文章主要介绍 ASM 库的结构、主要的 API,并且通过两个示例说明如何通过 ASM 修改 .class 文件中的方法和属性。
ASM 库是一款基于 Java 字节码层面的代码分析和修改工具。ASM 可以直接生产二进制的 class 文件,也可以在类被加载入 JVM 之前动态修改类行为。
ASM 库的结构如下所示:
如下所示,在 ClassVisitor 中提供了和类结构同名的一些方法,这些方法会对类中相应的部分进行操作,而且是有顺序的:visit [ visitSource ] [ visitOuterClass ] ( visitAnnotation | visitAttribute )* (visitInnerClass | visitField | visitMethod )* visitEnd
public abstract class ClassVisitor {
......
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);
public void visitSource(String source, String debug);
public void visitOuterClass(String owner, String name, String desc);
public AnnotationVisitor visitAnnotation(String desc, boolean visible);
public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible);
public void visitAttribute(Attribute attr);
public void visitInnerClass(String name, String outerName, String innerName, int access);
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value);
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions);
public void visitEnd();
}
这个类会将 .class 文件读入到 ClassReader 中的字节数组中,它的 accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法
ClassWriter 是一个 ClassVisitor 的子类,是和 ClassReader 对应的类,ClassReader 是将 .class 文件读入到一个字节数组中,ClassWriter 是将修改后的类的字节码内容以字节数组的形式输出。
MethodVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Method 时就转入 MethodVisitor 接口处理。
AdviceAdapter 是 MethodVisitor 的子类,使用 AdviceAdapter 可以更方便的修改方法的字节码。
AdviceAdapter 的方法如下所示:
其中比较重要的几个方法如下:
FieldVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Field 时就转入 FieldVisitor 接口处理。和分析 MethodVisitor 的方法一样,也可以查看源码注释进行学习,这里不再详细介绍
假如现在我们有一个 HelloWorld 类,如下
package com.lijiankun24.asmpractice.demo;
public class HelloWorld {
public void sayHello() {
try {
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
通过 javac HelloWorld.java
和 javap -verbose HelloWorld.class
可以查看到 sayName() 方法的字节码如下所示:
public void sayHello();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: ldc2_w #2 // long 2000l
3: invokestatic #4 // Method java/lang/Thread.sleep:(J)V
6: goto 14
9: astore_1
10: aload_1
11: invokevirtual #6 // Method java/lang/InterruptedException.printStackTrace:()V
14: return
Exception table:
from to target type
0 6 9 Class java/lang/InterruptedException
LineNumberTable:
line 5: 0
line 8: 6
line 6: 9
line 7: 10
line 9: 14
StackMapTable: number_of_entries = 2
frame_type = 73 /* same_locals_1_stack_item */
stack = [ class java/lang/InterruptedException ]
frame_type = 4 /* same */
我们通过 ASM 修改 HelloWorld.class 字节码文件,实现统计方法执行时间的功能
public class CostTime {
public static void main(String[] args) {
redefinePersonClass();
}
private static void redefinePersonClass() {
String className = "com.lijiankun24.asmpractice.demo.HelloWorld";
try {
InputStream inputStream = new FileInputStream("/Users/lijiankun/Desktop/HelloWorld.class");
ClassReader reader = new ClassReader(inputStream); // 1. 创建 ClassReader 读入 .class 文件到内存中
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS); // 2. 创建 ClassWriter 对象,将操作之后的字节码的字节数组回写
ClassVisitor change = new ChangeVisitor(writer); // 3. 创建自定义的 ClassVisitor 对象
reader.accept(change, ClassReader.EXPAND_FRAMES); // 4. 将 ClassVisitor 对象传入 ClassReader 中
Class clazz = new MyClassLoader().defineClass(className, writer.toByteArray());
Object personObj = clazz.newInstance();
Method nameMethod = clazz.getDeclaredMethod("sayHello", null);
nameMethod.invoke(personObj, null);
System.out.println("Success!");
byte[] code = writer.toByteArray(); // 获取修改后的 class 文件对应的字节数组
try {
FileOutputStream fos = new FileOutputStream("/Users/lijiankun/Desktop/HelloWorld2.class"); // 将二进制流写到本地磁盘上
fos.write(code);
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("Failure!");
}
}
static class ChangeVisitor extends ClassVisitor {
ChangeVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
if (name.equals("")) {
return methodVisitor;
}
return new ChangeAdapter(Opcodes.ASM4, methodVisitor, access, name, desc);
}
}
static class ChangeAdapter extends AdviceAdapter {
private int startTimeId = -1;
private String methodName = null;
ChangeAdapter(int api, MethodVisitor mv, int access, String name, String desc) {
super(api, mv, access, name, desc);
methodName = name;
}
@Override
protected void onMethodEnter() {
super.onMethodEnter();
startTimeId = newLocal(Type.LONG_TYPE);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitIntInsn(LSTORE, startTimeId);
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
int durationId = newLocal(Type.LONG_TYPE);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LLOAD, startTimeId);
mv.visitInsn(LSUB);
mv.visitVarInsn(LSTORE, durationId);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "", "()V", false);
mv.visitLdcInsn("The cost time of " + methodName + " is ");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(LLOAD, durationId);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
}
执行结果如下图所示
反编译 HelloWorld2.class 文件的内容如下所示
3.2 修改类中属性的字节码
这一节中我们将展示一下如何使用 Core API 对类中的属性进行操作。
假如说,现在有一个 Person.java 类如下所示:
public class Person {
public String name;
public int sex;
}
我们想为这个类,添加一个 ‘public int age’ 的属性该怎么添加呢?我们会面对两个问题:
接下来,我们就一一解决上面的两个问题?
按照我们分析的上述的 2.6 操作流程叙述,需要以下三个步骤:
在上面三个步骤中,可以操作的就是 ClassVisitor 了。ClassVisitor 接口提供了和类结构同名的一些方法,这些方法可以对相应的类结构进行操作。
在使用 ClassVisitor 添加类属性的时候,只需要添加一句话就可以了:
classVisitor.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class), null, null);
我们先暂且在 ClassVisitor 的 visitEnd() 方法中写入上面的代码,如下所示
public class Transform extends ClassVisitor {
public Transform(ClassVisitor cv) {
super(cv);
}
@Override
public void visitEnd() {
cv.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class), null, null);
}
}
我们写如下的测试类,测试一下
public class FieldPractice {
public static void main(String[] args) {
addAgeField();
}
private static void addAgeField() {
try {
InputStream inputStream = new FileInputStream("/Users/lijiankun/Desktop/Person.class");
ClassReader reader = new ClassReader(inputStream);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = new Transform(writer);
reader.accept(visitor, ClassReader.SKIP_DEBUG);
byte[] classFile = writer.toByteArray();
MyClassLoader classLoader = new MyClassLoader();
Class clazz = classLoader.defineClass("Person", classFile);
Object obj = clazz.newInstance();
System.out.println(clazz.getDeclaredField("name").get(obj)); //----(1)
System.out.println(clazz.getDeclaredField("age").get(obj)); //----(2)
} catch (Exception e) {
e.printStackTrace();
}
}
}
其输出入下所示:
那如果我们尝试在 ClassVisitor#visitField() 方法中添加属性可以吗?我们可以修改 Transform 测试一下:
public class Transform extends ClassVisitor {
Transform(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
cv.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class), null, null);
return super.visitField(access, name, desc, signature, value);
}
}
还是使用上面的测试代码测试一下,会有如下的测试结果
在 Person 类中有重复的属性,为什么会报这个错误呢?
分析 ClassVisitor#visitField() 方法可得知,只要访问类中的一个属性,visitField() 方法就会被调用一次,在 Person 类中有两个属性,所以 visitField() 方法就会被调用两次,也就添加了两次 ‘public int age’ 属性,就报了上述的错误,而 visitEnd() 方法只有在最后才会被调用且只调用一次,所以在 visitEnd() 方法中是添加属性的最佳时机
可能有人会问,我刚开始学,上面例子中那些 ASM 的代码我还不会写,怎么办呢?ASM 官方为我们提供了 ASMifier,可以帮助我们生成这些晦涩难懂的 ASM 代码。
比如,我想通过 ASM 实现统计一个方法的执行时间,该怎么做呢?一般会有如下的代码:
package com.lijiankun24.classpractice;
public class Demo {
public void costTime() {
long startTime = System.currentTimeMillis();
// ......
long duration = System.currentTimeMillis() - startTime;
System.out.println("The cost time of this method is " + duration + " ms");
}
}
那上面这段代码对应的 ASM 代码是什么呢?我们可以通过以下两个步骤,使用 ASMifier 自动生成:
javac
编译该 Demo.java
文件生成对应的 Demo.class
文件,如下所示javac Demo.java
asm-all.jar
库,我下载的是最新的 asm-all-5.2.jar
,然后使用如下命令,即可生成java -classpath asm-all-5.2.jar org.objectweb.asm.util.ASMifier Demo.class
截图如下:
DemoDump.png
深入字节码 -- 玩转 ASM-Bytecode 原 荐
美团热更方案ASM实践
访问者模式和 ASM
本篇文章就介绍一下访问者模式的概念以及其在 ASM 中的应用。
如果老师教学反馈得分大于等于85分、学生成绩大于等于90分,则可以入选成绩优秀奖;如果老师论文数目大于8、学生论文数目大于2,则可以入选科研优秀奖。
在这个例子中,老师和学生就是Element,他们的数据结构稳定不变。从上面的描述中,我们发现,对数据结构的操作是多变的,一会儿评选成绩,一会儿评选科研,这样就适合使用访问者模式来分离数据结构和操作。
public interface Element {
void accept(Visitor visitor);
}
创建两个具体元素 Student 和 Teacher,分别实现 Element 接口
public class Student implements Element {
private String name;
private int grade;
private int paperCount;
public Student(String name, int grade, int paperCount) {
this.name = name;
this.grade = grade;
this.paperCount = paperCount;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
......
}
public class Teacher implements Element {
private String name;
private int score;
private int paperCount;
public Teacher(String name, int score, int paperCount) {
this.name = name;
this.score = score;
this.paperCount = paperCount;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
......
}
public interface Visitor {
void visit(Student student);
void visit(Teacher teacher);
}
创建一个根据分数评比的具体访问者 GradeSelection,实现 Visitor 接口
public class GradeSelection implements Visitor {
@Override
public void visit(Student student) {
if (student != null && student.getGrade() >= 90) {
System.out.println(student.getName() + "的分数是" + student.getGrade() + ",荣获了成绩优秀奖。");
}
}
@Override
public void visit(Teacher teacher) {
if (teacher != null && teacher.getScore() >= 85) {
System.out.println(teacher.getName() + "的分数是" + teacher.getScore() + ",荣获了成绩优秀奖。");
}
}
}
public class VisitorClient {
public static void main(String[] args) {
Element element = new Student("lijiankun24", 90, 3);
Visitor visitor = new GradeSelection();
element.accept(visitor);
}
}
上述代码即是一个简单的访问者模式的示例代码,输出如下所示:
visitor.png
上述代码可以分为三步:
1. 创建一个元素类的对象
2. 创建一个访问类的对象
3. 元素对象通过 Element#accept(Visitor visitor) 方法传入访问者对象
ASM 库是 Visitor 模式的典型应用。
在 ASM 库中存在以下几个重要的类:
ASM 大致的工作流程是: