注解( annontation )是 Java 1.5 之后引入的一個為程序添加元數據的功能。注解本身并不是魔法,只是在代碼里添加了描述代碼自身的信息,至于如何理解和使用這些信息,則需要專門的解析代碼來負責。
本文首先介紹注解的基本知識,包括注解的分類和運用時的領域知識。隨后,給出一個通過的在運行時解析注解的框架代碼,介紹處理注解的一般思路。最后,通過現實世界里使用注解的例子,來加深對注解的實用性方面的認識。
注解的基本知識
注解作為程序中的元數據,其本身的性質也被其上的注解所描述。
剛剛我們提到,理解和使用注解信息,需要專門的解析代碼。其中,Java 的編譯器和虛擬機也包含解析注解信息的邏輯,而它們判斷一個注解的性質,就是依賴注解之上的元注解。
能夠注解一個注解的注解就是元注解,Java 本身能夠識別的元注解有以下幾個。
@Retention
Retention 注解的相關定義如下
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {RetentionPolicy value();
}public enum RetentionPolicy {SOURCE,CLASS,RUNTIME
}
首先我們看到它自己也被幾個元注解包括自身所注解,因此在注解的源頭有一個類似于自舉的概念,最終觸發自舉的是編譯器和源代碼中的先驗知識。
再看到 Retention 注解的值,是一個注解保留性質的枚舉,包括三種情況。
- SOURCE 表示注解信息僅在編譯時保留,在編譯之后就被丟棄,這樣的注解為代碼的編譯提供原信息。例如常用的
@Override
注解就提示 Java 編譯器進行重寫方法的檢查。 - CLASS 表示注解信息保留在字節碼中,但在運行時不可見。這是注解的默認行為,如果定義注解時沒有使用 Retention 注解顯式表明保留性質,默認的保留性質就是這個。
- RUNTIME 表示注解信息在運行時可見,當然,也就必須保留在字節碼中。
SOURCE 標注的注解通常稱為編譯期注解,Lombok 項目提供大量的編譯期注解,以幫助開發者簡寫自己的代碼。例如 @Setter
注解注解在類上時,在編譯期由 Lombok 的注解處理器處理,為被注解的類的每一個字段生成 Setter 方法。
編譯期的注解需要專門的注解處理器來處理,并且在編譯時指定處理器的名字提示編譯期使用該處理器進行處理。技術上說,編譯期處理注解和運行時處理注解完全是兩個概念的事情。本文主要介紹運行時處理注解的技術,關于編譯期處理注解的資料,可以參考這篇 ANNOTATION PROCESSING 101 的文章以及 Lombok 的源碼。
CLASS 性質雖然是默認的保留性質,但實際使用中幾乎沒有采用這一保留性質的。準確需要這一性質的情形應該是某些專門的字節碼處理框架,大多數時候使用這一性質的注解僅僅是在編譯期使用,使用 SOURCE 足以,且使用 SOURCE 還可以減少字節碼文件的大小。
本文介紹運行時處理注解的技術,所有在運行時可見的注解都需要顯式地標注 @Retention(RetentionPolicy.RUNTIME)
注解。CLASS 和 RUNTIME 性質的注解都會出現在字節碼中。編譯器將注解信息寫成字節碼時,通過為 CLASS 性質的注解賦予 RuntimeInvisibleAnnotations 屬性,為 RUNTIME 性質的注解賦予 RuntimeVisibleParameterAnnotations 來提示虛擬機在運行時加載的時候區別對待。
運行時,我們可以調用被注解對象的相應方法取得其上的注解,具體手段在【注解解析的框架代碼】一節中介紹。
@Target
上一節最后我們提到,注解有不同的注解對象,這正是 Target 注解加入的元數據,其定義如下
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {ElementType[] value();
}public enum ElementType {TYPE,FIELD,METHOD,PARAMETER,CONSTRUCTOR,LOCAL_VARIABLE,ANNOTATION_TYPE,PACKAGE,TYPE_PARAMETER,TYPE_USE,MODULE
}
Target 元注解的信息解釋了一個注解能夠被注解在什么位置上,或者說能夠接受該注解的對象集合。一個注解可以有多種類型的注解對象,所有這些對象類型存在 ElementType 枚舉中。
大多數枚舉值的含義就是字面含義,值得一提的取值包括
- TYPE 在 Java 中指類、接口、注解或者枚舉類
- TYPE_PARAMETER 在 Java 1.8 中被引入,指的是泛型中的類型參數
- TYPE_USE 在 Java 1.8 中被引入,指的是所有可以出現類型的位置,具體參考 Java 語言標準的對應章節
常見的 Override 注解只能注解在方法上,Spring 框架中的 Component 注解只能注解在類型上。SuppressWarnings 注解能注解在除了本地變量和類型參數以外的幾乎所有地方,Spring 框架中的 Autowired 注解也能注解在字段、構造器、方法參數和注解等多種位置上。
@Inherited
Inherited 主要用來標注注解在類繼承關系之間的傳遞關系。它本身不攜帶自定義信息,僅作為一個布爾信息存在,即是或者不是 Inherited 的注解。
標注 Inherited 元注解的注解,標注在某個類型上時,其子類也默認視為標注此注解。或者換個方向說,獲取某個類的注解時,會遞歸的搜索其父類的注解,并獲取其中標注 Inherited 元注解的注解。注意,標注 Inherited 元注解的注解在子類上也標注時,子類上的注解優先級最高。
技術上說,可以通過 getAnnotations
和 getDeclaredAnnotations
的區別來獲取確切標注在當前類型上的注解和按照上面描述的方法查找的注解。另一個值得強調的是這種繼承僅發生在類的繼承上,實現接口并不會導致標注 Inherited 元注解的注解的傳遞。
值得注意的是,注解本身是不能繼承的。為了實現類似繼承的效果,開發者們從基于原型的繼承找到靈感,采用本節后續將講到的組合注解技術來達到注解繼承的目的。
@Repeatable
Repeatable 注解在 Java 1.8 中被引入,主要是為了解決相同的注解只能出現一次的情況下,為了表達實際中需要的相同注解被標注多次的邏輯,開發者不得不首先創建出一個容器注解,然后使用者在單個和多個注解的情況下分別使用基礎注解和容器注解的繁瑣邏輯。具體例子如下
@ComponentScan(basePackages = "my.package")
class MySimpleConfig { }@ComponentScans({ @ComponentScan(basePackages = "my.package") @ComponentScan(basePackages = "my.another.package")
})
class MyCompositeConfig { }
有了 Repeatable 注解,從注解處理方,代碼不會精簡,仍然需要分開處理兩種注解類型,但是使用方就可以精簡代碼。例如上面 MyCompositeConfig 的標注可以變為
@ComponentScan(basePackages = "my.package")
@ComponentScan(basePackages = "my.another.package")
class MyCompositeConfig { }
對應的注解定義為
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {// ...
}@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface ComponentScans {ComponentScan[] value();
}
對于注解的處理方,重復注解會在背后由 Java 編譯器轉化為容器注解的形式傳遞。就上面的例子而言,無論有沒有 Repeatable 注解,MyCompositeConfig 在獲取注解時,都會獲取到 ComponentScans 注解及其 ComponentScan[] 形式的元數據信息。
值得注意的是,重復注解和容器注解不能同時存在,即在標記了 @Repeatable(ComponentScans.class)
之后,ComponentScans
和 ComponentScan
不能同時標注同一個對象。
@Documented
這個注解沒有太多好說的,注解信息在生成文檔時默認是不會留存的。如果使用此注解標注某個注解,那么被標注的注解注解的對象的文檔會顯示它被對應的注解所標注。
組合注解
嚴格來說,組合注解是一種設計模式而不是語言特性。
由于注解無法繼承,例如 Spring 框架中具有 "is-a" 關系的 Service 注解和 Component 注解,無法通過繼承將 Service 定義為 Component 的特例。但是在實際使用的時候,又確實有表達這樣 "is-a" 關系的需求。
在框架代碼中,無法窮盡對下游項目擴展注解實質上的繼承關系的情況,但是又需要支持下游項目自定義框架注解的擴展。如何將下游項目自定義的注解和框架注解之間的繼承關系表達出來,就是一個技術上實際的需求。
為了解決這個問題,開發者們注意到在注解設計之初,留下了注解能夠標注注解的路徑。這一路徑使得我們可以采用一種類似基于原型的繼承的方式,通過遞歸獲取注解上的注解來追溯注解的鏈條,從而類似原型鏈上找父類的方式找到當前注解邏輯上繼承的注解。
這一技術在 Spring 框架中被廣泛使用,例如 Service/Repository/Controller 等注解組合了 Component 注解,從而在下一節的注解解析的框架代碼中能夠作為 Component 的某種意義上的子注解被識別,同時在需要時取出繼承的注解的元數據信息。
注解解析的框架代碼
Java 語言提供的方法
注解解析最基礎的手段是通過 Java 語言本身提供的方法。哪怕是其他框架增強注解解析的功能,最終也需要依賴基本方法的支持。
運行時獲取注解信息,可想而知是通過反射的手段來獲取的。Java 為被注解的元素定義了一個 AnnotatedElement
的接口,通過這一接口的方法可以在運行時取得被注解元素之上的注解。該接口的實現類是運行時通過反射拿到的元素里面能夠被注解的類。
我們先看到這一接口提供的方法。
public interface AnnotatedElement {<T extends Annotation> T getAnnotation(Class<T> annotationClass);Annotation[] getAnnotations();<T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass);Annotation[] getDeclaredAnnotations();<T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass);<T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass);boolean isAnnotationPresent(Class<? extends Annotation> annotationClass);
}
這些方法沒必要一個一個講,其實可以簡單地分成兩類
- 獲取被注解對象上聲明的注解,即
getDeclaredAnnotations
系列的方法 - 獲取被注解對象所擁有的注解,即
getAnnotations
系列的方法,比起上一類,額外包括@Inherited
的注解
最后 isAnnotationPresent 方法僅僅是一個判斷標簽式注解的簡易方法,內容只有一行。
default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {return getAnnotation(annotationClass) != null;
}
我們可以通過 Java 語言自身的 AnnotationSupport#getIndirectlyPresent
方法來看看怎么用這套基礎支持解析注解。
private static <A extends Annotation> A[] getIndirectlyPresent(Map<Class<? extends Annotation>, Annotation> annotations,Class<A> annoClass
) {Repeatable repeatable = annoClass.getDeclaredAnnotation(Repeatable.class);if (repeatable == null)return null; // Not repeatable -> no indirectly present annotationsClass<? extends Annotation> containerClass = repeatable.value();Annotation container = annotations.get(containerClass);if (container == null)return null;// Unpack containerA[] valueArray = getValueArray(container);checkTypes(valueArray, container, annoClass);return valueArray;
}
以上這段代碼是在 Java 1.8 引入 Repeatable 注解后,由于默認的會將重復的 Repeatable 的注解在獲取時直接合并成容器注解,為了提供一個方便的按照基礎注解來獲取注解信息的手段提供的方法。
我們看到,傳入的內容包括一個根據 Class 對象查找實現類對象的映射,這個是被注解類所取得的所擁有的注解的類到實例的字典,不用過多關注。另一方面 annoClass 則是我們想要獲取的基礎注解的類。
例如,annoClass 為上面提過的 Spring 的 ComponentScan
類,對于僅注解了 ComponentScans
的類來說,以 ComponentScan.class
作為參數調用 getDeclaredAnnotationsByType
方法一路走到上面這個方法里,代碼邏輯將會看到 ComponentScan
標注了 @Repeatable(ComponentScans.class)
注解,從而在 annotations
映射里查找 ComponentScans
注解的信息,并將它轉換為 ComponentScan
的數組返回。
Spring 解析注解的方案
Spring 解析注解的核心是 MergedAnnotation
接口及相關的工具類。
Spring 框架重度使用了注解來簡化開發的復雜度。對于具體的某一個或某幾個注解,圍繞它展開的代碼散布在其邏輯鏈條的各處。但是,Spring 的注解處理的特別之處就在于它定義了 MergedAnnotation
接口,并支持了基于組合注解和 AliasFor
的注解增強機制。
AliasFor 注解的解析非常簡單,就是查看當前注解或者 targetAnnotation 注解里面相應名稱的注解。在 5.2.7.RELEASE 版本中,其解析邏輯基本在 AnnotationTypeMapping#resolveAliasTarget
方法里,最終組裝出來的 AnnotationTypeMapping 對象能夠在獲取屬性值的時候顯示處理了 AliasFor 之后的屬性值。
下面我們展開說一下如何遞歸解析組合注解。
為了支持前面提到的組合注解,即注解上的注解的遞歸查找,Spring 中提供了 AnnotationUtils#findAnnotation
系列方法來做查詢,區別于 AnnotationUtils#getAnnotation
的單層查找。
Spring 對這個查找邏輯的演化花了很多心思。
在最新的 Spring 5.2.7.RELEASE 版本中,這兩個方法都對 AnnotatedElement 構造了 MergedAnnotation 實例,在最終查找的時候通過不同的謂詞策略來做篩選。構造 MergedAnnotation 實例的過程經由幾個工廠函數之后構造出一個 TypeMappedAnnotations 的實例,調用其上的 get 方法構造出實際的 MergedAnnotation 對象,這個對象就是對要查找的注解遞歸查找的結果。
相關邏輯為了定制各種策略變得非常復雜,我們從 4.3.8.RELEASE 版本入手,查看在復雜的定制引入之前,這一查找過程核心邏輯的實現框架。
Annotation[] anns = clazz.getDeclaredAnnotations();
for (Annotation ann : anns) {if (ann.annotationType() == annotationType) {return (A) ann;}
}
for (Annotation ann : anns) {if (!isInJavaLangAnnotationPackage(ann) && visited.add(ann)) {A annotation = findAnnotation(ann.annotationType(), annotationType, visited);if (annotation != null) {return annotation;}}
}
無論后期代碼演化得再復雜,其核心還是一個遞歸查找的過程,也就是以上的代碼。
- 首先,獲取當前的類上的注解,注意這里的類可以是一個注解類,如果此次獲取的注解就包含了我們要查找的注解,那么直接返回。
- 如果沒有包含,對剛才取得的注解遞歸的查找。注意這里有一個類似于深度優先搜索的 visited 集合。這是因為有些注解可以以自己為目標,導致出現遞歸查找的自環。典型的例如 Java 自帶的元注解 Retention 也被自己所注解。
- 如果深度優先搜索窮盡之后沒有得到結果,則返回空。
可以看到,上面的邏輯中對 Repeatable 和 Inherited 等元注解的復雜組合情況沒有定制的邏輯,而是采用了一些默認的硬編碼策略。
最新版本的 Spring 之所以變得相當復雜,有一部分代碼量是為了解決搜索的不同策略以及跟進新版 Java 的注解特性。另一部分,注意到上述邏輯在獲取注解時沒有關心 AliasFor 注解的邏輯,在早期版本中這是由 AnnotationUtils 中的一個全局靜態映射來管理的。在最新版本中,產生 MergedAnnotation 時將構造并維護一個本地的 alias 映射。
現實世界的注解解析
上一節介紹了處理注解的兩個通用套路,背后的思想是基礎的注解信息獲取和遞歸的注解信息獲取。本節我們將從現實世界的注解解析入手,介紹實際項目里面特定的注解是如何被解析的。
Flink
@RpcTimeout
Flink 采用類似 RMI 的方式來進行遠程調用,為了避免無限阻塞,方法調用時可以傳遞一個超時參數。本地攔截遠端調用的動作時,從方法的簽名中反射取得標注 RpcTimeout 的參數,將它作為超時參數傳遞到實際的方法調用過程中,以在超過限定時間時返回超時異常而非阻塞等待遠端調用的返回。
取得標注 RpcTimeout 的參數的邏輯代碼展開如下
final Annotation[][] parameterAnnotations = method.getParameterAnnotations();for (int i = 0; i < parameterAnnotations.length; i++) {for (Annotation annotation : parameterAnnotations[i]) {if (annotation.annotationType().equals(RpcTimeout.class)) {if (args[i] instanceof Time) {return (Time) args[i];} else {throw new RuntimeException(/* ... */)}}}
}return defaultTimeout;
可以看到,是針對先驗知識能得知的可能出現該注解的位置進行遍歷獲取。其實,所有的注解解析代碼都遵循這樣的模式,這也是最基礎的模式。
JUnit 4
@Test
JUnit 4 測試框架的用戶最熟悉的就是 Test 注解了。不同于上一節提到的基礎解析和遞歸解析,JUnit 4 的 Test 注解有一個特殊的場景需要支持,即在獲取當前類的所有待測試方法時,獲取到父類中的 Test 標注的方法。
這是因為我們常常把相似的測試的配置和基礎測試方法抽成抽象基類,在根據不同的實現場景實現不同的測試子類。雖然類似的功能可以用 Parameterized Runner 和 Parameter 注解來實現,但是 Parameter 的方案只能支持參數化字段,如果測試方法是有和沒有的區別而不是參數的不同,子類是比使用 Parameter 向量并加入 Enable 開關更好的解決方案。
總之,JUnit 4 支持查找父類中標注 Test 的其他方法,此邏輯實現如下。
// TestClass#scanAnnotatedMembers
for (Class<?> eachClass : getSuperClasses(clazz)) {for (Method eachMethod : MethodSorter.getDeclaredMethods(eachClass)) {addToAnnotationLists(new FrameworkMethod(eachMethod), methodsForAnnotations);}// ensuring fields are sorted to make sure that entries are inserted// and read from fieldForAnnotations in a deterministic orderfor (Field eachField : getSortedDeclaredFields(eachClass)) {addToAnnotationLists(new FrameworkField(eachField), fieldsForAnnotations);}
}// TestClass#addToAnnotationLists
for (Annotation each : member.getAnnotations()) {Class<? extends Annotation> type = each.annotationType();List<T> members = getAnnotatedMembers(map, type, true);T memberToAdd = member.handlePossibleBridgeMethod(members);if (memberToAdd == null) {return;}if (runsTopToBottom(type)) {members.add(0, memberToAdd);} else {members.add(memberToAdd);}
}
其實也很簡單,在初始化 TestClass 時遍歷測試的候選類及其父類的所有方法和字段,將它們的注解信息存在一個注解類型到被注解對象的列表的映射中。后續需要查找的時候從該映射查找,即可查找到標注對應注解的所有方法或字段。
@RunWith
另一個常見的注解是 RunWith 注解,用于標注運行測試時采用自定義的 Runner 實現。其代碼如下
for (Class<?> currentTestClass = testClass; currentTestClass != null;currentTestClass = getEnclosingClassForNonStaticMemberClass(currentTestClass)) {RunWith annotation = currentTestClass.getAnnotation(RunWith.class);if (annotation != null) {return buildRunner(annotation.value(), testClass);}
}
可以看到,是從內到外層層查找的形式。注意這里沒有去查找父類的 RunWith 注解,這是由于 RunWith 注解本身被 @Inherited
所標注,調用 Java 提供的基礎方法獲取類的注解時已經做了相應的處理。
Spring
@SpringBootApplication
SpringBootApplication 可以說是最好的解釋 Spring 中重度使用組合注解的例子了。對于這一注解的解析,我們甚至不需要或者說不能列舉出任何解析代碼,因為 SpringBootApplication 從來沒有作為它自己被解析。該注解的定義如下
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {@AliasFor(annotation = EnableAutoConfiguration.class)Class<?>[] exclude() default {};@AliasFor(annotation = EnableAutoConfiguration.class)String[] excludeName() default {};@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")String[] scanBasePackages() default {};@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")Class<?>[] scanBasePackageClasses() default {};@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;@AliasFor(annotation = Configuration.class)boolean proxyBeanMethods() default true;
}
這里有兩件事情值得關注,分別對應介紹 Spring 的注解解析框架的時候指出的 Spring 的兩個關鍵的增強
- 實際使用 SpringBootApplication 時,Spring 框架的解析代碼是通過 findAnnotation 查找其組合的注解來實現具體功能的。
- SpringBootApplication 通過 AliasFor 支持用戶在使用該注解時覆蓋其所組合的注解的屬性。
從這里我們也看出,組合注解僅僅是一種形式上相關聯的組合,與任一形式的繼承不同,不會以某種形式繼承屬性。
@Autowired
Autowired 可以說是 Spring 框架中使用最為廣泛的注解之一了,它和 Value 注解以及 JSR-330 的 Inject 注解一起組成了注入 Bean 的核心手段。
Autowired 的處理邏輯在 AutowiredAnnotationBeanPostProcessor 中,即 Bean 被創造和加載之后的一個后處理邏輯或者成為裝飾邏輯。其中涉及到 Autowired 等注解的地方主要是篩選出需要為目標注入 Bean 的候選。
首先,在初始化的時候,會將對應的 Autowired 系列注解保存到 autowiredAnnotationTypes 集合字段中。
隨后,當 Bean 處理框架調用后處理邏輯時,調用后處理器的 findAutowiringMetadata 方法,通過標記型注解找到需要 Autowired 的候選。整個過程通過反射將被 Autowired 注解的對象及 Autowired 注解中持有的是否必須( required )的信息保存到 InjectElement 中。
再之后,對獲取到的所有 InjectElement 調用 inject 方法進行注入。根據不同的被注入對象,注入的邏輯有所不同。例如,對于字段的注入,由 AutowiredFieldElement 對象處理,從 BeanFactory 中根據依賴關系初始化 Bean 并將 Bean 賦值給字段。
這一套邏輯支持了 Bean 注入最常用的字段注入的功能,以及運行配置方法的功能。
Autowired 注解還能被用在參數和構造函數上,其中參數上的標注目前僅用于在 JUnit Jupiter 框架測試時使用,而構造函數的標注廣泛替代了直接標注字段的用法,其代碼路徑存在于 AbstractAutowireCapableBeanFactory 創建 Bean 實例的時候。從結果來說,標注在構造函數的 Autowired 能夠將參數對應類型的 Bean 作為構造函數的實參,調用構造函數以構造出對象。