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


寫在前面的話

相關背景及資源:

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

這樣才能學的勞。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM