美文网首页
ASM——运行时/编译时动态修改class源码

ASM——运行时/编译时动态修改class源码

作者: FENGAO | 来源:发表于2019-05-08 00:11 被阅读0次

    简述

    最近在看阿里的ARouter的源码,从git上clone下来之后,run起来发现项目运行的效果和源码有明显区别。打个比方,源码是这样

    boolean b = true;
    System.out.println(b);
    

    但是当你跑起来之后去发现打印出来的false,打开编译好的class文件却发现编译出来的class的代码和源码不一样。经过翻看ARouter的工程源码,发现其实ARouter是利用了Gradle的 Transform API和ASM共同完成的编译时修改源码的功能。

    Transform API的功能是让你在java文件编译成class文件之后对这些class文件进行读写,发生在编译时,是Android的gradle打包插件自带的功能,这里不详细展开。本片文章主要是讲解ASM的基本使用方法。有机会会出一个Transform + ASM插件教程。

    ASM简介

    ASM 是一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。许多AOP框架以及动态修改字节码的库的底层都是由ASM实现的,例如Spring AOP,cglib等等

    一句话概括:ASM可以动态的修改创建class文件,达到动态修改java代码的效果。

    ASM 核心API

    public abstract class ClassVisitor {
      // 实现的ASM的API版本。该字段的值必须为如下几个之一:Opcodes.ASM4,ASM5,ASM6,ASM7
      protected final int api;
      // 该类的方法可以委托给子类
      protected ClassVisitor cv;
      // 构造器 
      public ClassVisitor(final int api) {
        this(api, null);
      }
      // 构造器
      public ClassVisitor(final int api, final ClassVisitor classVisitor) {
        if (api != Opcodes.ASM6 && api != Opcodes.ASM5 && api != Opcodes.ASM4 && api != Opcodes.ASM7) {
          throw new IllegalArgumentException();
        }
        this.api = api;
        this.cv = classVisitor;
      }
      /**
      * 访问类头部信息
      *
      * @param version
      *            类版本
      * @param access
      *            类访问标识符public等
      * @param name
      *            类名称
      * @param signature
      *            类签名(非泛型为NUll)
      * @param superName
      *            类的父类
      * @param interfaces
      *            类实现的接口
      */
      public void visit(
          final int version,final int access,
          final String name, final String signature,
          final String superName,final String[] interfaces) {
        if (cv != null) {
          cv.visit(version, access, name, signature, superName, interfaces);
        }
      }
      /**
      * 访问类的源文件.
      *
      * @param source
      *            源文件名称
      * @param debug
      *            附加的验证信息,可以为空
      */
      public void visitSource(final String source, final String debug) {
        if (cv != null) {
          cv.visitSource(source, debug);
        }
      }
    
      /**
      * 访问与类对应的模块. ASM6之后才有的API
      *
      * @param name
      *            模块名称
      * @param access
      *            模式 ACC_MANDATED 等
      * @param version
      *            版本号
      */
      public ModuleVisitor visitModule(final String name, final int access, final String version) {
        if (api < Opcodes.ASM6) {
          throw new UnsupportedOperationException("This feature requires ASM6");
        }
        if (cv != null) {
          return cv.visitModule(name, access, version);
        }
        return null;
      }
    
      public void visitNestHost(final String nestHost) {
        if (api < Opcodes.ASM7) {
          throw new UnsupportedOperationException("This feature requires ASM7");
        }
        if (cv != null) {
          cv.visitNestHost(nestHost);
        }
      }
      /**
      * 这个其实并不是访问外部类的回调,而是访问方法体中含有匿名内部类的方法
      *
      * @param owner 为创建匿名类的类,当然其也是一个enclosing class类型的类
      * @param name 创建匿名类的方法。
      * @param desc 创建匿名类的方法描述信息。
      * @return 返回一个注解值访问器
      */
      public void visitOuterClass(final String owner, final String name, final String descriptor) {
        if (cv != null) {
          cv.visitOuterClass(owner, name, descriptor);
        }
      }
      /**
      * 访问类的注解
      *
      * @param desc
      *            注解类的类描述
      * @param visible
      *            runtime时期注解是否可以被访问
      * @return 返回一个注解值访问器
      */
      public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) {
        if (cv != null) {
          return cv.visitAnnotation(descriptor, visible);
        }
        return null;
      }
      /**
      * 访问标注在类型上的注解
      *
      * @param typeRef
      * @param typePath
      * @param desc
      * @param visible
      * @return
      */
      public AnnotationVisitor visitTypeAnnotation(
          final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) {
        if (api < Opcodes.ASM5) {
          throw new UnsupportedOperationException("This feature requires ASM5");
        }
        if (cv != null) {
          return cv.visitTypeAnnotation(typeRef, typePath, descriptor, visible);
        }
        return null;
      }
      /**
      * 访问一个类的属性
      *
      * @param attribute
      *            类的属性
      */
      public void visitAttribute(final Attribute attribute) {
        if (cv != null) {
          cv.visitAttribute(attribute);
        }
      }
    
      public void visitNestMember(final String nestMember) {
        if (api < Opcodes.ASM7) {
          throw new UnsupportedOperationException("This feature requires ASM7");
        }
        if (cv != null) {
          cv.visitNestMember(nestMember);
        }
      }
      /**
      * 访问内部类信息
      * @param name
      * @param outerName
      * @param innerName
      * @param access
      */
      public void visitInnerClass(
          final String name, final String outerName, final String innerName, final int access) {
        if (cv != null) {
          cv.visitInnerClass(name, outerName, innerName, access);
        }
      }
      /**
      * 访问类的字段
      * @param access
      * @param name
      * @param desc
      * @param signature
      * @param value
      * @return
      */
      public FieldVisitor visitField(
          final int access,
          final String name,
          final String descriptor,
          final String signature,
          final Object value) {
        if (cv != null) {
          return cv.visitField(access, name, descriptor, signature, value);
        }
        return null;
      }
      /**
      * 访问类的方法
      * @param access
      * @param name
      * @param desc
      * @param signature
      * @param exceptions
      * @return
      */
      public MethodVisitor visitMethod(
          final int access,
          final String name,
          final String descriptor,
          final String signature,
          final String[] exceptions) {
        if (cv != null) {
          return cv.visitMethod(access, name, descriptor, signature, exceptions);
        }
        return null;
      }
    
     /**
      * 访问类结束
      */
      public void visitEnd() {
        if (cv != null) {
          cv.visitEnd();
        }
      }
    }
    

    ClassVisitor 的调用必须是遵循下面的调用顺序的:

     visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*
    ( visitInnerClass | visitField | visitMethod )*
    visitEnd
    

    围绕着ClassVisitor ,还有两个核心类: 后续的例子代码中可以看到,我们必须先调用visit方法,这就因为class是字节流的二进制文件,而我们解析和生成也是要遵循一定的顺序。ClassVisitor定义了我们需要操作的所有接口,并且ClassVisitor也可以接收一个ClassVisitor实例来构造,有点类似于一个事件的filter,可以套很多层的filter来一层层处理逻辑。

    1、ClassReader 将class解析成byte 数组,然后会通过accept方法去按顺序调用绑定对象(继承了ClassVisitor的实例)的方法。可以视为一个事件的生产者。

    2、ClassWriter 是ClassVisitor 的子类。直接可以通过toByteArray()方法以返回的byte数组形式构建编译后的class。可以视为一个事件的消费者。

    但是需要注意的是虽然ClassReader和ClassWriter 看起来像是对称类例如InputStream和OutputStream但其实类结构上并无关联,ClassWriter 继承于ClassVisitor,而ClassReader 直接继承于Object,只是提供解析class,并依次调用ClassVisitor对象。也就是说ClassReader的api和ClassWriter 的api基本没有相关性。

    另外补充一下,ASM中常见参数desc直译是描述,但是作用其实时限定方法的输入参数和返回参数类型,比如"()V"是无输入无输出,"(I)Ljava/lang/String;"是输入int,返回String。

    无中生有 ——利用ASM动态创建一个类

    由于是凭空创建,所以只需要ClassWriter 即可。

    先上目标代码,我们的目的是创造一个下面的类

    public class Student{
        public int age = 11;
    
        public int getAge() {
            return age;
        }
    }
    

    一个特别简单的javabean类。

    简单说一下创建流程:

    1. 创建一个类需要先调用visit创建类的头部信息。
    2. 分别调用visitMethod或visitField生成需要的创建的方法或者字段。
    3. 调用visitEnd结束类的创建
    4. 调用ClassWriter 的toByteArray将动态生成的class转为byte[]数组,可以用ClassLoader动态载入,或者写出成.class文件
      完整代码:
    public byte[] createNewClass() {
            //创建ClassWriter ,构造参数的含义是是否自动计算栈帧,操作数栈及局部变量表的大小
            //0:完全手动计算 即手动调用visitFrame和visitMaxs完全生效
            //ClassWriter.COMPUTE_MAXS=1:需要自己计算栈帧大小,但本地变量与操作数已自动计算好,当然也可以调用visitMaxs方法,只不过不起作用,参数会被忽略;
            //ClassWriter.COMPUTE_FRAMES=2:栈帧本地变量和操作数栈都自动计算,不需要调用visitFrame和visitMaxs方法,即使调用也会被忽略。
            //这些选项非常方便,但会有一定的开销,使用COMPUTE_MAXS会慢10%,使用COMPUTE_FRAMES会慢2倍。
            ClassWriter cw = new ClassWriter(0);
            //创建类头部信息:jdk版本,修饰符,类全名,签名信息,父类,接口集
            cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "asm/Student", null, "java/lang/Object", null);
            //创建字段age:修饰符,变量名,类型,签名信息,初始值(不一定会起作用后面会说明)
            cw.visitField(Opcodes.ACC_PUBLIC , "age", "I", null, new Integer(11))
                    .visitEnd();
            //创建方法:修饰符,方法名,类型,描述(输入输出类型),签名信息,抛出异常集合
            // 方法的逻辑全部使用jvm指令来书写的比较晦涩,门槛较高,后面会介绍简单的方法
            MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "getAge", "()I", null, null);
            // 创建方法第一步
            mv.visitCode();
            // 将索引为 #0 的本地变量列表加到操作数栈下。#0 索引的本地变量列表永远是 this ,当前类实例的引用。
            mv.visitVarInsn(ALOAD, 0);
            // 获取变量的值,
            mv.visitFieldInsn(GETFIELD, "asm/Student", "age", "I");
            // 返回age
            mv.visitInsn(IRETURN);
            // 设置操作数栈和本地变量表的大小
            mv.visitMaxs(1, 1);
            //结束方法生成
            mv.visitEnd();
            //结束类生成
            cw.visitEnd();
            //返回class的byte[]数组
            return cw.toByteArray();
        }
    

    通过以上代码可以看出其实类以及字段的创建还是比较简单的,难点在于方法的创建上。如果对于jvm指令集不熟悉基本抓瞎。这里介绍一个方法,先手写目标类,即Student的java文件,然后用javac编译成class文件(或者用IDE编译),找到编译好的class文件,用javap -c Student打开class文件。输出如下:

    public class asm.Student {
      public int age;
    
      public asm.Student();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: aload_0
           5: bipush        10
           7: putfield      #2                  // Field age:I
          10: return
    
      public int getAge();
        Code:
           0: aload_0
           1: getfield      #2                  // Field age:I
           4: ireturn
    }
    

    可以看到jvm编译时帮助Student补全了构造方法Student(),着重看getAge的指令代码

           0: aload_0
           1: getfield      #2                  // Field age:I
           4: ireturn
    

    一共三条正好和生成方法的代码对应上

            mv.visitVarInsn(ALOAD, 0);
            mv.visitFieldInsn(GETFIELD, "asm/ASMDemo", "age", "I");
            mv.visitInsn(IRETURN);
    

    当没有思路时,可以用这参考这种办法。
    好了现在已经生成了新的class的byte[],剩下的就是加载,验证了。
    加载的代码:

    /**
      *用来加载byte[],由于defineClass不是public修饰的所以只能这样写。
      */
    public class MyClassLoader extends ClassLoader {
      public Class getClassByBytes(byte[] bytes) {
            return defineClass(null, bytes, 0, bytes.length);
        }
    
      public static void main(String[] args) throws Exception {
            MyClassLoader myClassLoader = new MyClassLoader();
            Class classByBytes = myClassLoader.getClassByBytes(create());
            Object o = classByBytes.newInstance();
            Field field = classByBytes.getField("age");
            Object o1 = field.get(o);
            Method method = classByBytes.getMethod("getAge");
            Object o2 = method.invoke(o);
            System.out.println("Field age:  " + o1 );
            System.out.println("Method method :  " + o2);
        }
    }
    
    

    点击运行,然后你就会发现——华丽丽的报错了

    Exception in thread "main" java.lang.InstantiationException: asm.CreateTest
        at java.lang.Class.newInstance(Class.java:427)
        at asm.ASMTest.main(ASMTest.java:20)
    Caused by: java.lang.NoSuchMethodException: asm.CreateTest.<init>()
        at java.lang.Class.getConstructor0(Class.java:3082)
        at java.lang.Class.newInstance(Class.java:412)
        ... 1 more
    

    asm.CreateTest.<init>()这个方法没有找到,熟悉jvm的可能会知道其实<init>就是构造函数,构造函数在jvm中会被重新命名成<init>。但是我们手写的java文件时也没有写构造函数,为什么就可以呢?翻到上面贴出的用javac编译出的Student文件,可以看到编译时编译器自动帮我们加好了构造函数。然后再把咱们自己生成的class文件的byte[]通过输出流写成class文件,在通过javap -c 查看:

     public class asm.Student{
      public int zero;
    
      public int getZero();
        Code:
           0: aload_0
           1: getfield      #11                 // Field zero:I
           4: ireturn
    }
    

    果然利用ASM生成的class里的确没有构造方法。ASM还是要比编译器懒一些的,哈。既然没有,咱们加上就行了。
    先参考一下上面由java编译成的class文件,其实构造函数的代码:

      public asm.Student();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: aload_0
           5: bipush        10
           7: putfield      #2                  // Field age:I
          10: return
    

    简单翻译一下这6条指令:

    1. this变量入栈
    2. 执行父类的<init>方法
    3. this再次入栈
    4. byte变量10入栈
    5. 给对象字段age赋值
    6. 方法结束

    如此可以看到其实构造函数最核心的指令就会调用父类的<init>方法(暂时不考虑字段赋值的事情)。现在基本能够确定,我们手写的构造函数必须包含这三条指令

    aload_0
    invokespecial
    return
    

    然后和上面生成getAge方法类似的生成一个<init>方法即可,代码如下:

            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            mv.visitCode();
            // aload_0
            mv.visitVarInsn(ALOAD, 0);
            // 获取变量的值,
            mv.visitMethodInsn(INVOKESPECIAL,"java/lang/Object", "<init>", "()V", false);
            // 结束
            mv.visitInsn(IRETURN);
            // 设置操作数栈和本地变量表的大小
            mv.visitMaxs(1, 1);
            //结束方法生成
            mv.visitEnd();
    

    然后再次运行,发现已经可以正常运行,输出如下

    Field age:  0
    Method method :  0
    

    说好的11呢???哈,其实通过查看java文件编译后的class就能发现全局变量的默认值赋值其实是在构造函数中进行的,也就是说我们通过ASM创建字段时设置的默认值没起效果,WTF!再次修改<init>方法(类似getAge,不在添加详细注释)

    mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
    mv.visitCode();
    mv.visitVarInsn(ALOAD, 0);
    mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
    mv.visitVarInsn(ALOAD, 0);
    mv.visitIntInsn(BIPUSH, 10);
    mv.visitFieldInsn(PUTFIELD, "asm/Student", "age", "I");
    mv.visitInsn(RETURN);
    mv.visitMaxs(2, 1);
    mv.visitEnd();
    

    再次运行

    Field age:  10
    Method method :  10
    

    哈,完美运行。那么可能有同学会问了,那么设置字段默认值卵用没有,为什么还有这个参数呢,其实也并不是一点用没有,当生成的字段时static时,就会起作用。这里边又会涉及到类的静态变量加载时机,<cinit>函数等等,这里就不展开细讲了,否则篇幅该hold不住了。总结起来一句话:ASM只是工具,掌握jvm知识才是硬道理。

    偷梁换柱——ASM修改已有的class

    其实除了动态生成class,还有一大部分需求是修改class,这里简单介绍下最复杂的修改class的Method。其他的修改照葫芦画瓢就可以。
    先上目标效果,首先原始类还用咱们的Student:

    public class Student{
        public int age = 11;
    
        public int getAge() {
            return age;
        }
    }
    

    目标是在getAge里边插入一句打印语句,即:

        public class Student{
            public int age = 11;
    
            public int getAge() {
                System.out.println("getAge");
                return age;
            }
        }
    

    思路如下:

    1. 首先自定义一个ClassVisitor,重写visitMethod,这样就可以收到每个方法的回调
    2. 判断方法名称是不是getAge
    3. 如果是返回一个自定义的MethodVisitor
    4. 自定义的MethodVisitor重写visitCode(访问方法的第一个步骤)
    5. 添加相应的逻辑
    6. 通过重写visitMaxs修改操作数栈和局部变量表的大小(添加了逻辑可能会导致操作数栈和局部变量表的最大值增大)

    代码如下

    public class MyMethodVisitor extends MethodVisitor {
        public MyMethodVisitor(MethodVisitor mv) {
            super(ASM5, mv);
        }
    
    
        @Override
        public void visitCode() {
            mv.visitCode();
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("getAge");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
    
        @Override
        public void visitMaxs(int maxStack, int maxLocals) {
            super.visitMaxs(maxStack+1, maxLocals);
        }
    }
    
    public class MyClassVisitor extends ClassVisitor {
        public MyClassVisitor(ClassVisitor cv) {
            super(ASM5, cv);
        }
    
        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
            if(name.equals("getAge")){
                return new MyMethodVisitor(methodVisitor);
            }else {
                return methodVisitor;
            }
        }
    }
    
    //修改测试代码
    public static void main(String[] args) throws Exception {
            ClassReader classReader = new ClassReader(createNewClass());
            ClassWriter classWriter = new ClassWriter(classReader, 0);
            ClassVisitor cv = new MyClassVisitor(classWriter);
            classReader.accept(cv,0);
            MyClassLoader myClassLoader = new MyClassLoader();
            Class classByBytes = myClassLoader.getClassByBytes(classWriter.toByteArray());
            Object o = classByBytes.newInstance();
            Field field = classByBytes.getField("age");
            Object o1 = field.get(o);
            Method method = classByBytes.getMethod("getAge");
            Object o2 = method.invoke(o);
            System.out.println("Field age:  " + o1);
            System.out.println("Method method :  " + o2);
        }
    

    运行:

    getAge
    Field age:  10
    Method method :  10
    

    注入的逻辑完美运行!修改方法逻辑不仅仅可以在方法开始插入逻辑,包括方法结束时,甚至方法体中间都可以,可以利用这种思路很方便的写出一个AOP框架。

    ASMifier

    ASM由于是基于jvm指令集的所以比较晦涩。官方可能是考虑到大家都是比较菜的,提供了很多的工具类,这里只介绍一种我认为最有用的:ASMifier。ASMifier最大的功能就是将一个java文件翻译成ASM生成此文件的代码。

    ASMifier.main(new String[]{"asm.Student"});
    

    运行后,就可以在控制台看见如何利用ASM生成Student类了,省了很大力气。工具类很多就不一 一介绍了,推荐一个博客有兴趣可以去看看:

    https://blog.csdn.net/ljz2016/article/details/81363828

    总结

    ASM相对于一些其他的操作字节码的框架偏底层了一些,只提供了一些低级api,要想熟练使用还是需要比较高的jvm知识的。但是作为其他操作字节码的框架的底层实现,还是非常有必要了解一下的。真实项目中如果对性能要求不是特别高的话,结合项目需求完全可以用其他高级库代替ASM,例如cglib javassist。
    突然想起来前两年做过的一个需求:拿到一个类序列化之后的文件,然后在本地没有这个类的情况下反序列化它。
    当时觉得这个需求真是扯淡,现在想想做反序列化时报出ClassNotFound这个错误之前,其实已经可以获取类的包名,类名,签名,以及字段详情了。其实完全可以重写反序列化方法,然后获取到类的信息后动态生成class文件,然后再加载到内存中,之后再做正常的反序列化操作。两年前的需求现在想出了解决方案,哈!

    代码地址:

    https://github.com/fengao1004/ASM.git

    参考:

    https://blog.csdn.net/lijingyao8206/article/category/3276863

    相关文章

      网友评论

          本文标题:ASM——运行时/编译时动态修改class源码

          本文链接:https://www.haomeiwen.com/subject/eojjoqtx.html