1. 程式人生 > >曹工說Spring Boot原始碼(23)-- ASM又立功了,Spring原來是這麼遞迴獲取註解的元註解的

曹工說Spring Boot原始碼(23)-- ASM又立功了,Spring原來是這麼遞迴獲取註解的元註解的

# 寫在前面的話 相關背景及資源: [曹工說Spring Boot原始碼(1)-- Bean Definition到底是什麼,附spring思維導圖分享](https://www.cnblogs.com/grey-wolf/p/12044199.html) [曹工說Spring Boot原始碼(2)-- Bean Definition到底是什麼,咱們對著介面,逐個方法講解](https://www.cnblogs.com/grey-wolf/p/12051957.html ) [曹工說Spring Boot原始碼(3)-- 手動註冊Bean Definition不比遊戲好玩嗎,我們來試一下](https://www.cnblogs.com/grey-wolf/p/12070377.html) [曹工說Spring Boot原始碼(4)-- 我是怎麼自定義ApplicationContext,從json檔案讀取bean definition的?](https://www.cnblogs.com/grey-wolf/p/12078673.html) [曹工說Spring Boot原始碼(5)-- 怎麼從properties檔案讀取bean](https://www.cnblogs.com/grey-wolf/p/12093929.html) [曹工說Spring Boot原始碼(6)-- Spring怎麼從xml檔案裡解析bean的](https://www.cnblogs.com/grey-wolf/p/12114604.html ) [曹工說Spring Boot原始碼(7)-- Spring解析xml檔案,到底從中得到了什麼(上)](https://www.cnblogs.com/grey-wolf/p/12151809.html) [曹工說Spring Boot原始碼(8)-- Spring解析xml檔案,到底從中得到了什麼(util名稱空間)](https://www.cnblogs.com/grey-wolf/p/12158935.html) [曹工說Spring Boot原始碼(9)-- Spring解析xml檔案,到底從中得到了什麼(context名稱空間上)](https://www.cnblogs.com/grey-wolf/p/12189842.html) [曹工說Spring Boot原始碼(10)-- Spring解析xml檔案,到底從中得到了什麼(context:annotation-config 解析)](https://www.cnblogs.com/grey-wolf/p/12199334.html) [曹工說Spring Boot原始碼(11)-- context:component-scan,你真的會用嗎(這次來說說它的奇技淫巧)](https://www.cnblogs.com/grey-wolf/p/12203743.html) [曹工說Spring Boot原始碼(12)-- Spring解析xml檔案,到底從中得到了什麼(context:component-scan完整解析)](https://www.cnblogs.com/grey-wolf/p/12214408.html) [曹工說Spring Boot原始碼(13)-- AspectJ的執行時織入(Load-Time-Weaving),基本內容是講清楚了(附原始碼)](https://www.cnblogs.com/grey-wolf/p/12228958.html) [曹工說Spring Boot原始碼(14)-- AspectJ的Load-Time-Weaving的兩種實現方式細細講解,以及怎麼和Spring Instrumentation整合](https://www.cnblogs.com/grey-wolf/p/12283544.html) [曹工說Spring Boot原始碼(15)-- Spring從xml檔案裡到底得到了什麼(context:load-time-weaver 完整解析)](https://www.cnblogs.com/grey-wolf/p/12288391.html) [曹工說Spring Boot原始碼(16)-- Spring從xml檔案裡到底得到了什麼(aop:config完整解析【上】)](https://www.cnblogs.com/grey-wolf/p/12314954.html) [曹工說Spring Boot原始碼(17)-- Spring從xml檔案裡到底得到了什麼(aop:config完整解析【中】)](https://www.cnblogs.com/grey-wolf/p/12317612.html) [曹工說Spring Boot原始碼(18)-- Spring AOP原始碼分析三部曲,終於快講完了 (aop:config完整解析【下】)](https://www.cnblogs.com/grey-wolf/p/12322587.html) [曹工說Spring Boot原始碼(19)-- Spring 帶給我們的工具利器,建立代理不用愁(ProxyFactory)](https://www.cnblogs.com/grey-wolf/p/12359963.html) [曹工說Spring Boot原始碼(20)-- 碼網恢恢,疏而不漏,如何記錄Spring RedisTemplate每次操作日誌](https://www.cnblogs.com/grey-wolf/p/12375656.html) [曹工說Spring Boot原始碼(21)-- 為了讓大家理解Spring Aop利器ProxyFactory,我已經拼了](https://www.cnblogs.com/grey-wolf/p/12384356.html) [曹工說Spring Boot原始碼(22)-- 你說我Spring Aop依賴AspectJ,我依賴它什麼了](https://www.cnblogs.com/grey-wolf/p/12418425.html) [工程程式碼地址](https://gitee.com/ckl111/spring-boot-first-version-learn ) [思維導圖地址](https://www.processon.com/view/link/5deeefdee4b0e2c298aa5596) 工程結構圖: ![](https://img2018.cnblogs.com/blog/519126/201912/519126-20191215144930717-1919774390.png) # 概要 spring boot原始碼系列,離上一篇,快有2周時間了,這兩週,本來是打算繼續寫這個系列的;結果中間腦熱,就去實踐了一把動態代理,實現了一個mini-dubbo這樣一個rpc框架,擴充套件性還是相當好的,今天看了下spring mvc的設計,思路差不多,都是框架提供預設的元件(比如handlermapping),然後程式裡自定義了的話,就覆蓋預設元件。 然後,因為mini-dubbo實現過程中的一些其他問題,以及工作上的需要,寫了netty實現的http 連線池,這個系列還沒講完,留著後邊再補,不然我們的原始碼系列就耽擱太久了,今天我們還是接著回來弄原始碼系列。 今天這講,主題是:給你一個class,怎麼讀取其上的註解,需要考慮註解的元註解(可以理解註解上的註解) # 讀取class上的註解 ## 常規做法 我們的Class類,就有很多獲取annotation的方法,如下: ![](https://img2020.cnblogs.com/blog/519126/202003/519126-20200320163124744-226295968.png) 但是,這個有一個問題是,無法遞迴獲取。 比如,大家使用spring的,都知道,controller這個註解上,是註解了component的。 ![](https://img2020.cnblogs.com/blog/519126/202003/519126-20200320163448356-2079088874.png) 如果你在一個標註了@controller註解的類的class上,去獲取註解,是拿不到Component這一層的。 為啥要拿Component這一層呢?你可以想一下,最開始寫spring的作者,是隻定義了Component這個註解的,業務邏輯也只能處理Component這個註解;後來呢,又多定義了@controller,@service這幾個,但是,難道要把所有業務邏輯的地方都去改一改?很明顯,你不會,大佬更不會,直接解析@controller註解,看看它的元註解有沒有@component就行了,有的話,直接複用之前的邏輯。 那麼,如何進行遞迴解析呢? ## 遞迴解析類上註解--方法1 我們要獲取的class,長這樣: ```java package org.springframework.test; @CustomController public class TestController { } @Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Controller public @interface CustomController { } ``` 這個方法,是從spring 原始碼裡摘抄的,在內部實現中,基本就這個樣子: ```java 我這個版本是4.0,在:org.springframework.bootstrap.sample.Test#recusivelyCollectMetaAnnotations public static void getAnnotationByClass(String className) throws ClassNotFoundException { Class clazz = Class.forName(className); Set metaAnnotationTypeNames = new LinkedHashSet(); for (Annotation metaAnnotation : clazz.getAnnotations()) { recusivelyCollectMetaAnnotations(metaAnnotationTypeNames, metaAnnotation); } } private static void recusivelyCollectMetaAnnotations(Set 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 這個是我自己實現的,要複雜一些,當然,是有理由的: ```java 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 result = new LinkedHashSet<>(); getAnnotationSet(result, "org.springframework.test.TestController", simpleMetadataReaderFactory); } public static void getAnnotationSet(LinkedHashSet 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 annotationTypes = annotationMetadata.getAnnotationTypes(); if (!CollectionUtils.isEmpty(annotationTypes)) { for (String annotationType : annotationTypes) { // 遞迴 getAnnotationSet(result, annotationType, simpleMetadataReaderFactory); } } } ``` 估計有的同學要罵人了,取個註解,搞一堆莫名其妙的工具類幹嘛?因為,spring就是這麼玩的啊,方法1,是spring的實現,不假。但是,那個已經是最內層了,人家外邊還封裝了一堆,封裝出來,基本就是方法2看到的那幾個類。 ##spring抽象出的註解獲取的核心介面 大家看看,就是下面這個,類圖如下: ![](https://img2020.cnblogs.com/blog/519126/202003/519126-20200320165610310-64308328.png) 其大致的功能,看下圖就知道了: ![](https://img2020.cnblogs.com/blog/519126/202003/519126-20200320170055124-1512350821.png) 這個介面,一共2個實現,簡單來說,一個是通過傳統的反射方式來獲取這些資訊,一個是通過asm的方式。 ![](https://img2020.cnblogs.com/blog/519126/202003/519126-20200320170448159-272719534.png) 兩者的優劣呢,大家可以看看小馬哥的書,裡面提到的是,asm方式的效能,遠高於反射實現,因為無需載入class,直接解析class檔案的位元組碼。 我們這裡也是主要講asm方式的實現,大家看到了上面這個asm實現的類,叫:AnnotationMetadataReadingVisitor,它的類結構,如下: ![](https://img2020.cnblogs.com/blog/519126/202003/519126-20200320170936749-1906693547.png) 從上圖可以大致知道,其繼承了ClassMetadataReadingVisitor,這個類,負責去實現ClassMetaData介面;它自己呢,就自己負責實現AnnotationMetadata介面。 我們呢,不是很關心類的相關資訊,只聚焦註解的獲取。 ##AnnotationMetadataReadingVisitor如何實現AnnotationMetadata介面 AnnotationMetadata介面,我們最關注的就是下面這2個方法: ```java // 獲取直接註解在當前class上的註解 Set getAnnotationTypes(); // 獲取某個直接註解的元註解,比如你這裡傳個controller進去,就能給你拿到controller這個註解的元註解 Set getMetaAnnotationTypes(String annotationType); ``` 大家可以看到,它呢,給了2個方法,而不是一個方法來獲取所有,可能有其他考慮吧,我們接著看。 ###getAnnotationTypes的實現 這個方法,獲取直接註解在target class上的註解。 那看看這個方法在AnnotationMetadataReadingVisitor的實現吧: ```java public Set getAnnotationTypes() { return this.annotationSet; } ``` 尷尬,看看啥時候給它賦值的: ```java @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框架,官網:
官網說明如下: > **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](https://asm.ow2.io/performance.html). 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: > > - the [**OpenJDK**](http://openjdk.java.net/), to generate the [lambda call sites](http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java), and also in the [Nashorn](https://en.wikipedia.org/wiki/Nashorn_(JavaScript_engine)) [compiler](http://hg.openjdk.java.net/jdk8/jdk8/nashorn/file/096dc407d310/src/jdk/nashorn/internal/codegen/ClassEmitter.java), >
- the [**Groovy**](http://www.groovy-lang.org/) [compiler](https://github.com/apache/groovy/blob/GROOVY_2_4_15/src/main/org/codehaus/groovy/classgen/AsmClassGenerator.java) and the [**Kotlin**](https://kotlinlang.org/) [compiler](https://github.com/JetBrains/kotlin/blob/v1.2.30/compiler/backend/src/org/jetbrains/kotlin/codegen/ClassBuilder.java), > - [**Cobertura**](http://cobertura.github.io/cobertura/) and [**Jacoco**](http://www.eclemma.org/jacoco/), to [instrument](https://github.com/cobertura/cobertura/blob/v1_9_4/src/net/sourceforge/cobertura/instrument/ClassInstrumenter.java) [classes](https://github.com/jacoco/jacoco/blob/v0.8.1/org.jacoco.core/src/org/jacoco/core/instr/Instrumenter.java) in order to measure code coverage, > - [**CGLIB**](https://github.com/cglib/cglib), to dynamically generate [proxy](https://github.com/cglib/cglib/blob/RELEASE_3_2_6/cglib/src/main/java/net/sf/cglib/core/ClassEmitter.java) classes (which are used in other projects such as [**Mockito**](http://site.mockito.org/) and [**EasyMock**](http://easymock.org/)), > - [**Gradle**](https://gradle.org/), to [generate](https://github.com/gradle/gradle/blob/v4.6.0/subprojects/core/src/main/java/org/gradle/api/internal/AsmBackedClassGenerator.java) some classes at runtime. 簡單來說,就是: > 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,其大概的解析步驟如下: ```java 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裡面定義了一堆的回撥方法: ```java 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(); } ``` 這其中,方法的訪問順序如下: ```java visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )* ( visitInnerClass | visitField | visitMethod )* visitEnd 代表: visit必須最先訪問; 接著是最多一次的visitSource,再接著是最多一次的visitOuterClass; 接著是任意多次的visitAnnotation | visitAttribute ,這兩個,順序隨意; 再接著是,任意多次的visitInnerClass | visitField | visitMethod ,順序隨意 最後,visitEnd ``` 這個順序的? * () 等符號,其實類似於正則表示式的語法,對吧,還是比較好理解的。 然後呢,我對visitor的理解,現在感覺類似於spring裡面的event listener機制,比如,spring的生命週期中,釋出的事件,有如下幾個,其實也是有順序的: ![](https://img2020.cnblogs.com/blog/519126/202003/519126-20200320213833325-507465411.png) 這裡還有官網提供的一個例子: ```java 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的回撥處理 ![](https://img2020.cnblogs.com/blog/519126/202003/519126-20200320180210678-1630463533.png) ![](https://img2020.cnblogs.com/blog/519126/202003/519126-20200320175913871-245817836.png) 我們接著回到getAnnotationTypes的實現,大家看了上面2個圖,應該大致知道visitAnnotation的實現了: ```java @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訪問註解的具體屬性時,其中的如下方法被回撥。 ```java @Override public void doVisitEnd(Class annotationClass) { super.doVisitEnd(annotationClass); List attributes = this.attributesMap.get(this.annotationType); if(attributes == null) { this.attributesMap.add(this.annotationType, this.attributes); } else { attributes.add(0, this.attributes); } Set metaAnnotationTypeNames = new LinkedHashSet(); // 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 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 ```java @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Controller ``` 2. 迴圈處理這些元註解,因為這些元註解上,可能還有元註解,比如,在處理Target時,發現其上還有Documented、Retention、Target幾個註解,看到了吧,target註解還註解了target,在這塊的遞迴處理時,很容易棧溢位。 ```java @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { ``` 3. 遞迴處理上面的這些註解 具體的處理,基本就是這樣。文章開頭的遞迴,就是摘抄的這裡的程式碼。 經過最終的處理後,可以看看最後的效果,這裡擷取的就是AnnotationMetadataReadingVisitor這個物件: ![](https://img2020.cnblogs.com/blog/519126/202003/519126-20200320214500391-1729886720.png) # 總結 這個就是spring 註解驅動的基石,實際上,spring不是一開始就這麼完備的,在之前的版本,並不支援遞迴獲取,spring也是慢慢一步一步發展壯大的。 感謝spring賞飯吃! 下一講,會講解component-scan掃描bean時,怎麼掃描類上的注