曹工說Spring Boot源碼(24)-- Spring註解掃描的瑞士軍刀,asm技術實戰(上)

寫在前面的話

相關背景及資源:html

曹工說Spring Boot源碼(1)-- Bean Definition究竟是什麼,附spring思惟導圖分享java

曹工說Spring Boot源碼(2)-- Bean Definition究竟是什麼,我們對着接口,逐個方法講解git

曹工說Spring Boot源碼(3)-- 手動註冊Bean Definition不比遊戲好玩嗎,咱們來試一下web

曹工說Spring Boot源碼(4)-- 我是怎麼自定義ApplicationContext,從json文件讀取bean definition的?spring

曹工說Spring Boot源碼(5)-- 怎麼從properties文件讀取beanjson

曹工說Spring Boot源碼(6)-- Spring怎麼從xml文件裏解析bean的設計模式

曹工說Spring Boot源碼(7)-- Spring解析xml文件,到底從中獲得了什麼(上)api

曹工說Spring Boot源碼(8)-- Spring解析xml文件,到底從中獲得了什麼(util命名空間)框架

曹工說Spring Boot源碼(9)-- Spring解析xml文件,到底從中獲得了什麼(context命名空間上)ide

曹工說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

這樣才能學的勞。

相關文章
相關標籤/搜索