一、ASM库简介
ASM 库是一款基于 Java 字节码层面的代码分析和修改工具。它能分析二进制的 class 文件并对其进行动态修改。ASM 库侧重性能,设计和实现尽可能小和快。
通过 ASM 库,我们可以方便地获取类信息,并实现类似于反射的功能。ASM 库处理字节码,因此能得到仅仅使用反射无法获取的信息,如方法内的结构。
ASM 库同样具有动态修改和创建类文件的功能,但本篇文章主要使用 ASM 库的字节码读取功能。
本篇文章假设读者能自行通过Maven或其他方式添加ASM库依赖。
二、利用ASM库访问类文件
ASM 库的核心库提供了读取和修改字节码的基本 API。核心库包含如下几个工具类:
ClassReader
ClassReader
类用于从 class 文件中加载字节码。这样,这一 ClassReader
对象就拥有了关于某一类的所有信息。
ClassVisitor
ClassVisitor
类可以从 ClassReader
中获取想要的信息。
ClassVisitor
是访问者模式的访问类。简单解释访问者模式,就是被访问的对象调用访问者类的方法,从而使得访问者获取希望得到的数据。
还是通过 ASM 的例子来理解吧,ClassVisitor
实际是一个抽象类,其中定义了一个方法 visit
。该方法的签名为:
visit(int version, int access, String name, String signature, String superName, String[] interfaces)
假设我们继承 ClassVisitor
创建了一个新的访问类,那么获取信息的方式如下:
ClassReader cr = new ClassReader("className");
ClassVisitor cv = new ClassVisitor(Opcodes.ASM5) {
// this is an anonymous class
};
cr.accept(cv, 0); // second argument is the parsingOptions
如上代码中,ClassReader
调用 accept
方法允许一个 ClassVisitor
。在 accept
方法内部,会按一定解析规则调用 ClassVisitor
中的 visit
方法。这样我们就可以获得该类的类名、父类名、接口名等等信息。
ClassVisitor
类中还有许多访问方法可以重写,如visitAnnotation
、visitField
、visitMethod
等等。其中 visitField
、visitMethod
等方法的返回值类似 FieldVisitor
、MethodVisitor
等等 ,同样是访问者模式的抽象访问者类型。使用方法与 ClassVisitor
类似。
ClassWriter
ClassWriter
用于修改或创建 class 文件,虽然在本篇文章中不会使用,但因为是ASM核心库的组成部分,还是略微介绍一下。
ClassWriter
继承了 ClassVisitor
。因此也具有 visit
等方法。不同于 ClassVisitor
通过 ClassReader
调用访问函数,ClassWriter
应由程序员自行调用,并由此让 ClassWriter
构建起类结构。最后,调用 ClassWriter
的 toByteArray
方法生成字节码,通过文件流生成新的文件。
例子如下,这里不赘述
ClassWriter cw = new ClassWriter(0);
cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,"pkg/Comparable", null, "java/lang/Object",new String[]{"pkg/Mesurable"});
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS","I", null, new Integer(-1)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL","I", null, new Integer(0)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER","I", null, new Integer(1)).visitEnd();
cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo","(Ljava/lang/Object;)I", null, null).visitEnd();
cw.visitEnd();
byte[] b = cw.toByteArray();
//输出
FileOutputStream fileOutputStream = new FileOutputStream(new File("F:/asm/Comparable.class"));
fileOutputStream.write(b);
fileOutputStream.close();
示例出自java - ASM入门篇 - ksfzhaohui技术专栏 - SegmentFault 思否
三、利用ASM Tree实现反射
ASM 将各种不同功能的 API 组织在不同的库中。Tree库就是其中之一,能够将解析后的类信息组织成树的形式。即,类用ClassNode表示,ClassNode中包含类的相关信息,以及包含的字段,用FieldNode表示;和方法,用MethodNode表示。
首先,我们需要获取字节码。字节码可能是项目中已经被加载的文件,也可能是未被加载的其他文件。我们可以通过包括包名的类全称获取已加载的类的字节码,也可以通过文件流加载字节码。
var cr = new ClassReader("package.name.ClassName");
var in = new FileInputStream("fileName");
var cr = new CLassReader(in);
接着我们创建一个 ClassNode
实例作为访问和存储类信息的对象。并使用该对象获取类信息。
var cls = new ClassNode();
cr.accept(cls, 0);
接着我们就可以获取 ClassNode
中保存的类信息。ClassNode
的所有字段都是公共的。这样做是因为 ClassNode
类只作为保存数据的对象,而没有其他功能。因此使用公共字段是合理的。
// ClassNode中的一些字段:
public List<Attribute> attrs;
public List<InnerClassNode> innerClasses;
public String nestHostClass;
public List<String> nestMembers;
public List<String> permittedSubclasses;
public List<RecordComponentNode> recordComponents;
public List<FieldNode> fields;
public List<MethodNode> methods;
这样我们就利用 ASM Tree 库实现了类似反射的功能。
四、自定义访问器实现反射
在这一部分,我们自定义一个简单的访问器,实现对类、方法、字段信息的获取。以此为例演示 ASM 核心库的使用方法。
我们创建 MyClassVisitor
类继承 ClassVisitor
。
public class MyClassVisitor extends ClassVisitor
添加希望获取的字段
public String className;
public String superClassName;
public List<String> interfacesName = new ArrayList<>();
重写 visit
方法。这样我们就获取到了类名、父类名和接口名等信息。
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
{
super.visit(version, access, name, signature, superName, interfaces);
className = name;
superClassName = superName;
interfacesName.addAll(List.of(interfaces));
}
类似的,我们创建 MyMethodVisitor
和 MyFieldVisitor
类,并添加字段
public class MyMethodVisitor extends MethodVisitor {
public String methodName;
public String descriptor;
}
public class MyFieldVisitor extends FieldVisitor {
public String fieldType;
public String fieldName;
}
然后重写 ClassVisitor
的 visitMethod
和 visitField
。
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions)
{
var mv = new MyMethodVisitor(Opcodes.ASM5,
super.visitMethod(access, name, descriptor, signature, exceptions));
mv.methodName = name;
mv.descriptor = descriptor;
methods.add(mv);
return mv;
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value)
{
var fv = new MyFieldVisitor(Opcodes.ASM5,
super.visitField(access, name, descriptor, signature, value));
fv.fieldName = name;
fv.fieldType = descriptor;
fields.add(fv);
return fv;
}
这样就可以利用 MyMethodVisitor
获取 ClassReader
中的字节码信息了。
var cr = new ClassReader("org.objectweb.asm.ClassVisitor");
var cv = new MyClassVisitor(Opcodes.ASM5);
cr.accept(cv, 0);