@Qualifier高級應用---按類別批量依賴注入【享學Spring】


每篇一句

羅斯:選秀狀元可能有水貨,但MVP絕對沒有

前言

上篇文章(講解@LoadBalanced負載均衡)的末尾,我拋出了一個很重要的問題,建議小伙伴自己深入思考一番;本文主要針對此問題,作出一個統一的答復和講解。
由於本人覺得這塊知識點它屬於Spring Framework的核心內容之一,非常的重要,因此單拎出來作專文講述,希望對你有所幫助。

背景案例

說到@Qualifier這個注解大家並不陌生:它用於“精確匹配”Bean,一般用於同一類型的Bean有多個不同實例的case下,可通過此注解來做鑒別和匹配。
本以為@Qualifier注解使用在屬性上、類上用於鑒別就夠了,直到我看到LoadBalancerAutoConfiguration里有這么應用:

@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();	

它能把容器內所有RestTemplate類型並且標注有@LoadBalanced注解的Bean全注入進來
這個用法讓我非常的驚喜,它給我提供額外一條思路,讓我的框架多了一種玩法。為了融匯貫通它,使用起來盡量避免不采坑,那就只能揭開它,從底層原理處理解它的用法了。


QualifierAnnotationAutowireCandidateResolver詳解

它是依賴注入候選處理器接口AutowireCandidateResolver的實現類,繼承自GenericTypeAwareAutowireCandidateResolver,所以此類是功能最全、最為強大的一個處理器(ContextAnnotationAutowireCandidateResolver除外~),Spring內默認使用它進行候選處理。

它幾乎可以被稱為@Qualifier注解的"實現類",專門用於解析此注解。
帶着上面的疑問進行原理分析如下:

