曹工说Spring Boot源码(24)– Spring注解扫描的瑞士军刀,asm技术实战(上)

  • 2020 年 3 月 26 日
  • 筆記

写在前面的话

相关背景及资源:

曹工说Spring Boot源码(1)– Bean Definition到底是什么,附spring思维导图分享

曹工说Spring Boot源码(2)– Bean Definition到底是什么,咱们对着接口,逐个方法讲解

曹工说Spring Boot源码(3)– 手动注册Bean Definition不比游戏好玩吗,我们来试一下

曹工说Spring Boot源码(4)– 我是怎么自定义ApplicationContext,从json文件读取bean definition的?

曹工说Spring Boot源码(5)– 怎么从properties文件读取bean

曹工说Spring Boot源码(6)– Spring怎么从xml文件里解析bean的

曹工说Spring Boot源码(7)– Spring解析xml文件,到底从中得到了什么(上)

曹工说Spring Boot源码(8)– Spring解析xml文件,到底从中得到了什么(util命名空间)

曹工说Spring Boot源码(9)– Spring解析xml文件,到底从中得到了什么(context命名空间上)

曹工说Spring Boot源码(10)– Spring解析xml文件,到底从中得到了什么(context:annotation-config 解析)

曹工说Spring Boot源码(11)– context:component-scan,你真的会用吗(这次来说说它的奇技淫巧)

曹工说Spring Boot源码(12)– Spring解析xml文件,到底从中得到了什么(context:component-scan完整解析)

曹工说Spring Boot源码(13)– AspectJ的运行时织入(Load-Time-Weaving),基本内容是讲清楚了(附源码)

曹工说Spring Boot源码(14)– AspectJ的Load-Time-Weaving的两种实现方式细细讲解,以及怎么和Spring Instrumentation集成

曹工说Spring Boot源码(15)– Spring从xml文件里到底得到了什么(context:load-time-weaver 完整解析)

曹工说Spring Boot源码(16)– Spring从xml文件里到底得到了什么(aop:config完整解析【上】)

曹工说Spring Boot源码(17)– Spring从xml文件里到底得到了什么(aop:config完整解析【中】)

曹工说Spring Boot源码(18)– Spring AOP源码分析三部曲,终于快讲完了 (aop:config完整解析【下】)

曹工说Spring Boot源码(19)– Spring 带给我们的工具利器,创建代理不用愁(ProxyFactory)

曹工说Spring Boot源码(20)– 码网恢恢,疏而不漏,如何记录Spring RedisTemplate每次操作日志

曹工说Spring Boot源码(21)– 为了让大家理解Spring Aop利器ProxyFactory,我已经拼了

曹工说Spring Boot源码(22)– 你说我Spring Aop依赖AspectJ,我依赖它什么了

曹工说Spring Boot源码(23)– ASM又立功了,Spring原来是这么递归获取注解的元注解的

工程代码地址 思维导图地址

工程结构图:

概要

上一篇,我们讲了spring是怎么获取class上的注解,以及注解的元注解的。在注解大行其道的今天,理解这些相对底层一点的知识,是绝对有必要的。另外,在上一讲中,我们提到了,spring其实最终也是利用ASM去读取注解的,其中,还使用了访问者设计模式。

访问者设计模式有效地分离了对数据的访问和和对数据的操作,因为class结构是很固定的,所以,visitor模式就尤其适合。在访问到特定数据时,就回调应用注册的回调方法。ASM基本上就是在visitor这个设计模式的基础上建立起来的。

今天,我们的主题有两个,1是简单地了解下ASM,2是投入实战,看看要怎么去利用ASM + java的Intrumentation机制,来在java启动时,就去修改class,实现简单的aop功能。

本篇覆盖第一个主题,下一个主题留带下一篇(demo已经ok了)。

ASM的核心之读取功能

我们目的是读取以下测试类上的注解和所有的方法的名称。

