字节码增强之ASM

5,344 阅读5分钟

1. 字节码

1.1 什么是字节码

Java之所以可以"一次编译,到处运行",一是因为JVM针对各种平台和操作系统都进行了定制,对开发者屏蔽了底层细节。二是因为无论在任何平台都会编译生成固定格式的字节码(.class)文件供JVM使用,不同平台上的JVM虚拟机都可以载入和执行同一种和平台无关的字节码。由此可见字节码对于Java生态的重要性。之所以被称为字节码,是因为字节码文件由16进制值组成,JVM以字节为单位进行读取。在Java中一般使用javac命令编译源代码为字节码文件,一个.java文件从编译到运行的示例如下图所示。

java运行示意图

1.2 字节码结构

.java文件通过javac编译后将得到.class文件,如编写一个简单的ByteCodeDemo类,如下图的左侧部分。编译后生成ByteCodeDemo.class文件,打开后是一堆16进制数,按字节为单位分割展示如下图右侧部分展示。

示例代码及对应的字节码

JVM规范要求每一个字节码文件都需要由10个部分按固定顺序组成,整体结果如下图所示。

字节码文件结构

2. ASM

ASM是一个Java字节码操控框架。它可以被用来动态生成类或者增强既有类的功能。ASM可以直接产生二进制的class文件,也可以在类被加载到Java虚拟机之前改变类行为。ASM的应用场景有AOP(cglib基于ASM)、热部署、修改其它jar包中的类等。ASM修改字节码流程如下图所示。

ASM修改字节码

2.1 ASM核心API

2.1.1 ClassReader

用于读取已经编译好的.class文件;

2.1.2 ClassWriter

用于重新构建编译后的类,如修改类名、属性及方法等;

2.1.3 ClassVistor

ASM内部采用访问者模式根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的Visitor。

  • 访问类文件时,会回调ClassVistor的visit方法;
  • 访问类注解时,会回调ClassVistor的visitAnnotation方法;
  • 访问类成员时,会回调ClassVistor的visitField方法;
  • 访问类方法时,会回调ClassVistor的visitMethod方法;
  • .......

当访问不同区域时会回调相应方法,该方法会返回一个对应的字节码操作对象。通过修改这个对象就可以修改class文件相应结构对应的内容。最后将这个对象的字节码内容覆盖原先的.class文件就可以实现字节码的切入。

2.2 ASM实现AOP示例

代码示例如下,原本Base的process方法只输出一行"process"。

目标:我们期望通过字节码增强后,在方法执行前输出"start",在方法执行后输出"end"。

public class Base {
    public void process() {
        System.out.println("process");
    }
}

要实现在process方法前后插入输出代码,需要有以下步骤:

  • (1)读取Base.class文件,可以通过ClassReader进行class文件的读取;
  • (2)构造System.out.println(String)的字节码,通过ClassVisitor将额外的字节码嵌入合适地方;
  • (3)在ClassVisitor处理完成后,由ClassWriter将新的字节码替换掉旧的字节码;

基本实现代码参考如下,SimpleGenerator类定义了ClassReader和ClassWriter对象。其中ClassReader负责读取字节码,然后交给ClassVisitor类处理,处理完成后由ClassWriter完成字节码文件替换。

public class SimpleGenerator {

    public static void main(String[] args) throws Exception {
        //读取类文件
        ClassReader classReader = new ClassReader("com.example.aop.asm.Base");
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        //处理,通过classVisitor修改类
        ClassVisitor classVisitor = new SimpleClassVisitor(classWriter);
        classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
        byte[] data = classWriter.toByteArray();
        //保存新的字节码文件
        File file = new File("target/classes/com/example/aop/asm/Base.class");
        FileOutputStream outputStream = new FileOutputStream(file);
        outputStream.write(data);
        outputStream.close();
        System.out.println("generator new class file success.");
    }
}

SimpleClassVisitor类继承ClassVisitor类。ClassVisitor中各方法按以下顺序调用,这个顺序在ClassVisitor的java doc中也有说明。

[ visitSource ] [ visitOuterClass ] ( visitAnnotation | visitTypeAnnotation | visitAttribute )* (visitInnerClass | visitField | visitMethod )* visitEnd.

其中visitMethod在访问方法时调用,我们可以通过扩展MethodVisitor方法来实现增强逻辑。类似地,MethodVisitor也需要按顺序调用其方法。

( visitParameter )* [ visitAnnotationDefault ] ( visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation visitTypeAnnotation | visitAttribute )* [ visitCode ( visitFrame | visitXInsn | visitLabel | visitInsnAnnotation | visitTryCatchBlock | visitTryCatchAnnotation | visitLocalVariable | visitLocalVariableAnnotation | visitLineNumber )* visitMaxs ] visitEnd.

public class SimpleClassVisitor extends ClassVisitor {

    public SimpleClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM5, classVisitor);
    }

    // visitMethod在访问类方法时回调,可以获知当前访问到方法信息
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature,
                exceptions);
        // 不需要增强Base类的构造方法
        if (mv != null && !name.equals("<init>")) {
            // 通过定制MethodVisitor,替换原来的MethodVisitor对象来实现方法改写
            mv = new SimpleMethodVisitor(mv);
        }
        return mv;
    }


    /**
     * 定制方法visitor
     */
    class SimpleMethodVisitor extends MethodVisitor {

        public SimpleMethodVisitor(MethodVisitor methodVisitor) {
            super(Opcodes.ASM5, methodVisitor);
        }

        // visitCode方法在ASM开始访问方法的Code区时回调
        @Override
        public void visitCode() {
            super.visitCode();
            // System.out.println("start")对应的字节码,在visitCode访问方法之后添加
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("start");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }

        // 每当ASM访问到无参数指令时,都会调用visitInsn方法
        @Override
        public void visitInsn(int opcode) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
                // System.out.println("end")对应的字节码,在visitInsn方法返回前添加
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("end");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

            }
            super.visitInsn(opcode);
        }

    }
}

运行SimpleGenerator类的main方法即可完成对Base类的字节码增强。增强后的class文件通过反编译后结果如下图所示。可以看到对应的代码已经改变,在其前后增加了"start"和"end"的输出。

字节码增强后的Base类

可以通过ASM ByteCode Outline 插件很方便地实现源码到字节码的映射。

3. 参考资料

Java字节码增强探秘


欢迎关注我的公众号~