曹工說Spring Boot源碼(23)-- ASM又立功了,Spring原來是這么遞歸獲取注解的元注解的


寫在前面的話

相關背景及資源:

曹工說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源碼系列,離上一篇,快有2周時間了,這兩周,本來是打算繼續寫這個系列的;結果中間腦熱,就去實踐了一把動態代理,實現了一個mini-dubbo這樣一個rpc框架,擴展性還是相當好的,今天看了下spring mvc的設計,思路差不多,都是框架提供默認的組件(比如handlermapping),然后程序里自定義了的話,就覆蓋默認組件。

然后,因為mini-dubbo實現過程中的一些其他問題,以及工作上的需要,寫了netty實現的http 連接池,這個系列還沒講完,留着后邊再補,不然我們的源碼系列就耽擱太久了,今天我們還是接着回來弄源碼系列。

今天這講,主題是:給你一個class,怎么讀取其上的注解,需要考慮注解的元注解(可以理解注解上的注解)

讀取class上的注解

常規做法

我們的Class類,就有很多獲取annotation的方法,如下:

但是,這個有一個問題是,無法遞歸獲取。

比如,大家使用spring的,都知道,controller這個注解上,是注解了component的。

如果你在一個標注了@controller注解的類的class上,去獲取注解,是拿不到Component這一層的。

為啥要拿Component這一層呢?你可以想一下,最開始寫spring的作者,是只定義了Component這個注解的,業務邏輯也只能處理Component這個注解;后來呢,又多定義了@controller,@service這幾個,但是,難道要把所有業務邏輯的地方都去改一改?很明顯,你不會,大佬更不會,直接解析@controller注解,看看它的元注解有沒有@component就行了,有的話,直接復用之前的邏輯。

那么,如何進行遞歸解析呢?

遞歸解析類上注解--方法1

我們要獲取的class,長這樣:

package org.springframework.test;

@CustomController
public class TestController {
}


@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Controller
public @interface CustomController {
}

這個方法,是從spring 源碼里摘抄的,在內部實現中,基本就這個樣子:

我這個版本是4.0,在:org.springframework.bootstrap.sample.Test#recusivelyCollectMetaAnnotations

public static void getAnnotationByClass(String className) throws ClassNotFoundException {
    Class<?> clazz = Class.forName(className);
    Set<String> metaAnnotationTypeNames = new LinkedHashSet<String>();
    for (Annotation metaAnnotation : clazz.getAnnotations()) {
        recusivelyCollectMetaAnnotations(metaAnnotationTypeNames, metaAnnotation);
    }
}


private static void recusivelyCollectMetaAnnotations(Set<String> visited, Annotation annotation) {
    if (visited.add(annotation.annotationType().getName())) {
        for (Annotation metaMetaAnnotation : annotation.annotationType().getAnnotations()) {
        	//遞歸
            recusivelyCollectMetaAnnotations(visited, metaMetaAnnotation);
        }
    }
}

我試了下,這個方法在新版本里,方法名變了,核心還是差不多,spring 5.1.9可以看這個類:

org.springframework.core.type.classreading.AnnotationAttributesReadingVisitor#recursivelyCollectMetaAnnotations

輸出如下:

java.lang.annotation.Documented
java.lang.annotation.Retention
java.lang.annotation.Target
org.springframework.stereotype.Controller
org.springframework.stereotype.Component

遞歸解析類上注解--方法2

這個是我自己實現的,要復雜一些,當然,是有理由的:

import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;