以下代码demo见:https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/asm-demo/src/main/java/com/yn/onlyvisit

  1. 测试类

    package com.yn.onlyvisit;    @CustomAnnotationOnClass  public class Person {      private String name;        private int age;        public String getName() {          return name;      }        public void setName(String name) {          this.name = name;      }        public int getAge() {          return age;      }        public void setAge(int age) {          this.age = age;      }  }    @Documented  @Target(ElementType.TYPE)  @Retention(RetentionPolicy.RUNTIME)  public @interface CustomAnnotationOnClass {  }    
  2. 定义classVisitor,里面实现visitor的回调方法

    package com.yn.onlyvisit;      import org.objectweb.asm.*;  import org.objectweb.asm.commons.AdviceAdapter;  import org.objectweb.asm.commons.AnalyzerAdapter;  import org.objectweb.asm.util.ASMifier;  import org.objectweb.asm.util.Textifier;  import org.objectweb.asm.util.TraceMethodVisitor;    import java.util.ArrayList;  import java.util.List;    public class MyClassVistor extends ClassVisitor {      private List<String> methodList =  new ArrayList<>();      private List<String> annotationOnClass =  new ArrayList<>();      public MyClassVistor() {          super(Opcodes.ASM6);      }          @Override      public MethodVisitor visitMethod(int access, String name,                                       String desc, String signature,                                       String[] exceptions) {          //每访问到一个方法,加入到field中          System.out.println("visitMethod: " + name);          methodList.add(name);            return super.visitMethod(access, name, desc, signature, exceptions);      }        @Override      public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {          // 访问到类上注解,加入field          annotationOnClass.add(descriptor);          return super.visitAnnotation(descriptor, visible);      }        @Override      public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {          System.out.println("field:" + name);          return super.visitField(access, name, descriptor, signature, value);      }        public List<String> getMethodList() {          return methodList;      }        public List<String> getAnnotationOnClass() {          return annotationOnClass;      }  }    
  3. 测试代码

    import org.objectweb.asm.ClassReader;    import java.io.IOException;  import java.util.List;    public class TestClassVisit {      public static void main(String[] args) throws IOException {          // 使用classreader读取目标类          ClassReader classReader = new ClassReader("com.yn.onlyvisit.Person");          // new一个visitor          MyClassVistor classVisitor = new MyClassVistor();          // 传入classreader          classReader.accept(classVisitor,ClassReader.SKIP_DEBUG);          // 此时,目标类已经读取完毕,我们可以打印看看效果          List<String> methodList = classVisitor.getMethodList();          System.out.println(methodList);          System.out.println(classVisitor.getAnnotationOnClass());      }  }  

    输出如下:

    field:name
    field:age
    visitMethod:
    visitMethod: getName
    visitMethod: setName
    visitMethod: getAge
    visitMethod: setAge
    [, getName, setName, getAge, setAge]
    [Lcom/yn/onlyvisit/CustomAnnotationOnClass;]

ASM的核心之生成全新class

案例讲解

注意,我们限定的是,生成全新的class,为什么限定这么死,因为还有一种是,在已经存在的类的基础上,修改class。

生成全新class的场景也是常见的,比如cglib底层就使用了asm,代理类是动态生成的,对吧?虽然我还没验证,但基本就是目前要讲的这种场景。

还有就是,fastjson里也用了asm,至于里面是否是生成全新class,留带验证。

asm的官方文档,有下面这样一个例子。

目标类如下,我们的目标,就是生成这样一个类的class:

package pkg;  public interface Comparable extends Mesurable {  int LESS = -1;  int EQUAL = 0;  int GREATER = 1;  int compareTo(Object o);  }  

我们只需要如下几行代码,即可完成该目标。

package com.yn.classgenerate;    import org.objectweb.asm.ClassWriter;    import java.io.*;  import java.lang.reflect.Field;    import static org.objectweb.asm.Opcodes.*;    public class TestClassWriter {      public static void main(String[] args) throws IOException {          ClassWriter cw = new ClassWriter(0);          cw.visit(V1_7, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,                  "pkg/Comparable", null, "java/lang/Object",                  null);          cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I",                  null, new Integer(-1)).visitEnd();          cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I",                  null, new Integer(0)).visitEnd();          cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I",                  null, new Integer(1)).visitEnd();          cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo",                  "(Ljava/lang/Object;)I", null, null).visitEnd();          cw.visitEnd();          byte[] b = cw.toByteArray();              File file = new File("F:\gitee-ckl\all-simple-demo-in-work\asm-demo\src\main\java\com\yn\classgenerate\Target.class");          FileOutputStream fos = new FileOutputStream(file);          fos.write(b);          fos.close();          }  }    

执行上述代码,在指定位置,就会生成一个Target.class,反编译之后,如下:

ClassWriter初识

上面那个demo,是否够神奇?为什么这么神奇呢,核心都在ClassWriter这个类。

这个类,大家可以理解为,一个class文件包含了很多东西,对吧?常量池、field集合、method集合、注解、class名、实现的接口集合等等,这个classWriter呢,其中就有很多field,分别来存储这些东西。

注意的是,上图中,有些字段,比如firstField,为什么不是集合呢?按理说,一个class里很多field啊,因为,这里用了链表结构来存储field。我们看这个field上的注释。

/**   * The fields of this class, stored in a linked list of {@link FieldWriter} linked via their   * {@link FieldWriter#fv} field. This field stores the first element of this list.   */  private FieldWriter firstField;  

看到了吧,链表结构。

所以,ClassWriter,大家一定要好好理解,这个ClassWriter,主要的使用方法就是:提供给你一堆方法,你可以调用他们,来给里面的field设置东西,比如,你要设置类名,那你就调用:

cw.visit(V1_7, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,          "pkg/Comparable", null, "java/lang/Object",          null);  

要加个field,那就这样:

cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I",                null, new Integer(0)).visitEnd();  

ClassWriter为啥要实现ClassVisitor

如小标题所言,ClassWriter是实现了ClassVisitor的。

public class ClassWriter extends ClassVisitor  

前面我们说的那些,手动去调用的方法,也是来源于ClassVisitor的。

cw.visit(V1_7, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,           "pkg/Comparable", null, "java/lang/Object",           null);  

该方法,来源于:

org.objectweb.asm.ClassVisitor#visit  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);      }    }  

那么,接下来这段话,大家好好理解下:

前面的demo中,我们手动调用了ClassWriter的各种visit方法,去生成class;但是,我们又知道,ClassWriter的那些方法,来自于ClassVisitor,而:当我们向下面这样来编码的时候,ClassVisitor的方法会自动被调用(忘了的,往前翻到:ASM的核心之读取功能),那么,我们可以实现如下的class复制功能了:

package com.yn.classgenerate;    import org.objectweb.asm.ClassReader;  import org.objectweb.asm.ClassVisitor;  import org.objectweb.asm.ClassWriter;    import java.io.File;  import java.io.FileOutputStream;  import java.io.IOException;    import static org.objectweb.asm.Opcodes.ASM4;    public class CopyClassVersion1 {      public static void main(String[] args) throws IOException {          ClassReader classReader = new ClassReader("com.yn.classgenerate.CopyClass");  	    //1          ClassWriter cw = new ClassWriter(0);          //2          classReader.accept(cw, 0);          byte[] b2 = cw.toByteArray();            File file = new File("F:\gitee-ckl\all-simple-demo-in-work\asm-demo\src\main\java\com\yn\classgenerate\CopyClass2.class");          FileOutputStream fos = new FileOutputStream(file);          fos.write(b2);          fos.close();      }  }  

这里的核心,就是要把classWriter,当成ClassVisitor,传递给ClassReader。

  1. 上述代码点1,此时,classWriter内部是空的,没法生成一个class
  2. 传递给classReader后,随着classReader不断去解析com.yn.classgenerate.CopyClass这个类,classWriter的各个visit方法,不断被回调,因此,com.yn.classgenerate.CopyClass的各类field、method等,不断被写入classWriter中,于是,复制就这样完成了。

ClassVisitor那些链式操作

前面那个复制class的操作中,classreader是直接回调classWriter的,我们其实也可以在中间横插一脚。

public class CopyClass {      public static void main(String[] args) throws IOException {          ClassReader classReader = new ClassReader("com.yn.classgenerate.CopyClass");          ClassWriter cw = new ClassWriter(0);            // cv forwards all events to cw          ClassVisitor cv = new ClassVisitor(ASM4, cw) { };          classReader.accept(cv, 0);          byte[] b2 = cw.toByteArray();            File file = new File("F:\gitee-ckl\all-simple-demo-in-work\asm-demo\src\main\java\com\yn\classgenerate\CopyClass2.class");          FileOutputStream fos = new FileOutputStream(file);          fos.write(b2);          fos.close();      }  }  

在上面这个例子中,我们从classReader的下面这句开始看:

 classReader.accept(cv, 0);  

那么,可以知道,classReader是去回调cv,那么cv是谁?

ClassVisitor cv = new ClassVisitor(ASM4, cw) { };  

cv的构造函数里,传入了cw,cw呢,就是classwriter。

现在的链路是这样的:

classReader –> cv –> cw。

上面这个链路中,classReader肯定会回调cv,但是cv,怎么就确定它会当个二传手呢?

看看ClassVisitor的构造函数:

public ClassVisitor(final int api, final ClassVisitor classVisitor) {      if (api < Opcodes.ASM4 || api > Opcodes.ASM6) {        throw new IllegalArgumentException();      }      this.api = api;      this.cv = classVisitor;    }  

其把ClassVisitor保存到了一个域:cv中。这个cv如何被使用呢?我们看看下面的方法:

org.objectweb.asm.ClassVisitor#visit  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);      }    }      public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) {      if (cv != null) {        return cv.visitAnnotation(descriptor, visible);      }      return null;    }  

这就有意思了,如果cv不为null,就调用cv去处理,这就是个delegate啊,代理啊。

中间商搞鬼那些事

上面的demo中,cv简直是尽忠职守,自己在中间,丝毫不做什么事,就是一个称职的代理。但不是所有代理都需要这样,甚至是不鼓励这样。

官网中有个demo,如下所示,可以修改class的版本:

public class ChangeVersionAdapter extends ClassVisitor {        public ChangeVersionAdapter(ClassVisitor classVisitor) {          super(ASM4, classVisitor);      }        @Override      public void visit(int version, int access, String name,                        String signature, String superName, String[] interfaces) {          cv.visit(V1_8, access, name, signature, superName, interfaces);      }    }  

测试类:

  public class TestChangeClassVersion {      public static void main(String[] args) throws IOException {          ClassReader classReader = new ClassReader("com.yn.classgenerate.CopyClass");          ClassWriter cw = new ClassWriter(0);            ClassVisitor cv = new ChangeVersionAdapter(cw) { };          classReader.accept(cv, 0);          byte[] b2 = cw.toByteArray();            File file = new File("F:\gitee-ckl\all-simple-demo-in-work\asm-demo\src\main\java\com\yn\classgenerate\CopyClass2.class");          FileOutputStream fos = new FileOutputStream(file);          fos.write(b2);          fos.close();      }  }  

官网还画了个图,贴心:

通过这样,classWriter中,版本号已经被改了,但它还被蒙在鼓里,可怜。

如果要删除字段、删除方法,怎么整

在ClassVisitor中,有几个特殊的方法:

主要就是这几个,你看他们的返回值,不太一样,是xxxVistor,和ClassVisitor有点像?那就对了。

我们看看fieldVisitor:

其结构和方法,都和ClassVisitor类似,也就是说,我们可以返回一个自定义的FieldVistor,然后,ASM框架,就会使用我们返回的这个FieldVisitor去visit我们的field的相关属性,回调fieldVisitor中的相关方法。

那,怎么删除呢?返回null。

这么简单吗,是的。

package com.yn.classgenerate;    import org.objectweb.asm.ClassVisitor;  import org.objectweb.asm.MethodVisitor;    import static org.objectweb.asm.Opcodes.ASM6;  // 该demo来自官网文档  public class RemoveMethodAdapter extends ClassVisitor {      private String mName;      private String mDesc;        public RemoveMethodAdapter(              ClassVisitor cv, String mName, String mDesc) {          super(ASM6, cv);          this.mName = mName;          this.mDesc = mDesc;      }        @Override      public MethodVisitor visitMethod(int access, String name,                                       String desc, String signature, String[] exceptions) {          if (name.equals(mName) && desc.equals(mDesc)) {              // 这样就可以了。  		   // do not delegate to next visitor -> this removes the method              return null;          }          return cv.visitMethod(access, name, desc, signature, exceptions);      }  }  

总结

asm的基本操作大概如此,这些比较粗浅,下一讲我们会实现一个有用一点的东西,会结合java的instrument机制来讲。

大家要跟着我的demo一起来实践,https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/asm-demo

这样才能学的劳。