1. 程式人生 > >Spring中的@AliasFor標籤

Spring中的@AliasFor標籤

在Spring的眾多註解中,經常會發現很多註解的不同屬性起著相同的作用,比如@RequestMapping的value屬性和path屬性,這就需要做一些基本的限制,比如value和path的值不能衝突,比如任意設定value或者設定path屬性的值,都能夠通過另一個屬性來獲取值等等。為了統一處理這些情況,Spring建立了@AliasFor標籤。

使用 @AliasFor標籤有幾種使用方式。

1,在同一個註解內顯示使用;比如在@RequestMapping中的使用示例:

@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Mapping public @interface RequestMapping {       @AliasFor("path")     String[] value() default {};       @AliasFor("value")     String[] path() default {};       //... } 又比如@ContextConfiguration註解中的value和locations屬性:

@Documented @Inherited @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface ContextConfiguration {     @AliasFor("locations")     String[] value() default {};       @AliasFor("value")     String[] locations() default {};     //... } 在同一個註解中成對使用即可,比如示例程式碼中,value和path就是互為別名。但是要注意一點,@AliasFor標籤有一些使用限制,但是這應該能想到的,比如要求互為別名的屬性屬性值型別,預設值,都是相同的,互為別名的註解必須成對出現,比如value屬性添加了@AliasFor(“path”),那麼path屬性就必須新增@AliasFor(“value”),另外還有一點,互為別名的屬性必須定義預設值。

那麼如果違反了別名的定義,在使用過程中就會報錯,我們來做個簡單測試:

@ContextConfiguration(value = "aa.xml", locations = "bb.xml") public class AnnotationUtilsTest {     @Test     public void testAliasfor() {         ContextConfiguration cc = AnnotationUtils.findAnnotation(getClass(),                 ContextConfiguration.class);         System.out.println(                 StringUtils.arrayToCommaDelimitedString(cc.locations()));         System.out.println(StringUtils.arrayToCommaDelimitedString(cc.value()));     } }

執行測試,報錯;value和locations互為別名,不能同時設定;

稍微調整一下程式碼:

@MyAnnotation @ContextConfiguration(value = "aa.xml", locations = "aa.xml") public class AnnotationUtilsTest { 或者

@MyAnnotation @ContextConfiguration(value = "aa.xml") public class AnnotationUtilsTest { 執行測試,均打印出:

aa.xml aa.xml 2,顯示的覆蓋元註解中的屬性; 先來看一段程式碼:

@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = AopConfig.class) public class AopUtilsTest { 這段程式碼是一個非常熟悉的基於JavaConfig的Spring測試程式碼;假如現在我有個癖好,我覺得每次寫@ContextConfiguration(classes = AopConfig.class)太麻煩了,我想寫得簡單一點,我就可以定義一個這樣的標籤:

@Retention(RetentionPolicy.RUNTIME) @ContextConfiguration public @interface STC {       @AliasFor(value = "classes", annotation = ContextConfiguration.class)     Class<?>[] cs() default {};   } 1,因為@ContextConfiguration註解本身被定義為@Inherited的,所以我們的STC註解即可理解為繼承了@ContextConfiguration註解; 2,我覺得classes屬性太長了,所以我建立了一個cs屬性,為了讓這個屬性等同於@ContextConfiguration屬性中的classes屬性,我使用了@AliasFor標籤,分別設定了value(即作為哪個屬性的別名)和annotation(即作為哪個註解);

使用我們的STC:

@RunWith(SpringJUnit4ClassRunner.class) @STC(cs = AopConfig.class) public class AopUtilsTest {       @Autowired     private IEmployeeService service; 正常執行; 這就是@AliasFor標籤的第二種用法,顯示的為元註解中的屬性起別名;這時候也有一些限制,比如屬性型別,屬性預設值必須相同;當然,在這種使用情況下,@AliasFor只能為作為當前註解的元註解起別名;

3,在一個註解中隱式宣告別名; 這種使用方式和第二種使用方式比較相似,我們直接使用Spring官方文件的例子:

 @ContextConfiguration  public @interface MyTestConfig {       @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")     String[] value() default {};       @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")     String[] groovyScripts() default {};       @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")     String[] xmlFiles() default {};  } 可以看到,在MyTestConfig註解中,為value,groovyScripts,xmlFiles都定義了別名@AliasFor(annotation = ContextConfiguration.class, attribute = “locations”),所以,其實在這個註解中,value、groovyScripts和xmlFiles也互為別名,這個就是所謂的在統一註解中的隱式別名方式;

4,別名的傳遞; @AliasFor註解是允許別名之間的傳遞的,簡單理解,如果A是B的別名,並且B是C的別名,那麼A是C的別名; 我們看一個例子:

 @MyTestConfig  public @interface GroovyOrXmlTestConfig {       @AliasFor(annotation = MyTestConfig.class, attribute = "groovyScripts")     String[] groovy() default {};       @AliasFor(annotation = ContextConfiguration.class, attribute = "locations")     String[] xml() default {};  } 1,GroovyOrXmlTestConfig把 @MyTestConfig(參考上一個案例)作為元註解; 2,定義了groovy屬性,並作為MyTestConfig中的groovyScripts屬性的別名; 3,定義了xml屬性,並作為ContextConfiguration中的locations屬性的別名; 4,因為MyTestConfig中的groovyScripts屬性本身就是ContextConfiguration中的locations屬性的別名;所以xml屬性和groovy屬性也互為別名; 這個就是別名的傳遞性;

原理 明白@AliasFor標籤的使用方式,我們簡單來看看@AliasFor標籤的使用原理; 首先來看看該標籤的定義:

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface AliasFor {       @AliasFor("attribute")     String value() default "";       @AliasFor("value")     String attribute() default "";       Class<? extends Annotation> annotation() default Annotation.class;   } 可以看到,@AliasFor標籤自己就使用了自己,為value屬性添加了attribute屬性作為別名;

那麼就把這個註解放在我們需要的地方就可以了麼?真就這麼簡單麼?我們來做一個例子: 1,建立一個註解

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MyAnnotation {       @AliasFor("alias")     String value() default "";       @AliasFor("value")     String alias() default "";   } 在該註解中,我們讓alias和value屬性互為別名; 2,完成測試類:

@MyAnnotation(value = "aa", alias = "bb") public class AnnotationUtilsTest {       @Test     public void testAliasfor2() {         MyAnnotation ann = getClass().getAnnotation(MyAnnotation.class);         System.out.println(ann.value());         System.out.println(ann.alias());     } } 我們將MyAnnotation放在AnnotationUtilsTest上,可以看到,我們故意將value和alias值設定為不一樣的,然後在測試程式碼中分別獲取value()和alias()的值,結果列印: aa bb

WTF?和預期的不一樣?原因很簡單,AliasFor是Spring定義的標籤,要使用他,只能讓Spring來處理,修改測試程式碼:

@MyAnnotation(value = "aa", alias = "bb") public class AnnotationUtilsTest {       @Test     public void testAliasfor3() {         MyAnnotation ann = AnnotationUtils.findAnnotation(getClass(),                 MyAnnotation.class);         System.out.println(ann.value());         System.out.println(ann.alias());     } } 這次我們使用Spring的AnnotationUtils工具類的findAnnotation方法來獲取標籤,然後再次列印value()和alias()值:

如願報錯;所以,使用@AliasFor最需要注意一點的,就是隻能使用Spring的AnnotationUtils工具類來獲取;

而真正在起作用的,是AnnotationUtils工具類中的<A extends Annotation> A synthesizeAnnotation(A annotation, AnnotatedElement annotatedElement)方法; 這個方法傳入註解物件,和這個註解物件所在的型別,返回一個經過處理(這個處理就主要是用於處理@AliasFor標籤)之後的註解物件,簡單說,這個方法就是把A註解物件—-(經過處理)——>支援AliasFor的A註解物件,我們來看看其中的關鍵程式碼:

DefaultAnnotationAttributeExtractor attributeExtractor =     new DefaultAnnotationAttributeExtractor(annotation, annotatedElement); InvocationHandler handler =      new SynthesizedAnnotationInvocationHandler(attributeExtractor); return (A) Proxy.newProxyInstance(annotation.getClass().getClassLoader(),     new Class<?>[] {(Class<A>) annotationType, SynthesizedAnnotation.class}, handler); 可以看到,本質原理就是使用了AOP來對A註解物件做了次動態代理,而用於處理代理的物件為SynthesizedAnnotationInvocationHandler;我們來看看SynthesizedAnnotationInvocationHandler中的重要處理程式碼:

@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {     if (isEqualsMethod(method)) {         return annotationEquals(args[0]);     }     if (isHashCodeMethod(method)) {         return annotationHashCode();     }     if (isToStringMethod(method)) {         return annotationToString();     }     if (isAnnotationTypeMethod(method)) {         return annotationType();     }     if (!isAttributeMethod(method)) {         String msg = String.format("Method [%s] is unsupported for synthesized annotation type [%s]", method,             annotationType());         throw new AnnotationConfigurationException(msg);     }     return getAttributeValue(method); } 在invoke(即攔截方法中,這個攔截方法就是在註解中獲取屬性值的方法,不要忘了,註解的屬性實際上定義為介面的方法),其次判斷,如果當前執行的方法不是equals、hashCode、toString、或者屬性是另外的註解,或者不是屬性方法,之外的方法(這些方法就是要處理的目標屬性),都呼叫了getAttributeValue方法,所以我們又跟蹤到getAttributeValue方法的重要程式碼:

String attributeName = attributeMethod.getName();         Object value = this.valueCache.get(attributeName);         if (value == null) {             value = this.attributeExtractor.getAttributeValue(attributeMethod); 這裡我們重點關注的是如果沒有快取到值(這個先不用管),直接呼叫attributeExtractor.getAttributeValue方法獲取屬性值,那麼,很容易猜到,如果屬性有@AliasFor註解,就應該是這個方法在處理;那我們來看看這個方法又在做什麼事情:

attributeExtractor是一個AnnotationAttributeExtractor型別,這個物件是在構造SynthesizedAnnotationInvocationHandler時傳入的,預設是一個DefaultAnnotationAttributeExtractor物件;而DefaultAnnotationAttributeExtractor是繼承AbstractAliasAwareAnnotationAttributeExtractor,看名字,真正的處理AliasFor標籤的動作,應該就在這裡面,於是繼續看程式碼:

public final Object getAttributeValue(Method attributeMethod) {         String attributeName = attributeMethod.getName();         Object attributeValue = getRawAttributeValue(attributeMethod);           List<String> aliasNames = this.attributeAliasMap.get(attributeName);         if (aliasNames != null) {             Object defaultValue = AnnotationUtils.getDefaultValue(getAnnotationType(), attributeName);             for (String aliasName : aliasNames) {                 Object aliasValue = getRawAttributeValue(aliasName);                   if (!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) &&                         !ObjectUtils.nullSafeEquals(attributeValue, defaultValue) &&                         !ObjectUtils.nullSafeEquals(aliasValue, defaultValue)) {                     throw new AnnotationConfigurationException(...)                 }                 if (ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) {                     attributeValue = aliasValue;                 }             }         }           return attributeValue;     } 對原始碼做了些改造,但是我們能清晰的看到重點: 1,首先正常獲取當前屬性的值; 2,List<String> aliasNames = this.attributeAliasMap.get(attributeName);得到所有的標記為別名的屬性名稱; 3,Object aliasValue = getRawAttributeValue(aliasName);遍歷獲取所有別名屬性的值; 4,三個重要判斷,attributeValue、aliasValue、defaultValue相同,我們前面介紹的@AliasFor標籤的傳遞性也是在這裡體現;如果不相同,直接丟擲異常;否則正常返回屬性值;

至此,AliasFor的執行過程分析完畢;

小結 類似@AliasFor這樣的註解,在Spring框架中比比皆是,而每一個這樣的細節的點,都值得我們去體會。我們常常說Spring框架非常複雜,因為在每一個點的實現,都要考慮很多健壯性和擴充套件性的問題,這些,都是我們值得去研究的。