public static void main(String[] args) throws IOException, ClassNotFoundException {
        SimpleMetadataReaderFactory simpleMetadataReaderFactory = new SimpleMetadataReaderFactory();
        LinkedHashSet<String> result = new LinkedHashSet<>();

        getAnnotationSet(result, "org.springframework.test.TestController", simpleMetadataReaderFactory);
    }

    public static void getAnnotationSet(LinkedHashSet<String> result, String className, SimpleMetadataReaderFactory simpleMetadataReaderFactory) throws IOException {
        boolean contains = result.add(className);
        if (!contains) {
            return;
        }

        MetadataReader metadataReader = simpleMetadataReaderFactory.getMetadataReader(className);
        AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
        Set<String> annotationTypes = annotationMetadata.getAnnotationTypes();

        if (!CollectionUtils.isEmpty(annotationTypes)) {
            for (String annotationType : annotationTypes) {
                // 遞歸
                getAnnotationSet(result, annotationType, simpleMetadataReaderFactory);
            }
        }

    }

估計有的同學要罵人了,取個注解,搞一堆莫名其妙的工具類干嘛?因為,spring就是這么玩的啊,方法1,是spring的實現,不假。但是,那個已經是最內層了,人家外邊還封裝了一堆,封裝出來,基本就是方法2看到的那幾個類。

spring抽象出的注解獲取的核心接口

大家看看,就是下面這個,類圖如下:

其大致的功能,看下圖就知道了:

這個接口,一共2個實現,簡單來說,一個是通過傳統的反射方式來獲取這些信息,一個是通過asm的方式。

兩者的優劣呢,大家可以看看小馬哥的書,里面提到的是,asm方式的性能,遠高於反射實現,因為無需加載class,直接解析class文件的字節碼。

我們這里也是主要講asm方式的實現,大家看到了上面這個asm實現的類,叫:AnnotationMetadataReadingVisitor,它的類結構,如下:

從上圖可以大致知道,其繼承了ClassMetadataReadingVisitor,這個類,負責去實現ClassMetaData接口;它自己呢,就自己負責實現AnnotationMetadata接口。

我們呢,不是很關心類的相關信息,只聚焦注解的獲取。

AnnotationMetadataReadingVisitor如何實現AnnotationMetadata接口

AnnotationMetadata接口,我們最關注的就是下面這2個方法:

   // 獲取直接注解在當前class上的注解
   Set<String> getAnnotationTypes();

   // 獲取某個直接注解的元注解,比如你這里傳個controller進去,就能給你拿到controller這個注解的元注解
   Set<String> getMetaAnnotationTypes(String annotationType);

大家可以看到,它呢,給了2個方法,而不是一個方法來獲取所有,可能有其他考慮吧,我們接着看。

getAnnotationTypes的實現

這個方法,獲取直接注解在target class上的注解。

那看看這個方法在AnnotationMetadataReadingVisitor的實現吧:

public Set<String> getAnnotationTypes() {
    return this.annotationSet;
}

尷尬,看看啥時候給它賦值的:

@Override
public AnnotationVisitor visitAnnotation(final String desc, boolean visible) {
    String className = Type.getType(desc).getClassName();
    this.annotationSet.add(className);
    return new AnnotationAttributesReadingVisitor(className, this.attributeMap,
                                                  this.metaAnnotationMap, this.classLoader, this.logger);
}

方法名字,見名猜意思,:visit注解,可能還使用了visitor設計模式,但是這個方法又是什么時候被調用的呢

asm簡介

簡單介紹下asm框架,官網:

https://asm.ow2.io/

https://asm.ow2.io/asm4-guide.pdf

官網說明如下:

ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused on performance. Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers).

ASM is used in many projects, including:

簡單來說,就是:

asm是一個字節碼操作和分析的框架,能夠用來修改已存在的class,或者動態生成class,直接以二進制的形式。ASM提供一些通用的字節碼轉換和分析算法,通過這些算法,可以構建復雜的字節碼轉換和代碼分析工具。ASM提供和其他字節碼框架類似的功能,但是其專注於性能。因為它被設計和實現為,盡可能的小,盡可能的快。

ASM被用在很多項目,包括:

OpenJDK,生成lambda調用;

Groovy和Kotlin的編譯器

Cobertura和Jacoco,通過探針,檢測代碼覆蓋率

CGLIB,動態生成代理類,也用在Mockito和EasyMock中

Gradle,運行時動態生成類

