ASM的基础用法
本文转载自ASM的基础用法
导语
新闻里使用的热补丁修复方案是基于AspectJ,AspectJ是AOP的一种实现。
无意接触到一种小巧轻便的Java字节码操控框架ASM,它也能方便地生成和改造Java代码。
本文主要分为几个部分:
- 什么是ASM;
- 为什么要动态生成Java类;
- 为什么选择ASM;
- ASM中的核心类和核心方法;
- ASM示例;
什么是ASM?
ASM是一个Java字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM可以直接产生二进制 class文件,也可以在类被加载入Java虚拟机之前动态改变类行为。
如果想了解Java虚拟机的工作过程可参考JVM原理浅析
为什么要动态生成Java类?
举个例子,目前有一个既有的银行管理系统,包括Bank、Customer、Account、Invoice等对象,现在要加入一个安全检查模块,对已有类的所有操作之前都必须进行一次安全检查。
然而 Bank、Customer、Account、Invoice 是代表不同的事务,派生自不同的父类,很难在高层上加入关于Security Checker的共有功能。对于没有多继承的Java来说,更是如此。
传统解决方案是使用装饰器模式,装饰器模式动态的将责任链附加到对象上,若要扩展功能,装饰者提供了比继承更加富有弹性的代替方案。
装饰器模式可以在一定程度上改善耦合,而功能仍旧是分散的,每个需要Security Checker的类都必须派生一个Decorator,每个需要Security Checker的方法都要被包装(wrap)。
下面我们以 Account类为例看一下Decorator:
首先,有一个SecurityChecker类,其静态方法checkSecurity执行安全检查功能:
public class SecurityChecker {
public static void checkSecurity() {
System.out.println("SecurityChecker.checkSecurity ...");
}
}
另一个是Account类:
public class Account {
public void operation() {
System.out.println("operation...");
}
}
若想对operation加入对SecurityCheck.checkSecurity()调用,标准的Decorator需要先定义一个 Account类的接口:
public interface IAccount {
void operation();
}
然后定义一个实现类:
public class AccountImpl implements IAccount {
@Override
public void operation() {
System.out.println("operation...");
}
}
定义一个AccountImpl类的Decorator,并包装operation方法:
public class AccountWithSecurityCheck implements IAccount {
private IAccount account;
public AccountWithSecurityCheck(IAccount IAccount) {
this.account = IAccount;
}
public void operation() {
SecurityChecker.checkSecurity();
account.operation();
}
}
最后的调用方式为:
public class Test {
public static void main(String[] args) throws Exception {
// 1.使用包装类
AccountWithSecurityCheck account = new AccountWithSecurityCheck(new AccountImpl());
account.operation();
}
}
在这个简单的例子里,改造一个类的一个方法还好,如果是变动整个模块,Decorator很快就会演化成另一个噩梦。动态改变Java类就是要解决AOP的问题,提供一种得到系统支持的可编程的方法,自动化地生成或者增强Java代码。
为什么选择ASM?
最直接的改造Java类的方法莫过于直接改写class文件。Java 规范详细说明了class文件的格式,直接编辑字节码确实可以改变Java类的行为。
还有一种比较理想且流行的方式是是使用java.lang.reflect.Proxy。我们仍旧使用以上的例子,给Account类加上checkSecurity功能。
首先,Proxy编程是面向接口的,Proxy并不负责实例化对象,和Decorator模式一样,要把Account定义成一个接口,然后在AccountImpl里实现Account接口,接着实现一个InvocationHandlerAccount方法被调用的时候,虚拟机都会实际调用这个InvocationHandler的invoke方法:
public class SecurityProxyInvocationHandler implements InvocationHandler {
private Object proxyedObject;
public SecurityProxyInvocationHandler(Object o) {
proxyedObject = o;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (proxy instanceof IAccount && "operation".equals(method.getName())) {
SecurityChecker.checkSecurity();
}
return method.invoke(proxyedObject, args);
}
}
最后,在应用程序中指定InvocationHandler生成代理对象:
public class Test {
public static void main(String[] args) throws Exception {
// 2.使用代理
IAccount account = (IAccount) Proxy.newProxyInstance(
IAccount.class.getClassLoader(),
new Class[]{IAccount.class},
new SecurityProxyInvocationHandler(new AccountImpl()));
account.operation();
}
}
其不足之处在于:
- Proxy是面向接口的,所有使用Proxy的对象都必须定义一个接口,而且用这些对象的代码也必须是对接口编程的:Proxy生成的对象是接口一致的而不是对象一致的:例子中Proxy.newProxyInstance生成的是实现IAccount接口的对象而不是AccountImpl的子类。这对于软件架构设计,尤其对于既有软件系统是有一定掣肘的。
- Proxy毕竟是通过反射实现的,必须在效率上付出代价:有实验数据表明,调用反射比一般的函数开销至少要大10倍。而且,从程序实现上可以看出,对proxy class的所有方法调用都要通过使用反射的invoke方法。因此,对于性能关键的应用,使用proxy class是需要精心考虑的,以避免反射成为整个应用的瓶颈。
ASM能够通过改造既有类,直接生成需要的代码。增强的代码是硬编码在新生成的类文件内部的,没有反射带来性能上的付出。同时,ASM与Proxy编程不同,不需要为增强代码而新定义一个接口,生成的代码可以覆盖原来的类,或者是原始类的子类。它是一个普通的Java类而不是Proxy类,甚至可以在应用程序的类框架中拥有自己的位置,派生自己的子类。
ASM使用
使用javap -c命令查看Account类的字节码
5-9行表示的是一个的默认构造方法,是编译器为我们自动添加的。参考来自深入字节码 – 使用 ASM 实现 AOP
11-结束表示我们编写的operation方法。
aload_0:这个指令是LOAD系列指令中的一个,它的意思表示装载当前第0个元素到堆栈中。代码上相当于“this”。
invokespecial:这个指令是调用系列指令中的一个。其目的是调用对象类的方法。后面需要给上父类的方法完整签名。“#1”的意思是class文件常量表中第1个元素。值为:“java/lang/Object.””😦)V”。结合aload_0。这两个指令可以翻译为:“super()”。其含义是调用自己的父类构造方法。
getstatic:这个指令是GET系列指令中的一个,其作用是获取静态字段内容到堆栈中。
ldc:从常量表中装载一个数据到堆栈中。
invokevirtual:也是一种调用指令,这个指令区别与invokespecial的是它是根据引用调用对象类的方法。
return:也是一系列指令中的一个,其目的是方法调用完毕返回:可用的其他指令有:IRETURN,DRETURN,ARETURN等,用于表示不同类型参数的返回。
invokespecial和invokevirtual的主要区别在于: invokespecial通常根据引用的类型选择方法,而不是根据对象的类来选择。
IntelliJ中ASM的插件
ASM Bytecode Outline
使用方式:
- 生成对应的.class文件;
- Code-Show Bytecode Outline
我们写一个代码示例:
public class AccountASM {
public void operation() {
SecurityChecker.checkSecurity();
System.out.println("operation...");
}
}
通过上述方式查看ASM代码
红框的代码是用ASM输出整个operation方法字节码:
第36行:表示准备输出一个公有方法“operation”,ACC_PUBLIC表示公有,相当于public修饰符;“()V”是方法的参数包括返回值签名,“V”是void的缩写,表示无返回值;后面两个null分别是方法的异常抛出信息和属性信息。
第37行:表示开始正式输出方法的执行代码。
第41行:表示调用静态方法,这行代码相当于“SecurityChecker.checkSecurity();”。
上面的38,39,40,42,43,44,48,49,50行看到的内容表示Java代码的行号标记,可以删除不用。
在方法的最后部分代码52,53,54行表示向class文件中写入方法本地变量表的名称以及类型,可以删除不用。
所以精简的代码如下:
{
mv = cw.visitMethod(ACC_PUBLIC, "operation", "()V", null, null);
mv.visitCode();
mv.visitMethodInsn(INVOKESTATIC, "com/xiongcen/asm/SecurityChecker", "checkSecurity", "()V", false);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("operation...");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
}
01行:相当于public void operation()方法声明;
02行:正式开始方法内容的填充;
03行:调用静态方法,相当于“SecurityChecker.checkSecurity();”;
04行:取得一个静态字段将其放入堆栈,相当于“System.out”。“Ljava/io/PrintStream;”是字段类型的描述,翻译过来相当于:“java.io.PrintStream”类型。在字节码中凡是引用类型均由“L”开头“;”结束表示,中间是类型的完整名称;
05行:将字符串“operation…”放入堆栈,此时对战中第一个元素是“System.out”,第二个元素是”operation…”
06行:调用PrintStream类型的“println”方法。签名“(Ljava/lang/String;)V”表示方法需要一个字符串类型的参数,并且无返回值。
07行:是JVM在编译时为方法自动加上的“return”指令。该指令必须在方法结束时执行不可缺少。
08行:表示在执行这个方法期间方法的堆栈空间最大给予多少。
09行:表示方法输出结束。
ASM框架中的核心类
ClassVisitor接口:定义在读取Class字节码时会触发的事件,如类头解析完成、注解解析、字段解析、方法解析等。每当有事件发生时,调用注册的ClassVisitor、AnnotationVisitor、FieldVisitor、MethodVisitor做相应的处理。
AnnotationVisitor接口:定义在解析注解时会触发的事件,如解析到一个基本值类型的注解、enum值类型的注解、Array值类型的注解、注解值类型的注解等。
FieldVisitor接口:定义在解析字段时触发的事件,如解析到字段上的注解、解析到字段相关的属性等。
MethodVisitor接口:定义在解析方法时触发的事件,如方法上的注解、属性、代码等。
ClassVisitor接口文档说明
对接口中方法的调用必须遵守以下规则:
visit-visitSource方法(一次)-visitOuterClass(一次)-visitAnnotation|visitAttribute(任意)-visitInnerClass | visitField | visitMethod(任意)-visitEnd.
ClassVisitor的关键方法
参数含义请参考深入字节码 – ASM 关键接口 ClassVisitor
1.visit(int version, int access, String name, String signature, String superName, String[] interfaces),该方法是当扫描类时第一个调用的方法,主要用于类的声明。
visit(类版本,修饰符,类名,泛型信息,继承的父类,实现的接口)
2.visitAnnotation(String desc, boolean visible),该方法是当扫描器扫描到类注解声明时进行调用。
visitAnnotation(注解类型,注解是否可以在JVM中可见)
3.visitField(int access, String name, String desc, String signature, Object value),该方法是当扫描器扫描到类中字段时进行调用。
visitField(修饰符,字段名,字段类型,泛型描述,默认值)
4.visitMethod(int access, String name, String desc, String signature, String[] exceptions),该方法是当扫描器扫描到类的方法时进行调用。
visitMethod(修饰符,方法名,方法签名,泛型信息,抛出的异常)。方法签名的格式如下:“(参数列表)返回值类型”。参考签名ASM 操作字节码初探
5.visitEnd(),该方法是当扫描器完成类扫描时才会调用。
ASM的三大组件
ClassReader类:该类用来解析编译过的class字节码文件。
ClassWriter类:该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。
ClassAdapter类:实现了ClassVisitor接口所定义的所有函数,当新建一个ClassAdapter对象的时候,需要传入一个实现了ClassVisitor接口的对象,作为职责链中的下一个访问者(Visitor),这些函数的默认实现就是简单的把调用委派给这个对象,然后依次传递下去形成职责链。
使用ASM增强既有类的功能
我们还是用上面的例子,给Account类加上Security check 的功能。与Proxy编程不同,ASM不需要将 Account声明成接口,Account可以仍旧是一个实现类。ASM将直接在Account类上动手术,给Account类的operation方法首部加上对SecurityChecker.checkSecurity的调用。
首先,我们将从ClassAdapter继承一个类。ClassAdapter是ASM框架提供的一个默认类,负责沟通 ClassReader和ClassWriter。如果想要改变ClassReader处读入的类,然后从ClassWriter处输出,可以重写相应的ClassAdapter函数。这里,为了改变Account类的operation方法,我们将重写visitMethdod方法。
public class AddSecurityCheckClassAdapter extends ClassAdapter {
public AddSecurityCheckClassAdapter(ClassVisitor cv) {
super(cv);
}
// 重写 visitMethod,访问到 "operation" 方法时,给出自定义 MethodVisitor,实际改写方法内容
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
System.out.println("AddSecurityCheckClassAdapter;" + "name:" + name + ";desc:" + desc + ";signature:" + signature);
MethodVisitor wrappedMv = mv;
if (mv != null) {
if ("operation".equals(name)) {
// 使用自定义的MethodVisitor,实际改写方法内容
wrappedMv = new AddSecurityCheckMethodAdapter(mv);
}
}
return wrappedMv;
}
}
下一步就是定义一个继承自MethodAdapter的AddSecurityCheckMethodAdapter,在“operation”方法首部插入对SecurityChecker.checkSecurity()的调用。
public class AddSecurityCheckMethodAdapter extends MethodAdapter {
public AddSecurityCheckMethodAdapter(MethodVisitor mv) {
super(mv);
}
@Override
public void visitCode() {
// ClassReader读到每个方法的首部时调用 visitCode(),在这个重写方法里,
// 我们用 visitMethodInsn(Opcodes.INVOKESTATIC, "com/xiongcen/asm/SecurityChecker","checkSecurity", "()V");插入了安全检查功能。
visitMethodInsn(Opcodes.INVOKESTATIC, "com/xiongcen/asm/SecurityChecker",
"checkSecurity", "()V");
}
}
最后,我们将集成上面定义的ClassAdapter,ClassReader和ClassWriter产生修改后的Account类文件 :
public static void main(String[] args) throws Exception {
// 3.使用ASM
// 使用 ClassReader 去读取 Account 类的字节码信息。
ClassReader cr = new ClassReader("com.xiongcen.asm.Account");
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);
// 通过accept方法扫描整个字节码,SKIP_DEBUG选项的意义是在扫描过程中掠过所有有关行号方面的内容
cr.accept(classAdapter, ClassReader.SKIP_DEBUG);
byte[] data = cw.toByteArray();
File file = new File("/Users/xiongcen/Documents/IdeaProject/JniProject/out/production/JniProject/com/xiongcen/asm/Account.class");
FileOutputStream fout = new FileOutputStream(file);
fout.write(data);
fout.close();
Account account = new Account();
account.operation();
}
使用这个 Account,我们会得到下面的输出:
SecurityChecker.checkSecurity ...
operation...
源码可参考ASMDemo