ASM的基础用法

本文转载自ASM的基础用法

导语

新闻里使用的热补丁修复方案是基于AspectJ,AspectJ是AOP的一种实现。

无意接触到一种小巧轻便的Java字节码操控框架ASM,它也能方便地生成和改造Java代码。

本文主要分为几个部分:

  1. 什么是ASM;
  2. 为什么要动态生成Java类;
  3. 为什么选择ASM;
  4. ASM中的核心类和核心方法;
  5. ASM示例;

什么是ASM?

ASM是一个Java字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM可以直接产生二进制 class文件,也可以在类被加载入Java虚拟机之前动态改变类行为。

如果想了解Java虚拟机的工作过程可参考JVM原理浅析

为什么要动态生成Java类?

举个例子,目前有一个既有的银行管理系统,包括Bank、Customer、Account、Invoice等对象,现在要加入一个安全检查模块,对已有类的所有操作之前都必须进行一次安全检查。

img

然而 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类的字节码

img

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

使用方式:

  1. 生成对应的.class文件;
  2. Code-Show Bytecode Outline

我们写一个代码示例:

public class AccountASM {

    public void operation() {
        SecurityChecker.checkSecurity();
        System.out.println("operation...");
    }
}

通过上述方式查看ASM代码

img

红框的代码是用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接口文档说明

img

对接口中方法的调用必须遵守以下规则:

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

参考

  1. AOP 的利器:ASM 3.0 介绍
  2. 深入字节码 – 使用 ASM 实现 AOP
  3. 深入字节码 – ASM 关键接口 ClassVisitor
  4. 深入字节码 – ASM 关键接口 MethodVisitor
  5. ASM 操作字节码初探
  6. ASM3.0使用指南(中文)
  7. ASM4.0使用指南(英文)
posted @ 2020-06-21 13:30  Yungyu  阅读(3208)  评论(0编辑  收藏  举报