這里補充一句,ASM為啥說它專注於性能,因為,要動態生成類、動態進行字節碼轉換,如果性能太差的話,還有人用嗎? 為啥要足夠小,足夠小因為它也希望自己用在一些內存受限的環境中。

查看了asm的官方文檔,發現一個有趣的知識,asm這個名字,來源於c語言里面的__asm__關鍵字,這個關鍵字可以在c語言里用匯編來實現某些功能。

另外,其官方文檔里提到,解析class文件的過程,有兩種模型,一種是基於事件的,一種是基於對象的,可以類比xml解析中的sax和dom模型,sax就是基於事件的,同樣也是和asm一樣,使用visitor模式。

visitor模式呢,我的簡單理解,就是主程序定義好了一切流程,比如我會按照順序來訪問一個class,先是class name,就去調用visitor的對應方法,此時,visitor可以做些處理;我訪問到field時,也會調用visitor的對應方法...以此類推。

asm怎么讀取class

針對每個class,asm是把它當作一個Resource,其大概的解析步驟如下:

import org.springframework.asm.ClassReader;
import org.springframework.core.NestedIOException;
import org.springframework.core.io.Resource;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.ClassMetadata;

SimpleMetadataReader(Resource resource, ClassLoader classLoader, MetadataReaderLog logger) throws IOException {
    	// 1.
		InputStream is = new BufferedInputStream(resource.getInputStream());
		ClassReader	classReader = new ClassReader(is);
		// 2.
		AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(classLoader, logger);
    	// 3.
		classReader.accept(visitor, ClassReader.SKIP_DEBUG);

		this.annotationMetadata = visitor;
		// (since AnnotationMetadataReader extends ClassMetadataReadingVisitor)
		this.classMetadata = visitor;
		this.resource = resource;
	}

各講解點:

  1. 讀取class resource為輸入流,作為構造器參數,new一個asm的ClassReader出來;

  2. 新建一個AnnotationMetadataReadingVisitor類的實例,這個繼承了ClassVisitor抽象類,這個visitor里面定義了一堆的回調方法:

    public abstract class ClassVisitor {
    	public ClassVisitor(int api);
        
    	public ClassVisitor(int api, ClassVisitor cv);
        
    	public void visit(int version, int access, String name,
    	String signature, String superName, String[] interfaces);
        
    	public void visitSource(String source, String debug);
        
    	public void visitOuterClass(String owner, String name, String desc);
        // 解析到class文件中的注解時回調本方法
    	AnnotationVisitor visitAnnotation(String desc, boolean visible);
        
    	public void visitAttribute(Attribute attr);
        
    	public void visitInnerClass(String name, String outerName,String innerName,int access);
        // 解析到field時回調
    	public FieldVisitor visitField(int access, String name, String desc,String signature, Object value);
        
        // 解析到method時回調
    	public MethodVisitor visitMethod(int access, String name, String desc,
    	String signature, String[] exceptions);
        
    	void visitEnd();
    }
    
    

    這其中,方法的訪問順序如下:

    visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*
    ( visitInnerClass | visitField | visitMethod )*
    visitEnd
    
    代表:
    visit必須最先訪問;
    接着是最多一次的visitSource,再接着是最多一次的visitOuterClass;
    接着是任意多次的visitAnnotation | visitAttribute ,這兩個,順序隨意;
    再接着是,任意多次的visitInnerClass | visitField | visitMethod ,順序隨意
    最后,visitEnd
    

    這個順序的? * () 等符號,其實類似於正則表達式的語法,對吧,還是比較好理解的。

    然后呢,我對visitor的理解,現在感覺類似於spring里面的event listener機制,比如,spring的生命周期中,發布的事件,有如下幾個,其實也是有順序的:

    這里還有官網提供的一個例子:

    public class ClassPrinter extends ClassVisitor {
        public ClassPrinter() {
            super(ASM4);
        }
        public void visit(int version, int access, String name,
        	String signature, String superName, String[] interfaces) {
        	System.out.println(name + " extends " + superName + " {");
        }
        public void visitSource(String source, String debug) {
        }
        public void visitOuterClass(String owner, String name, String desc) {
        }
        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        	return null;
        }
        public void visitAttribute(Attribute attr) {
        }
        public void visitInnerClass(String name, String outerName,String innerName, int access) 	{
        }
        public FieldVisitor visitField(int access, String name, String desc,String signature, Object value) {
        	System.out.println(" " + desc + " " + name);
        	return null;
        }
        public MethodVisitor visitMethod(int access, String name,String desc, String signature, String[] exceptions) {
        	System.out.println(" " + name + desc);
        	return null;
        }
        public void visitEnd() {
        	System.out.println("}");
        }
    }
    
    
  3. 將第二步的visitor策略,傳遞給classReader,classReader開始進行解析