// @since 2.5
public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwareAutowireCandidateResolver {
	// 是個List,可以知道它不僅僅只支持org.springframework.beans.factory.annotation.Qualifier
	private final Set<Class<? extends Annotation>> qualifierTypes = new LinkedHashSet<>(2);
	private Class<? extends Annotation> valueAnnotationType = Value.class;


	// 空構造:默認支持的是@Qualifier以及JSR330標准的@Qualifier
	public QualifierAnnotationAutowireCandidateResolver() {
		this.qualifierTypes.add(Qualifier.class);
		try {
			this.qualifierTypes.add((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Qualifier", QualifierAnnotationAutowireCandidateResolver.class.getClassLoader()));
		} catch (ClassNotFoundException ex) {
			// JSR-330 API not available - simply skip.
		}
	}
	
	// 非空構造:可自己額外指定注解類型
	// 注意:如果通過構造函數指定qualifierType,上面兩種就不支持了,因此不建議使用
	// 而建議使用它提供的addQualifierType() 來添加~~~
	public QualifierAnnotationAutowireCandidateResolver(Class<? extends Annotation> qualifierType) {
	... // 省略add/set方法	

	// 這是個最重要的接口方法~~~  判斷所提供的Bean-->BeanDefinitionHolder 是否是候選的
	// (返回true表示此Bean符合條件)
	@Override
	public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) {
		// 1、先看父類:ean定義是否允許依賴注入、泛型類型是否匹配
		boolean match = super.isAutowireCandidate(bdHolder, descriptor);
		// 2、若都滿足就繼續判斷@Qualifier注解~~~~
		if (match) {
			// 3、看看標注的@Qualifier注解和候選Bean是否匹配~~~(本處的核心邏輯)
			// descriptor 一般封裝的是屬性寫方法的參數,即方法參數上的注解
			match = checkQualifiers(bdHolder, descriptor.getAnnotations());
			// 4、若Field/方法參數匹配,會繼續去看看參數所在的方法Method的情況
			// 若是構造函數/返回void。 進一步校驗標注在構造函數/方法上的@Qualifier限定符是否匹配
		if (match) {
				MethodParameter methodParam = descriptor.getMethodParameter();
				// 若是Field,methodParam就是null  所以這里是需要判空的
				if (methodParam != null) {
					Method method = methodParam.getMethod();
					// method == null表示構造函數 void.class表示方法返回void
					if (method == null || void.class == method.getReturnType()) {
						// 注意methodParam.getMethodAnnotations()方法是可能返回空的
						// 畢竟構造方法/普通方法上不一定會標注@Qualifier等注解呀~~~~
						// 同時警示我們:方法上的@Qualifier注解可不要亂標
						match = checkQualifiers(bdHolder, methodParam.getMethodAnnotations());
					}
				}
			}
		}
		return match;
	}
	...
}

在源碼注釋的地方,我按照步驟標出了它進行匹配的一個執行步驟邏輯。需要注意如下幾點:

  • qualifierTypes是支持調用者自己指定的(默認只支持@Qualifier類型)
  • 只有類型匹配、Bean定義匹配、泛型匹配等全部Ok了,才會使用@Qualifier去更加精確的匹配
  • descriptor.getAnnotations()的邏輯是:
    - 如果DependencyDescriptor描述的是字段(Field),那就去字段里拿注解們
    - 若描述的是方法參數(MethodParameter),那就返回的是方法參數的注解
  • 步驟3的match = true表示Field/方法參數上的限定符是匹配的~

說明:能走到isAutowireCandidate()方法里來,那它肯定是標注了@Autowired注解的(才能被AutowiredAnnotationBeanPostProcessor后置處理),所以descriptor.getAnnotations()返回的數組長度至少為1

checkQualifiers()方法:
QualifierAnnotationAutowireCandidateResolver:

	// 將給定的限定符注釋與候選bean定義匹配。命名中你發現:這里是負數形式,表示多個注解一起匹配
	// 此處指的限定符,顯然默認情況下只有@Qualifier注解
	protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) {
		// 很多人疑問為何沒標注注解返回的還是true?
		// 請參照上面我的解釋:methodParam.getMethodAnnotations()方法是可能返回空的,so...可以理解了吧
		if (ObjectUtils.isEmpty(annotationsToSearch)) {
			return true;
		}
		SimpleTypeConverter typeConverter = new SimpleTypeConverter();

		// 遍歷每個注解(一般有@Autowired+@Qualifier兩個注解)
		// 本文示例的兩個注解:@Autowired+@LoadBalanced兩個注解~~~(@LoadBalanced上標注有@Qualifier)
		for (Annotation annotation : annotationsToSearch) {
			Class<? extends Annotation> type = annotation.annotationType();
			boolean checkMeta = true; // 是否去檢查元注解
			boolean fallbackToMeta = false;

			// isQualifier方法邏輯見下面:是否是限定注解(默認的/開發自己指定的)
			// 本文的org.springframework.cloud.client.loadbalancer.LoadBalanced是返回true的
			if (isQualifier(type)) {
				// checkQualifier:檢查當前的注解限定符是否匹配
				if (!checkQualifier(bdHolder, annotation, typeConverter)) {
					fallbackToMeta = true; // 沒匹配上。那就fallback到Meta去吧
				} else {
					checkMeta = false; // 匹配上了,就沒必要校驗元數據了嘍~~~
				}
			}

			// 開始檢查元數據(如果上面匹配上了,就不需要檢查元數據了)
			// 比如說@Autowired注解/其它自定義的注解(反正就是未匹配上的),就會進來一個個檢查元數據
			// 什么時候會到checkMeta里來:如@A上標注有@Qualifier。@B上標注有@A。這個時候限定符是@B的話會fallback過來
			if (checkMeta) {
				boolean foundMeta = false;
				// type.getAnnotations()結果為元注解們:@Documented、@Retention、@Target等等
				for (Annotation metaAnn : type.getAnnotations()) {
					Class<? extends Annotation> metaType = metaAnn.annotationType();
					if (isQualifier(metaType)) {
						foundMeta = true; // 只要進來了 就標注找到了,標記為true表示從元注解中找到了
						// Only accept fallback match if @Qualifier annotation has a value...
						// Otherwise it is just a marker for a custom qualifier annotation.
						// fallback=true(是限定符但是沒匹配上才為true)但沒有valeu值
						// 或者根本就沒有匹配上,那不好意思,直接return false~
						if ((fallbackToMeta && StringUtils.isEmpty(AnnotationUtils.getValue(metaAnn))) || !checkQualifier(bdHolder, metaAnn, typeConverter)) {
							return false;
						}
					}
				}
				// fallbackToMeta =true你都沒有找到匹配的,就返回false的
				if (fallbackToMeta && !foundMeta) {
					return false;
				}
			}
		}
		// 相當於:只有所有的注解都木有返回false,才會認為這個Bean是合法的~~~
		return true;
	}

	// 判斷一個類型是否是限定注解   qualifierTypes:表示我所有支持的限定符
	// 本文的關鍵在於下面這個判斷語句:類型就是限定符的類型 or @Qualifier標注在了此注解上(isAnnotationPresent)
	protected boolean isQualifier(Class<? extends Annotation> annotationType) {
		for (Class<? extends Annotation> qualifierType : this.qualifierTypes) {
			// 類型就是限定符的類型 or @Qualifier標注在了此注解上(isAnnotationPresent)
			if (annotationType.equals(qualifierType) || annotationType.isAnnotationPresent(qualifierType)) {
				return true;
			}
		}
		return false;
	}

checkQualifiers()方法它會檢查標注的所有的注解(循環遍歷一個個檢查),規則如下:

  • 若是限定符注解(自己就是@Qualifier或者isAnnotationPresent),匹配上了,就繼續看下一個注解
    - 也就說@Qualifier所標注的注解也算是限定符(isQualifier() = true)
  • 若是限定符注解但是沒匹配上,那就fallback。繼續看看標注在它身上的限定符注解(如果有)能否匹配上,若匹配上了也成
  • 若不是限定符注解,也是走fallback邏輯
  • 總之:若不是限定符注解直接忽略。若有多個限定符注解都生效,必須全部匹配上了,才算做最終匹配上。

Tips:限定符不生效的效果不一定是注入失敗,而是如果是單個的話還是注入成功的。只是若出現多個Bean它就無法起到區分的效果了,所以才會注入失敗了~

它的fallback策略最多只能再向上再找一個層級(多了就不行了)。例如上例子中使用@B標注也是能起到@Qualifier效果的,但是若再加一個@C層級,限定符就不生效了。

注意:Class.isAnnotationPresent(Class<? extends Annotation> annotationClass)表示annotationClass是否標注在此類型上(此類型可以是任意Class類型)。
此方法不具有傳遞性:比如注解A上標注有@Qualifier,注解B上標注有@A注解,那么你用此方法判斷@B上是否有@Qualifier它是返回false的(即使都寫了@Inherited注解,因為和它沒關系)

到這其實還是不能解釋本文中為何@LoadBalanced參與了依賴注入,還得繼續看精髓中的精髓checkQualifier()方法(方法名是單數,表示精確檢查某一個單獨的注解):

QualifierAnnotationAutowireCandidateResolver:

	// 檢查某一個注解限定符,是否匹配當前的Bean
	protected boolean checkQualifier(BeanDefinitionHolder bdHolder, Annotation annotation, TypeConverter typeConverter) {
		// type:注解類型 bd:當前Bean的RootBeanDefinition 
		Class<? extends Annotation> type = annotation.annotationType();		
		RootBeanDefinition bd = (RootBeanDefinition) bdHolder.getBeanDefinition();
	
		// ========下面是匹配的關鍵步驟=========
		// 1、Bean定義信息的qualifiers字段一般都無值了(XML時代的配置除外)
		// 長名稱不行再拿短名稱去試了一把。顯然此處 qualifier還是為null的
		AutowireCandidateQualifier qualifier = bd.getQualifier(type.getName());
		if (qualifier == null) {
			qualifier = bd.getQualifier(ClassUtils.getShortName(type));
		}
		
		//這里才是真真有料的地方~~~請認真看步驟
		if (qualifier == null) {
			// First, check annotation on qualified element, if any
			// 1、詞方法是從bd標簽里拿這個類型的注解聲明,非XML配置時代此處targetAnnotation 為null
			Annotation targetAnnotation = getQualifiedElementAnnotation(bd, type);
			// Then, check annotation on factory method, if applicable
			// 2、若為null。去工廠方法里拿這個類型的注解。這方法里標注了兩個注解@Bean和@LoadBalanced,所以此時targetAnnotation就不再為null了~~
			if (targetAnnotation == null) {
				targetAnnotation = getFactoryMethodAnnotation(bd, type);
			}

			// 若本類木有,還會去父類去找一趟
			if (targetAnnotation == null) {
				RootBeanDefinition dbd = getResolvedDecoratedDefinition(bd);
				if (dbd != null) {
					targetAnnotation = getFactoryMethodAnnotation(dbd, type);
				}
			}


			// 若xml、工廠方法、父里都還沒找到此方法。那好家伙,回退到還去類本身上去看
			// 也就是說,如果@LoadBalanced標注在RestTemplate上,也是闊儀的
			if (targetAnnotation == null) {
				// Look for matching annotation on the target class
				...
			}
		
			// 找到了,並且當且僅當就是這個注解的時候,就return true了~
			// Tips:這里使用的是equals,所以即使目標的和Bean都標注了@Qualifier屬性,value值相同才行喲~~~~
			// 簡單的說:只有value值相同,才會被選中的。否則這個Bean就是不符合條件的
			if (targetAnnotation != null && targetAnnotation.equals(annotation)) {
				return true;
			}
		}

		// 贊。若targetAnnotation還沒找到,也就是還沒匹配上。仍舊還不放棄,拿到當前這個注解的所有注解屬性繼續嘗試匹配
		Map<String, Object> attributes = AnnotationUtils.getAnnotationAttributes(annotation);
		if (attributes.isEmpty() && qualifier == null) {
			return false;
		}
		... // 詳情不描述了。這就是為什么我們吧@Qualifier標注在某個類上面都能生效的原因 就是這里做了非常強大的兼容性~
	}

// =================它最重要的兩個判斷=================
if (targetAnnotation != null && targetAnnotation.equals(annotation));

// Fall back on bean name (or alias) match
if (actualValue == null && attributeName.equals(AutowireCandidateQualifier.VALUE_KEY) &&
					expectedValue instanceof String && bdHolder.matchesName((String) expectedValue));

checkQualifier()方法的實現,足以看到Spring作為一個優秀框架它對case的全面性,兼容性、靈活性的考慮還是很到位的。正因為Spring提供的強大的支持和靈活擴展,才給與了SpringBoot、SpringCloud在框架層面設計上更多可能性~




@Qualifier高級使用

@Autowired是根據類型進行自動裝配的,當Spring容器內同一類型的Bean不止一個的時候,就需要借助@Qualifier來一起使用了。

示例一:
@Configuration
public class WebMvcConfiguration {

    @Qualifier("person1")
    @Autowired
    public Person person;

    @Bean
    public Person person1() {
        return new Person("fsx01", 16);
    }
    @Bean
    public Person person2() {
        return new Person("fsx02", 18);
    }
}

單測代碼如下(下同):

public static void main(String[] args) {
    ApplicationContext context = new AnnotationConfigApplicationContext(WebMvcConfiguration.class);
    WebMvcConfiguration bean = context.getBean(WebMvcConfiguration.class);
    // 打印字段的值
    System.out.println(bean.person);

}

運行后打印Person(name=fsx01, age=16),完全符合預期。這也是我們對@Qualifier注解最常規、最簡單的使用。

示例二:

若你細心的話你可能注意到了@Qualifier注解它允許繼承(@Inherited)、能標注在字段上、方法上、方法參數、類上、注解上
因此它還能這么用:

@Configuration
public class WebMvcConfiguration {

    @MyAnno // 會把所有標注有此注解的Bean都收入囊中,請List裝(因為會有多個)
    @Autowired
    public List<Person> person;
    
    @MyAnno
    @Bean
    public Person person1() {
        return new Person("fsx01", 16);
    }
    @MyAnno
    @Bean
    public Person person2() {
        return new Person("fsx02", 18);
    }

	// 自定義注解:上面標注有@Qualifier注解
    @Target({FIELD, METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Qualifier
    @interface MyAnno {
    }
}

運行單測,打印[Person(name=fsx01, age=16), Person(name=fsx02, age=18)],符合預期。

實例三:

若你不想自定義注解,直接使用@Qualifier注解分類注入也是可以的,如下案例:

@Configuration
public class WebMvcConfiguration {
    @Qualifier("person2")
    @Autowired
    public List<Person> person;

    @Qualifier("person2")
    @Bean
    public Person person1() {
        return new Person("fsx01", 16);
    }

    @Qualifier
    @Bean
    public Person person2() {
        return new Person("fsx02", 18);
    }
    @Qualifier
    @Bean
    public Person person3() {
        return new Person("fsx03", 20);
    }
}

運行的最終結果是:

[Person(name=fsx01, age=16), Person(name=fsx02, age=18)]

它把@Qualifier指定的value值相同的 或者 beanName(或者別名)相同的都注入進來了。這部分匹配代碼為:

checkQualifier方法:

1、頭上標注的注解完全equals(類型和value值都一樣,算作匹配成功)
	targetAnnotation != null && targetAnnotation.equals(annotation)
	
2、Fall back on bean name (or alias) match。若@Qualifier沒匹配上,回退到BeanName的匹配,規則為:
   取頭上注解的`value`屬性(必須有此屬性),如果beanName/alias能匹配上次名稱,也算最終匹配成功了
   
	actualValue == null && attributeName.equals(AutowireCandidateQualifier.VALUE_KEY) &&
	expectedValue instanceof String && bdHolder.matchesName((String) expectedValue)

備注:使用在類上、入參上的使用比較簡單,此處就不做示范了。
@Qualifier設計的細節可以看到,注解的value屬性並不是必須的,所以它可以很好的使用在聯合注解的場景。

關於依賴注入和@Qualifier的使用亦需注意如下細節:

  1. @Autowired可不要寫在Object類型的字段上去注入,因為容器內可以找到N多個會報錯的。但是List<Object>是可以的(相當於把所有Bean都拿過來~)
  2. 可以利用@Qualifier這個高級特性,實現按需、按類別(不是類型)進行依賴注入,這種能力非常贊,給了框架二次開發設計者提供了更多的可能性

如果說指定value是按照key進行限定/匹配,那么類似@LoadBalanced這種注解匹配可以理解成就是按照莫一類進行歸類限定了,並且自由度也更高了。

推薦閱讀

為何一個@LoadBalanced注解就能讓RestTemplate擁有負載均衡的能力?【享學Spring Cloud】

總結

本文介紹@Qualifier高級應用場景和案例,通過結合@LoadBalanced對此注解的使用,應該說是能給你打開了一個新的視角去看待@Qualifier,甚至看待Spring的依賴注入,這對后續的理解、自定義擴展/使用還是蠻有意義的。

== 若對Spring、SpringBoot、MyBatis等源碼分析感興趣,可加我wx:fsx641385712,手動邀請你入群一起飛 ==
== 若對Spring、SpringBoot、MyBatis等源碼分析感興趣,可加我wx:fsx641385712,手動邀請你入群一起飛 ==


免責聲明!

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



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