最近进行组内分享时选择了这个Java字节码处理这个主题,特此记录下来。众所周知,Java是一门运行在虚拟机上的语言,在创建之初就是为了"write once ,run anywhere "的目的,为了解决不同架构处理器区别,通过虚拟机来屏蔽不同的各个操作系统之间的区别,其虚拟机上运行的平台中立的二进制文件正是class字节码文件,当然JIT,AOT之类的技术是后来为了让Java运行更快加入的,不过这里我们只是对Java的字节码文件的处理。
ASM是什么呢,ASM是一个Java字节码层次的处理框架。它可以直接对class文件进行增删改的操作,Java中许多的框架的实现正是基于ASM,比如AOP的实现,Java自身的动态代理只能支持接口的形式,而使用ASM就能很方便的扩展到类的代理。可以说ASM就是一把利剑,是深入Java必须学习的一个点。
本章主要通过以下几点来解析ASM
- ASM的基础使用
- ASM的设计模式
- Class的文件格式
- ASM的源码解析
- ASM总结
ASM的基础使用
对于ASM的使用最推荐的莫过于官方的 《ASM Guide》,介绍的十分详细。这里先抛砖引玉给出一个比较简单的例子,引出ASM中最重要的三个类。假如我们有一个需求是给一个class文件添加一个field字段,代码如下
public class AddField extends ClassVisitor {
private String name;
private int access;
private String desc;
private Object value;
private boolean duplicate;
public AddField(ClassVisitor cv, String name, int access, String desc, Object value) {
super(ASM5, cv);
this.name = name;
this.access = access;
this.desc = desc;
this.value = value;
}
@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
if (this.name.equals(name)) {
duplicate = true;
}
return super.visitField(access, name, desc, signature, value);
}
@Override
public void visitEnd() {
if (!duplicate) {
FieldVisitor fv = super.visitField(access, name, desc, null, value);
if (fv != null) {
fv.visitEnd();
}
}
super.visitEnd();
}
public static void main(String[] args) throws Exception {
String output = System.getProperty("user.dir") + "/libjava/output";
String classDir = System.getProperty("user.dir") + "/libjava/output/MainActivity.class";
ClassReader classReader = new ClassReader(new FileInputStream(classDir));
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
ClassVisitor addField = new AddField(classWriter,
"field",
Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC + Opcodes.ACC_FINAL,
Type.getDescriptor(String.class),
"value"
);
classReader.accept(addField, ClassReader.EXPAND_FRAMES);
byte[] newClass = classWriter.toByteArray();
File newFile = new File(output, "MainActivity1.class");
new FileOutputStream(newFile).write(newClass);
}
}
这段代码是给一个Class文件多加一个字段的操作,在ASM的封装下,这样的操作实现非常简单,下面我们就将学习到ASM是如何做到这一切的。
ASM的设计模式
从前面的ClassVisitor以及accept方法就能看出这是明显的visitor(访问者)模式,visitor模式在Java的设计模式中是一种比较冷门的设计模式,冷门并不是因为这个模式的缺陷,而是相较于一些风格比较明显的比如单例,工厂,观察者模式,访问者模式的实际应用场景是比较少的,后面会介绍到这个模式实际存在的一个不小的问题。
访问者模式主要包含被访问的元素以及访问者两部分,元素一般是不同的类型,不同的访问者对于这些元素的操作一般也是不同的,访问者模式使得用户可以在不修改现有系统的情况下扩展系统的功能,为这些不同类型的元素增加新的操作。
访问者的模式讲解比较好的文章可以看这篇,操作复杂对象结构——访问者模式
一般访问者模式的元素会接受一个访问者的参数,在元素内部这个访问者会直接访问这个元素,说的比较绕,用代码来解释
public interface Employee {
public void accept(Department handler);
}
首先对于元素类我们提供一个接口,内部的方法就是accept,用来接收一个访问者类
public abstract class Department {
public abstract void visit(FulltimeEmployee employee);
public abstract void visit(ParttimeEmployee employee);
}
这个抽象的访问者类提供了两个访问具体元素的抽象方法,然后我们具体的实现元素类和访问者类。
public class FADepartment extends Department {
@Override
public void visit(FulltimeEmployee employee) {
System.out.println("正式员工 : " + employee.getName());
}
@Override
public void visit(ParttimeEmployee employee) {
System.out.println("临时工 : " + employee.getName());
}
}
public class FulltimeEmployee implements Employee {
private String name;
private String salary;
public FulltimeEmployee(String name, String salary) {
this.name = name;
this.salary = salary;
}
@Override
public void accept(Department handler) {
handler.visit(this);
}
}
这个的元素实现类中的accept方法中传进来的是抽象的访问者类,然后将自身转发出去,达到外界访问这个元素的目的。
public class EmployeeList {
private List<Employee> list = new ArrayList<>();
public void addEmployee(Employee employee) {
list.add(employee);
}
public void accept(Department department) {
for (Employee employee : list) {
employee.accept(department);
}
}
public static void main(String[] args) {
EmployeeList list = new EmployeeList();
Employee employee1 = new FulltimeEmployee("张三", "1000");
Employee employee2 = new ParttimeEmployee("李四", "500");
Employee employee3 = new ParttimeEmployee("王五", "400");
list.addEmployee(employee1);
list.addEmployee(employee2);
list.addEmployee(employee3);
Department department1 = new FADepartment();
list.accept(department1);
}
}
看上面这个例子,通过这种转发的方式,我们就能在不修改元素的条件下,添加对于元素访问的操作,只需要让元素类accept一个新的访问者就行。这样看起来我们通过accept的转发行为让元素类和操作类进行了解耦,所以这种模式这种模式对于添加新的访问者的操作是符合“开闭原则”的,但是如果我们一旦要增加新的元素时,就会导致所有的访问者类都需要增加相应的访问方法,这是明显违反设计模式,所以访问者模式并不适合元素类频繁变动的场景,这也是访问者模式自身最大的缺陷。
对于Java class文件来说,其中的元素从设计到现在,基本没有变化,仅仅只添加了一些细节,比如泛型的引入,其实Java就是为了保证低版本的兼容性,才导致了一些不合理的存在,比如为了引入泛型,但是又不想破坏兼容性,才有了泛型擦除这样的坑爹操作,相比C#这样的语言,Java的泛型可以说只是伪泛型,但是Java的优势就是兼容性好,对于访问者模式也就是元素类不会随意改动,这个缺陷也就不存在了。
Class的文件格式
前面说到Class文件结构,想要理解ASM的内部运行原理,首先需要了解Class文件,这样才能知道ASM如何对于Class文件进行操作,Class文件作为虚拟机所执行的平台中立文件,内部结构设计的十分的清晰,每一个Class文件都对应着唯一一个类或接口的定义信息。每个Class文件都由以8位为单位的字节流组成,下面是一个Class文件中所包括的项,在Class文件中,各项按照严格顺序连续存放,中间没有任何填充或者对齐作为各项间的分隔符号。
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
在Class文件结构中,上面各项的含义如下:
magic: 作为一个魔数,确定这个文件是否是一个能被虚拟机接受的class文件,值固定为0xCAFEBABE。
minor_version,major_version:分别表示class文件的副,主版本号,不同版本的虚拟机实现支持的Class文件版本号不同。
constant_pool_count:常量池计数器,constant_pool_count的值等于常量池表中的成员数加1。
constant_pool:常量池,constant_pool是一种表结构,包含class文件结构及其子结构中引用的所有字符常量、类或接口名、字段名和其他常量。
access_flags:access_flags是一种访问标志,表示这个类或者接口的访问权限及属性,包括有ACC_PUBLIC,ACC_FINAL,ACC_SUPER等等。
this_class:类索引,指向常量池表中项的一个索引。
super_class:父类索引,这个值必须为0或者是对常量池中项的一个有效索引值,如果为0,表示这个class只能是Object类,只有它是唯一没有父类的类。
interfaces_count:接口计算器,表示当前类或者接口的直接父接口数量。
interfaces[]:接口表,里面的每个成员的值必须是一个对常量池表中项的一个有效索引值。
fields_count:字段计算器,表示当前class文件中fields表的成员个数,每个成员都是一个field_info。
fields:字段表,每个成员都是一个完整的fields_info结构,表示当前类或接口中某个字段的完整描述,不包括父类或父接口的部分。
methods_count:方法计数器,表示当前class文件methos表的成员个数。
methods:方法表,每个成员都是一个完整的method_info结构,可以表示类或接口中定义的所有方法,包括实例方法,类方法,以及类或接口初始化方法。
attributes_count:属性表,其中是每一个attribute_info,包含以下这些属性,InnerClasses,EnclosingMethod,Synthetic,Signature,Annonation等。
ASM的源码解析
前面我们分析了ASM的场景刚好适合访问者模式,同时也是这么做的,这里我们就从accept
方法开始分析ASM,限于篇幅,这里省略了accept方法中部分的visitor调用,不过原理都是一样的,可以自行对照源码参考
//ClassReader.java
public void accept(final ClassVisitor classVisitor,
final Attribute[] attrs, final int flags) {
int u = header; // current offset in the class file
char[] c = new char[maxStringLength]; // buffer used to read strings
Context context = new Context();
context.attrs = attrs;
context.flags = flags;
context.buffer = c;
// reads the class declaration
int access = readUnsignedShort(u);
String name = readClass(u + 2, c);
String superClass = readClass(u + 4, c);
String[] interfaces = new String[readUnsignedShort(u + 6)];
u += 8;
for (int i = 0; i < interfaces.length; ++i) {
interfaces[i] = readClass(u, c);
u += 2;
}
// reads the class attributes
......
// visits the class declaration
classVisitor.visit(readInt(items[1] - 7), access, name, signature,
superClass, interfaces);
// visits the source and debug info
if ((flags & SKIP_DEBUG) == 0
&& (sourceFile != null || sourceDebug != null)) {
classVisitor.visitSource(sourceFile, sourceDebug);
}
// visits the outer class
if (enclosingOwner != null) {
classVisitor.visitOuterClass(enclosingOwner, enclosingName,
enclosingDesc);
}
// visits the class annotations and type annotations
if (ANNOTATIONS && anns != 0) {
for (int i = readUnsignedShort(anns), v = anns + 2; i > 0; --i) {
v = readAnnotationValues(v + 2, c, true,
classVisitor.visitAnnotation(readUTF8(v, c), true));
}
}
if (ANNOTATIONS && ianns != 0) {
for (int i = readUnsignedShort(ianns), v = ianns + 2; i > 0; --i) {
v = readAnnotationValues(v + 2, c, true,
classVisitor.visitAnnotation(readUTF8(v, c), false));
}
}
if (ANNOTATIONS && tanns != 0) {
for (int i = readUnsignedShort(tanns), v = tanns + 2; i > 0; --i) {
v = readAnnotationTarget(context, v);
v = readAnnotationValues(v + 2, c, true,
classVisitor.visitTypeAnnotation(context.typeRef,
context.typePath, readUTF8(v, c), true));
}
}
if (ANNOTATIONS && itanns != 0) {
for (int i = readUnsignedShort(itanns), v = itanns + 2; i > 0; --i) {
v = readAnnotationTarget(context, v);
v = readAnnotationValues(v + 2, c, true,
classVisitor.visitTypeAnnotation(context.typeRef,
context.typePath, readUTF8(v, c), false));
}
}
// visits the attributes
while (attributes != null) {
Attribute attr = attributes.next;
attributes.next = null;
classVisitor.visitAttribute(attributes);
attributes = attr;
}
// visits the inner classes
if (innerClasses != 0) {
int v = innerClasses + 2;
for (int i = readUnsignedShort(innerClasses); i > 0; --i) {
classVisitor.visitInnerClass(readClass(v, c),
readClass(v + 2, c), readUTF8(v + 4, c),
readUnsignedShort(v + 6));
v += 8;
}
}
// visits the fields and methods
u = header + 10 + 2 * interfaces.length;
for (int i = readUnsignedShort(u - 2); i > 0; --i) {
u = readField(classVisitor, context, u);
}
u += 2;
for (int i = readUnsignedShort(u - 2); i > 0; --i) {
u = readMethod(classVisitor, context, u);
}
// visits the end of the class
classVisitor.visitEnd();
}
在accept方法中传进来了一个参数ClassVisitor
,然后接着往下看,这里我们省略了解析class文件的过程,整个class的解析过程遵循上一小节中Class的文件格式,通过不断的读取ClassReader的构造函数中class二进制byte[],然后在解析后通过参数classVisitor的抽象visitXXX方法将属性全部转发出去,将其中的visitXXX方法按顺序抽离出来就是
classVisitor.visit(readInt(items[1] - 7), access, name, signature,superClass, interfaces);
classVisitor.visitSource(sourceFile, sourceDebug);
classVisitor.visitOuterClass(enclosingOwner, enclosingName,enclosingDesc);
classVisitor.visitAnnotation(readUTF8(v, c);
classVisitor.visitTypeAnnotation(context.typeRef,context.typePath, readUTF8(v, c);
classVisitor.visitAttribute(attributes);
classVisitor.visitInnerClass(readClass(v, c),readClass(v + 2, c), readUTF8(v + 4, c),readUnsignedShort(v + 6));
classVisitor.visitField(access, name, desc,signature, value);
classVisitor.visitMethod(context.access,context.name, context.desc, signature, exceptions);
classVisitor.visitEnd();
整个class文件在accept这个方法中,相当于以庖丁解牛的方式,肢解成了上一小节中ClassFile中的每一项,而这个classVisitor实际上是一个抽象类。
public abstract class ClassVisitor
有抽象类,自然会有实现类,也就是前面的ClassWriter。
public class ClassWriter extends ClassVisitor
在前面我们添加字段的示例之中,在最后处理完class文件后,通过toByteArray
方法生成了新的class文件,所以这里有两个疑问,第一,这个toByteArray
在这里担当了什么角色,为什么能够生成新的Class文件,第二,在参数转发出来之后,我们是如何通过visit系列方法改变整个Class文件的。带着这两个问题,我们继续往下看。
首先看一下 toByteArray
这个方法的前半部分
public byte[] toByteArray() {
if (index > 0xFFFF) {
throw new RuntimeException("Class file too large!");
}
// computes the real size of the bytecode of this class
int size = 24 + 2 * interfaceCount;
int nbFields = 0;
FieldWriter fb = firstField;
while (fb != null) {
++nbFields;
size += fb.getSize();
fb = (FieldWriter) fb.fv;
}
int nbMethods = 0;
MethodWriter mb = firstMethod;
while (mb != null) {
++nbMethods;
size += mb.getSize();
mb = (MethodWriter) mb.mv;
}
int attributeCount = 0;
......
if (ClassReader.ANNOTATIONS && itanns != null) {
++attributeCount;
size += 8 + itanns.getSize();
newUTF8("RuntimeInvisibleTypeAnnotations");
}
if (attrs != null) {
attributeCount += attrs.getCount();
size += attrs.getSize(this, null, 0, -1, -1);
}
size += pool.length;
// allocates a byte vector of this size, in order to avoid unnecessary
// arraycopy operations in the ByteVector.enlarge() method
ByteVector out = new ByteVector(size);
同样限于篇幅,这里省略了中间的一部分计算size的部分,作者在里面给出了注释,计算Class字节的真实大小,这个字节怎么计算呢,这里首先给了一个24,想必是前面ClassFile中的魔数以及版本号之类的字节数大小,然后在后面分别添加了字段,方法,属性等等的大小,通过这个最终的size,构造了一个ByteVetcor。
public byte[] toByteArray() {
......
ByteVector out = new ByteVector(size);
out.putInt(0xCAFEBABE).putInt(version);
out.putShort(index).putByteArray(pool.data, 0, pool.length);
int mask = Opcodes.ACC_DEPRECATED | ACC_SYNTHETIC_ATTRIBUTE
| ((access & ACC_SYNTHETIC_ATTRIBUTE) / TO_ACC_SYNTHETIC);
out.putShort(access & ~mask).putShort(name).putShort(superName);
out.putShort(interfaceCount);
for (int i = 0; i < interfaceCount; ++i) {
out.putShort(interfaces[i]);
}
out.putShort(nbFields);
fb = firstField;
while (fb != null) {
fb.put(out);
fb = (FieldWriter) fb.fv;
}
out.putShort(nbMethods);
mb = firstMethod;
while (mb != null) {
mb.put(out);
mb = (MethodWriter) mb.mv;
}
out.putShort(attributeCount);
if (bootstrapMethods != null) {
out.putShort(newUTF8("BootstrapMethods"));
out.putInt(bootstrapMethods.length + 2).putShort(
bootstrapMethodsCount);
out.putByteArray(bootstrapMethods.data, 0, bootstrapMethods.length);
}
if (ClassReader.SIGNATURES && signature != 0) {
out.putShort(newUTF8("Signature")).putInt(2).putShort(signature);
}
if (sourceFile != 0) {
out.putShort(newUTF8("SourceFile")).putInt(2).putShort(sourceFile);
}
if (sourceDebug != null) {
int len = sourceDebug.length;
out.putShort(newUTF8("SourceDebugExtension")).putInt(len);
out.putByteArray(sourceDebug.data, 0, len);
}
if (enclosingMethodOwner != 0) {
out.putShort(newUTF8("EnclosingMethod")).putInt(4);
out.putShort(enclosingMethodOwner).putShort(enclosingMethod);
}
if ((access & Opcodes.ACC_DEPRECATED) != 0) {
out.putShort(newUTF8("Deprecated")).putInt(0);
}
if ((access & Opcodes.ACC_SYNTHETIC) != 0) {
if ((version & 0xFFFF) < Opcodes.V1_5
|| (access & ACC_SYNTHETIC_ATTRIBUTE) != 0) {
out.putShort(newUTF8("Synthetic")).putInt(0);
}
}
if (innerClasses != null) {
out.putShort(newUTF8("InnerClasses"));
out.putInt(innerClasses.length + 2).putShort(innerClassesCount);
out.putByteArray(innerClasses.data, 0, innerClasses.length);
}
if (ClassReader.ANNOTATIONS && anns != null) {
out.putShort(newUTF8("RuntimeVisibleAnnotations"));
anns.put(out);
}
if (ClassReader.ANNOTATIONS && ianns != null) {
out.putShort(newUTF8("RuntimeInvisibleAnnotations"));
ianns.put(out);
}
if (ClassReader.ANNOTATIONS && tanns != null) {
out.putShort(newUTF8("RuntimeVisibleTypeAnnotations"));
tanns.put(out);
}
if (ClassReader.ANNOTATIONS && itanns != null) {
out.putShort(newUTF8("RuntimeInvisibleTypeAnnotations"));
itanns.put(out);
}
if (attrs != null) {
attrs.put(this, null, 0, -1, -1, out);
}
if (invalidFrames) {
anns = null;
ianns = null;
attrs = null;
innerClassesCount = 0;
innerClasses = null;
bootstrapMethodsCount = 0;
bootstrapMethods = null;
firstField = null;
lastField = null;
firstMethod = null;
lastMethod = null;
computeMaxs = false;
computeFrames = true;
invalidFrames = false;
new ClassReader(out.data).accept(this, ClassReader.SKIP_FRAMES);
return toByteArray();
}
return out.data;
}
方法的后半部分就是给这个ByteVector开始填数据了,按照ClassFile的格式依次填入数据,和在ClassReader中读取的顺序一模一样,这样生成的Class结构就是符合虚拟机规范的,也能被虚拟机正常的加载。
那么这些数据是从哪里来的呢,聪明的读者现在肯定猜到了,就是前面这些抽象的visit系列方法,它们从原始Class文件中依次读取了数据然后又作为参数传了进来,我们先看第一个方法
@Override
public final void visit(final int version, final int access,
final String name, final String signature, final String superName,
final String[] interfaces) {
this.version = version;
this.access = access;
this.name = newClass(name);
thisName = name;
if (ClassReader.SIGNATURES && signature != null) {
this.signature = newUTF8(signature);
}
this.superName = superName == null ? 0 : newClass(superName);
if (interfaces != null && interfaces.length > 0) {
interfaceCount = interfaces.length;
this.interfaces = new int[interfaceCount];
for (int i = 0; i < interfaceCount; ++i) {
this.interfaces[i] = newClass(interfaces[i]);
}
}
}
这个visit方法是转发的第一个方法,其中的几个参数分别表示了原Class文件中的编译版本,访问标志(是否是final,static,abstract等等),类或接口名,泛型,父类以及实现的接口,可以看到这个方法只是单纯的赋了一下值,并没有什么其他的操作,这些值在最后生成新Class文件的时候会再次写入到byte数组中。
再看一个有点复杂的,ASM对于method的处理
@Override
public final MethodVisitor visitMethod(final int access, final String name,
final String desc, final String signature, final String[] exceptions) {
return new MethodWriter(this, access, name, desc, signature,
exceptions, computeMaxs, computeFrames);
}
直接代理给了 MethodWriter
类进行处理,继续跟进去构造函数
MethodWriter(final ClassWriter cw, final int access, final String name,
final String desc, final String signature,
final String[] exceptions, final boolean computeMaxs,
final boolean computeFrames) {
super(Opcodes.ASM5);
if (cw.firstMethod == null) {
cw.firstMethod = this;
} else {
cw.lastMethod.mv = this;
}
cw.lastMethod = this;
......
}
前面分析Class的文件结构的时候,方法表是一个数组类型,一个类中存在多个方法是很正常的,这个visitMethod同样会被调用多次,这里重点关注这个 firstMethod
和 cw.lastMethod.mv
,在ClassWriter类中其实并没有明显的数组类型来存放多方法结构,这也是ASM对于method和field写的比较模糊的一点,每一次的 visitMethod
的调用都是 new 一个 MethodWriter对象,第一次 cw.firstMethod == null
,于是给了 firstMethod
和 lastMethod
赋值当前,既有头又有尾,有些双向链表的意思,但是这里在第二次进来的时候走的是 cw.lastMethod.mv = this
,而 cw.lastMethod
指向的是刚才的 MethodWriter
对象,这就很清晰了,每一个 MethodWriter
的 mv 字段保留着下一个的引用,实际上只是单向的链表。
然后我们看一下在输出新的Class文件的时候
mb = firstMethod;
while (mb != null) {
mb.put(out);
mb = (MethodWriter) mb.mv;
}
刚才我们构造的 method 链表又重新的一个个的写进去了,而我们每一次的visitMethod 都会加长这个链表。
ASM就是通过这种方式来修改Class结构的,当我们想要加一个方法的时候,只需要多调用一次 visitMethod 方法,而当我们想删除其中一个方法的时候,只需要 return null,让这个MethodWrite 对象不会被构建即可。
ASM总结
ClassReader通过读取整个class文件,作为访问者模式中的元素类,而ClassWrite作为ClassVisitor的实现类,是一个特殊的具体访问者,通过一个accept
方法将两个连接起来,并在对应的visitXXX系列方法中决定最终元素的属性,而且元素的访问操作是可以进行链式转发的,这样我们既可以拥有ClassWrite的class文件生成能力,也能在自定义的visitor中实现我们想要对于class元素的处理。整体来说,ASM是一款十分优秀的字节码处理的框架,访问者模式十分适合对于class这样的格式基本不会改变的元素形式。
这一章主要是介绍ASM对于单个class文件的处理,对于一个完整的Java工程来说,我们想要处理的class文件可能是整个项目所编译出来的全部,后面会带来一篇hook编译过程来实现class处理的文章,两篇结合起来就能实现许多的功能,动态代理,无侵入式埋点,热修复框架都将不是遥不可及。