getAnnotationTypes的回調處理

我們接着回到getAnnotationTypes的實現,大家看了上面2個圖,應該大致知道visitAnnotation的實現了:

	@Override
	public AnnotationVisitor visitAnnotation(final String desc, boolean visible) {
		String className = Type.getType(desc).getClassName();
		this.annotationSet.add(className);
		return new AnnotationAttributesReadingVisitor(className, this.attributeMap,
				this.metaAnnotationMap, this.classLoader, this.logger);
	}

這里每訪問到一個注解,就會加入到field: annotationSet中。

注解上的元注解,如何讀取

大家再看看上面的代碼,我們返回了一個AnnotationAttributesReadingVisitor,這個visitor會在:asm訪問注解的具體屬性時,其中的如下方法被回調。

	@Override
	public void doVisitEnd(Class<?> annotationClass) {
		super.doVisitEnd(annotationClass);
		List<AnnotationAttributes> attributes = this.attributesMap.get(this.annotationType);
		if(attributes == null) {
			this.attributesMap.add(this.annotationType, this.attributes);
		} else {
			attributes.add(0, this.attributes);
		}
		Set<String> metaAnnotationTypeNames = new LinkedHashSet<String>();
        // 1 
		for (Annotation metaAnnotation : annotationClass.getAnnotations()) {
            // 2
			recusivelyCollectMetaAnnotations(metaAnnotationTypeNames, metaAnnotation);
		}
		if (this.metaAnnotationMap != null) {
			this.metaAnnotationMap.put(annotationClass.getName(), metaAnnotationTypeNames);
		}
	}
	// 3 
	private void recusivelyCollectMetaAnnotations(Set<String> visited, Annotation annotation) {
		if(visited.add(annotation.annotationType().getName())) {
			this.attributesMap.add(annotation.annotationType().getName(),
					AnnotationUtils.getAnnotationAttributes(annotation, true, true));
            // 獲取本注解上的元注解
			for (Annotation metaMetaAnnotation : annotation.annotationType().getAnnotations()) 			   {	// 4 遞歸調用自己
				recusivelyCollectMetaAnnotations(visited, metaMetaAnnotation);
			}
		}
	}
  1. 獲取注解的元注解,比如,獲取controller注解上的注解;這里就能取到Target、Retention、Documented、Component

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Component
    public @interface Controller 
    
  2. 循環處理這些元注解,因為這些元注解上,可能還有元注解,比如,在處理Target時,發現其上還有Documented、Retention、Target幾個注解,看到了吧,target注解還注解了target,在這塊的遞歸處理時,很容易棧溢出。

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Target {
    
  3. 遞歸處理上面的這些注解

具體的處理,基本就是這樣。文章開頭的遞歸,就是摘抄的這里的代碼。

經過最終的處理后,可以看看最后的效果,這里截取的就是AnnotationMetadataReadingVisitor這個對象:

總結

這個就是spring 注解驅動的基石,實際上,spring不是一開始就這么完備的,在之前的版本,並不支持遞歸獲取,spring也是慢慢一步一步發展壯大的。

感謝spring賞飯吃!

下一講,會講解component-scan掃描bean時,怎么掃描類上的注解的。


免責聲明!

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



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