ASM工具的使用
简介
ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。
Java class 被存储在严格格式定义的 .class 文件里。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。说白了asm是直接通过字节码来修改class文件。
Spring中的cglib和jdk的动态代理,最底层使用的都是ASM,ASM主要就是使用visitor模式,直接先读取原来的文件,相当于reader,然后通过adapter,自己定制一套自己的写法,最后再写入到一个新的文件中,即相当于改变了原有代码,增加了切面,这就是动态代理,
这列再简单提一下访问者模式:
访问者模式(VisitorPattern),可以在不修改已有程序结构的前提下,通过添加额外的访问者来完成对已有代码功能的提升,它属于行为模式。其主要目的是将数据结构与数据操作分离
访问者模式主要由五个角色组成:
抽象访问者(Visitor)角色:声明了一个或者多个方法操作,形成所有的具体访问者角色必须实现的接口。,必须定义 accept(PartA),accept(PartB),accept(PartAll)等所有访问数据的方法
具体访问者(ConcreteVisitor)角色:实现抽象访问者所声明的接口,也就是抽象访问者所声明的各个访问操作。比如 VisitorA,VisitorB,只会实现访问对应的数据
抽象节点(Node)角色:声明一个接受操作,接受一个访问者对象作为一个参数。
具体节点(ConcreteNode)角色:实现了抽象节点所规定的接受操作。,比如 PartA,PartB
结构对象(ObjectStructure)角色:有如下的责任,可以遍历结构中的所有元素。PartAll
注意:访问者对象,适合part稳定不变的情况下,只需要新增Visitor即可
基本使用
1. 读取 ClassReader
- 构造器
ClassReader有四种构造器
比如:
// 根据类的全限定名获取
final ClassReader classReader = new ClassReader("com/hou/api/controller/TcmUserController");
- accept方法
accept是主要使用的方法,主要有两个:
第一个参数是传入的访问者对象,第二个int是读取模式,有四个值:
2. 访问-ClassVisitor
ClassVisitor是访问者模式的抽象接口,接口中定义了访问class各个部分的方法,比如访问方法,访问注解等,方法如下:
构造器只需要ASM API版本(在Opcodes中可以找到,1-9),或者再加上另一个ClassVisitor用于一起解析
当Visitor被传入accept之后,ClassReader会按顺序调用, 即调用所有的访问方法,同时会根据构造器传入的读取模式判断是否执行,如源代码:
当所有信息都访问结束,调用visitEnd,,其中防范字段或者注解等,会使用其他访问者抽象类的实现,比如AnnotationVisitor等,这里的具体调用,后面再分析
3. 访问注解-AnnotationVisitor
AnnotationVisitor是用于解析注释信息的抽象类,主要定义了四个访问方法:
- visit:传入注释方法名称和值,值必须是基本类型(基本数字、char及其数组,String和类)
- visitArray:传入注释方法名称,返回另一个AnnotationVisitor。这个新的Visitor会被传入数组内的值,所有的name传入都为null。
- visitAnnotation:传入注释方法名称和值的描述符,返回的是值的AnnotationVisitor。
- visitEnum:传入注释方法名、值的描述符和枚举名称。
4. 访问变量-FieldVisitor
FieldVisitor是用来访问字段的抽象类,定义的方法比较简单(他就类似只能访问某个部分的访问者,classvisitor可以访问所有部分),除了visitEnd在最后调用外,比较常用的就是visitAnnotation和visitTypeAnnotation。这些方法的使用都和ClassVisitor的使用差不多,
5. 访问方法-MethodVisitor
这个是非常重要的一个访问者,放到本文后面说
使用案例
上面看了那么多访问者模式定义的顶层接口和抽象类,那么这玩意到底有啥用,或者具体是如何使用的,下面就来写几个例子:
1. 解析一个类
原始类:
@Component("test")
public class MyLift {
private String name;
private static Integer age=27;
}
- 首先需要实现访问者接口,ClassVisitor,同时选择自己需要的解析信息,去覆写对应的方法,同时,在有返回值的方法,需要返回一个具体访问者的实例,比如AnnotationVisitor,所以还需要去继承此类并实现
public class MyClassVisitor extends ClassVisitor {
//构造器必须要传入 ASM版本
public MyClassVisitor(int api) {
super(api);
}
/**
* 因为我们需要访问类的名称,字段和注解,所以需要覆写以下三个方法
*/
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
System.out.println("类名:"+name);
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
System.out.println("注解:"+descriptor);
// 这里需要返回我们自定义的注解访问者,由它去解析注解
return new MyAnnotationVisitor(Opcodes.ASM9);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
System.out.println("字段:"+name+", 值"+value);
// 这里需要返回我们的字段访问者,由它去解析字段
return new MyFieldVisitor(Opcodes.ASM9);
}
}
- 自定义具体的字段和注解访问者实例
/**
* 如果没有额外操作,可以暂时不覆写方法
*/
public class MyAnnotationVisitor extends AnnotationVisitor {
public MyAnnotationVisitor(int api) {
super(api);
}
// 覆写注解访问的对应方法
@Override
public void visit(String name, Object value) {
System.out.println("注解name:"+name);
System.out.println("注解value:"+value);
super.visit(name, value);
}
}
public class MyFieldVisitor extends FieldVisitor {
public MyFieldVisitor(int api) {
super(api);
}
}
- 执行解析
2. 生成一个类
-
在生成类之前,需要了解到访问标志:
访问标志是用于JVM访问类、字段、方法检查和调用的一个int。这些标志既包含了我们常见的public这种访问限定符,还包含了static、final这种修饰符,除此之外还有声明类为接口的interface,为枚举的enum
所有标志都在Opcodes类中定义为常量(值都是用16进制表示), 常用的访问标志如:- 访问限定(选择其中一个或无):ACC_PUBLIC,ACC_PRIVATE,ACC_PROTECTED
- 类声明(选择一个):ACC_INTERFACE,int ACC_ENUM
- 类修饰(选择一个或没有):ACC_FINAL(类为enum必选),ACC_ABSTRACT(类为interface必选)
- 方法修饰(除了冲突外可以任选):static,final,abstract(在接口或抽象类里面使用,与static,final,native等冲突),synchronized,strict(关键词是strictfp,精度保留,只是口头保证罢了),native(本地方法,JNI调用)
注意:这些常量可以用or叠加修饰,如果访问标志不合法(比如吧ACC_PUBLIC和ACC_PRIVATE用or联系起来当了访问标志),在ASM写入时是不会报错的,但是在JVM试图加载这个类的时候可能会抛出ClassFormatError
-
生成类我们用到的是ClassWriter,它本质上就是ClassVisitor,我们只要用可以构建类的数据按照刚才的格式传给它就能生成对应的类
public class AsmTest {
public static void main(String[] args) throws IOException {
// 生成类
final ClassWriter classWriter = new ClassWriter(0);
// 生成类,使用visit方法,参数对应: jdk版本,访问标志,类的全限定名, 泛型,父类全限定名,接口
classWriter.visit(V1_8, ACC_PROTECTED + ACC_FINAL,"com/hou/api/controller/MyLift1",null,"java/lang/Object",null);
//javac编译时会把没有定义构造函数的普通类加入默认的构造函数,这里先不生成方法和构造器
// 生成类的注解
classWriter.visitAnnotation("org/springframework/stereotype/Component",true);
// 写入字段, 生成一个 public final static
FieldVisitor fv = classWriter.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "name", "Ljava/lang/String;",null, "houzheng");
fv.visitEnd();
classWriter.visitEnd(); // 生成需要调用结束方法
final byte[] bytes = classWriter.toByteArray();
// 写入文件后反编译查看,注意:生成的是class文件
fileToBytes(bytes,"D:/","MyLife1.class");
}
反编译查看:
3. 修改类
修改类需要ClassReader和ClassWriter互相配合。利用ClassVisitor等进行数据的转移和修改
public class AsmTest {
public static void main(String[] args) throws IOException {
ClassWriter classWriter = new ClassWriter(0);
ClassReader classReader = new ClassReader("com/hou/api/controller/MyLift");
//MyClassVisitor 构造器传入一个ClassWriter,这样,ClassReader传入的信息可以直接写到ClassWriter里面,我们只需要修改我们所需要的方法就可以达到修改的效果,而不用将所有ClassVisitor的方法实现
classReader.accept(new MyClassVisitor(Opcodes.ASM9,classWriter),ClassReader.EXPAND_FRAMES);
// 将读取到并修改后的文件输出写入文件
final byte[] bytes = classWriter.toByteArray();
// 写入文件后反编译查看,注意:生成的是class文件
fileToBytes(bytes,"D:/","MyLife2.class");
}
反编译查看: