徹底搞明白Spring中的自動裝配和Autowired

清幽之地發表於2019-03-10

一、自動裝配

當Spring裝配Bean屬性時,有時候非常明確,就是需要將某個Bean的引用裝配給指定屬性。比如,如果我們的應用上下文中只有一個org.mybatis.spring.SqlSessionFactoryBean型別的Bean,那麼任意一個依賴SqlSessionFactoryBean的其他Bean就是需要這個Bean。畢竟這裡只有一個SqlSessionFactoryBean的Bean。

為了應對這種明確的裝配場景,Spring提供了自動裝配(autowiring)。與其顯式的裝配Bean屬性,為何不讓Spring識別出可以自動裝配的場景。

當涉及到自動裝配Bean的依賴關係時,Spring有多種處理方式。因此,Spring提供了4種自動裝配策略。

public interface AutowireCapableBeanFactory{

	//無需自動裝配
	int AUTOWIRE_NO = 0;

	//按名稱自動裝配bean屬性
	int AUTOWIRE_BY_NAME = 1;

	//按型別自動裝配bean屬性
	int AUTOWIRE_BY_TYPE = 2;

	//按構造器自動裝配
	int AUTOWIRE_CONSTRUCTOR = 3;

	//過時方法,Spring3.0之後不再支援
	@Deprecated
	int AUTOWIRE_AUTODETECT = 4;
}
複製程式碼

Spring在AutowireCapableBeanFactory介面中定義了這幾種策略。其中,AUTOWIRE_AUTODETECT被標記為過時方法,在Spring3.0之後已經不再支援。

1、byName

它的意思是,把與Bean的屬性具有相同名字的其他Bean自動裝配到Bean的對應屬性中。聽起來可能比較拗口,我們來看個例子。

首先,在User的Bean中有個屬性Role myRole,再建立一個Role的Bean,它的名字如果叫myRole,那麼在User中就可以使用byName來自動裝配。

public class User{
	private Role myRole;
}
public class Role {
	private String id;	
	private String name;
}
複製程式碼

上面是Bean的定義,再看配置檔案。

<bean id="myRole" class="com.viewscenes.netsupervisor.entity.Role">
	<property name="id" value="1001"></property>
	<property name="name" value="管理員"></property>
</bean>

<bean id="user" class="com.viewscenes.netsupervisor.entity.User" autowire="byName"></bean>
複製程式碼

如上所述,只要屬性名稱和Bean的名稱可以對應,那麼在user的Bean中就可以使用byName來自動裝配。那麼,如果屬性名稱對應不上呢?

2、byType

是的,如果不使用屬性名稱來對應,你也可以選擇使用型別來自動裝配。它的意思是,把與Bean的屬性具有相同型別的其他Bean自動裝配到Bean的對應屬性中。

<bean class="com.viewscenes.netsupervisor.entity.Role">
	<property name="id" value="1001"></property>
	<property name="name" value="管理員"></property>
</bean>

<bean id="user" class="com.viewscenes.netsupervisor.entity.User" autowire="byType"></bean>
複製程式碼

還是上面的例子,如果使用byType,Role Bean的ID都可以省去。

3、constructor

它是說,把與Bean的構造器入參具有相同型別的其他Bean自動裝配到Bean構造器的對應入參中。值的注意的是,具有相同型別的其他Bean這句話說明它在查詢入參的時候,還是通過Bean的型別來確定。

構造器中入參的型別為Role

public class User{
	private Role role;

	public User(Role role) {
		this.role = role;
	}
}

<bean id="user" class="com.viewscenes.netsupervisor.entity.User" autowire="constructor"></bean>
複製程式碼

4、autodetect

它首先會嘗試使用constructor進行自動裝配,如果失敗再嘗試使用byType。不過,它在Spring3.0之後已經被標記為@Deprecated

5、預設自動裝配

預設情況下,default-autowire屬性被設定為none,標示所有的Bean都不使用自動裝配,除非Bean上配置了autowire屬性。 如果你需要為所有的Bean配置相同的autowire屬性,有個辦法可以簡化這一操作。 在根元素Beans上增加屬性default-autowire="byType"

<beans default-autowire="byType">
複製程式碼

Spring自動裝配的優點不言而喻。但是事實上,在Spring XML配置檔案裡的自動裝配並不推薦使用,其中筆者認為最大的缺點在於不確定性。或者除非你對整個Spring應用中的所有Bean的情況瞭如指掌,不然隨著Bean的增多和關係複雜度的上升,情況可能會很糟糕。

二、Autowired

從Spring2.5開始,開始支援使用註解來自動裝配Bean的屬性。它允許更細粒度的自動裝配,我們可以選擇性的標註某一個屬性來對其應用自動裝配。

Spring支援幾種不同的應用於自動裝配的註解。

  • Spring自帶的@Autowired註解。

  • JSR-330的@Inject註解。

  • JSR-250的@Resource註解。

我們今天只重點關注Autowired註解,關於它的解析和注入過程,請參考筆者Spring原始碼系列的文章。Spring原始碼分析(二)bean的例項化和IOC依賴注入

使用@Autowired很簡單,在需要注入的屬性加入註解即可。

@Autowired
UserService userService;
複製程式碼

不過,使用它有幾個點需要注意。

1、強制性

預設情況下,它具有強制契約特性,其所標註的屬性必須是可裝配的。如果沒有Bean可以裝配到Autowired所標註的屬性或引數中,那麼你會看到NoSuchBeanDefinitionException的異常資訊。

public Object doResolveDependency(DependencyDescriptor descriptor, String beanName,
			Set<String> autowiredBeanNames, TypeConverter typeConverter) throws BeansException {
	
	//查詢Bean
	Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
	//如果拿到的Bean集合為空,且isRequired,就丟擲異常。
	if (matchingBeans.isEmpty()) {
		if (descriptor.isRequired()) {
			raiseNoSuchBeanDefinitionException(type, "", descriptor);
		}
		return null;
	}
}
複製程式碼

看到上面的原始碼,我們可以得到這一資訊,Bean集合為空不要緊,關鍵isRequired條件不能成立,那麼,如果我們不確定屬性是否可以裝配,可以這樣來使用Autowired。

@Autowired(required=false)
UserService userService;
複製程式碼

2、裝配策略

我記得曾經有個面試題是這樣問的:Autowired是按照什麼策略來自動裝配的呢?

關於這個問題,不能一概而論,你不能簡單的說按照型別或者按照名稱。但可以確定的一點的是,它預設是按照型別來自動裝配的,即byType。

  • 預設按照型別裝配

關鍵點findAutowireCandidates這個方法。

protected Map<String, Object> findAutowireCandidates(
		String beanName, Class<?> requiredType, DependencyDescriptor descriptor) {
	
	//獲取給定型別的所有bean名稱,裡面實際迴圈所有的beanName,獲取它的例項
	//再通過isTypeMatch方法來確定
	String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
			this, requiredType, true, descriptor.isEager());
			
	Map<String, Object> result = new LinkedHashMap<String, Object>(candidateNames.length);
	
	//根據返回的beanName,獲取其例項返回
	for (String candidateName : candidateNames) {
		if (!isSelfReference(beanName, candidateName) && isAutowireCandidate(candidateName, descriptor)) {
			result.put(candidateName, getBean(candidateName));
		}
	}
	return result;
}
複製程式碼
  • 按照名稱裝配

可以看到它返回的是一個列表,那麼就表明,按照型別匹配可能會查詢到多個例項。到底應該裝配哪個例項呢?我看有的文章裡說,可以加註解以此規避。比如@qulifier、@Primary等,實際還有個簡單的辦法。

比如,按照UserService介面型別來裝配它的實現類。UserService介面有多個實現類,分為UserServiceImpl、UserServiceImpl2。那麼我們在注入的時候,就可以把屬性名稱定義為Bean實現類的名稱。

@Autowired
UserService UserServiceImpl2;
複製程式碼

這樣的話,Spring會按照byName來進行裝配。首先,如果查到型別的多個例項,Spring已經做了判斷。

public Object doResolveDependency(DependencyDescriptor descriptor, String beanName,
			Set<String> autowiredBeanNames, TypeConverter typeConverter) throws BeansException {
			
	//按照型別查詢Bean例項
	Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
	//如果Bean集合為空,且isRequired成立就丟擲異常
	if (matchingBeans.isEmpty()) {
		if (descriptor.isRequired()) {
			raiseNoSuchBeanDefinitionException(type, "", descriptor);
		}
		return null;
	}
	//如果查詢的Bean例項大於1個
	if (matchingBeans.size() > 1) {
		//找到最合適的那個,如果沒有合適的。。也丟擲異常
		String primaryBeanName = determineAutowireCandidate(matchingBeans, descriptor);
		if (primaryBeanName == null) {
			throw new NoUniqueBeanDefinitionException(type, matchingBeans.keySet());
		}
		if (autowiredBeanNames != null) {
			autowiredBeanNames.add(primaryBeanName);
		}
		return matchingBeans.get(primaryBeanName);
	}	
}
複製程式碼

可以看出,如果查到多個例項,determineAutowireCandidate方法就是關鍵。它來確定一個合適的Bean返回。其中一部分就是按照Bean的名稱來匹配。

protected String determineAutowireCandidate(Map<String, Object> candidateBeans, 
				DependencyDescriptor descriptor) {
	//迴圈拿到的Bean集合
	for (Map.Entry<String, Object> entry : candidateBeans.entrySet()) {
		String candidateBeanName = entry.getKey();
		Object beanInstance = entry.getValue();
		//通過matchesBeanName方法來確定bean集合中的名稱是否與屬性的名稱相同
		if (matchesBeanName(candidateBeanName, descriptor.getDependencyName())) {
			return candidateBeanName;
		}
	}
	return null;
}
複製程式碼

最後我們回到問題上,得到的答案就是:@Autowired預設使用byType來裝配屬性,如果匹配到型別的多個例項,再通過byName來確定Bean。

3、主和優先順序

上面我們已經看到了,通過byType可能會找到多個例項的Bean。然後再通過byName來確定一個合適的Bean,如果通過名稱也確定不了呢? 還是determineAutowireCandidate這個方法,它還有兩種方式來確定。

protected String determineAutowireCandidate(Map<String, Object> candidateBeans, 
				DependencyDescriptor descriptor) {
	Class<?> requiredType = descriptor.getDependencyType();
	//通過@Primary註解來標識Bean
	String primaryCandidate = determinePrimaryCandidate(candidateBeans, requiredType);
	if (primaryCandidate != null) {
		return primaryCandidate;
	}
	//通過@Priority(value = 0)註解來標識Bean value為優先順序大小
	String priorityCandidate = determineHighestPriorityCandidate(candidateBeans, requiredType);
	if (priorityCandidate != null) {
		return priorityCandidate;
	}
	return null;
}
複製程式碼
  • Primary

它的作用是看Bean上是否包含@Primary註解,如果包含就返回。當然了,你不能把多個Bean都設定為@Primary,不然你會得到NoUniqueBeanDefinitionException這個異常。

protected String determinePrimaryCandidate(Map<String, Object> candidateBeans, Class<?> requiredType) {
	String primaryBeanName = null;
	for (Map.Entry<String, Object> entry : candidateBeans.entrySet()) {
		String candidateBeanName = entry.getKey();
		Object beanInstance = entry.getValue();
		if (isPrimary(candidateBeanName, beanInstance)) {
			if (primaryBeanName != null) {
				boolean candidateLocal = containsBeanDefinition(candidateBeanName);
				boolean primaryLocal = containsBeanDefinition(primaryBeanName);
				if (candidateLocal && primaryLocal) {
					throw new NoUniqueBeanDefinitionException(requiredType, candidateBeans.size(),
							"more than one 'primary' bean found among candidates: " + candidateBeans.keySet());
				}
				else if (candidateLocal) {
					primaryBeanName = candidateBeanName;
				}
			}
			else {
				primaryBeanName = candidateBeanName;
			}
		}
	}
	return primaryBeanName;
}
複製程式碼
  • Priority

你也可以在Bean上配置@Priority註解,它有個int型別的屬性value,可以配置優先順序大小。數字越小的,就被優先匹配。同樣的,你也不能把多個Bean的優先順序配置成相同大小的數值,否則NoUniqueBeanDefinitionException異常照樣出來找你。

protected String determineHighestPriorityCandidate(Map<String, Object> candidateBeans, 
									Class<?> requiredType) {
	String highestPriorityBeanName = null;
	Integer highestPriority = null;
	for (Map.Entry<String, Object> entry : candidateBeans.entrySet()) {
		String candidateBeanName = entry.getKey();
		Object beanInstance = entry.getValue();
		Integer candidatePriority = getPriority(beanInstance);
		if (candidatePriority != null) {
			if (highestPriorityBeanName != null) {
				//如果優先順序大小相同
				if (candidatePriority.equals(highestPriority)) {
					throw new NoUniqueBeanDefinitionException(requiredType, candidateBeans.size(),
						"Multiple beans found with the same priority ('" + highestPriority + "') " +
							"among candidates: " + candidateBeans.keySet());
				}
				else if (candidatePriority < highestPriority) {
					highestPriorityBeanName = candidateBeanName;
					highestPriority = candidatePriority;
				}
			}
			else {
				highestPriorityBeanName = candidateBeanName;
				highestPriority = candidatePriority;
			}
		}
	}
	return highestPriorityBeanName;
}
複製程式碼

最後,有一點需要注意。Priority的包在javax.annotation.Priority;,如果想使用它還要引入一個座標。

<dependency>
	<groupId>javax.annotation</groupId>
	<artifactId>javax.annotation-api</artifactId>
	<version>1.2</version>
</dependency>
複製程式碼

三、總結

本章節重點闡述了Spring中的自動裝配的幾種策略,又通過原始碼分析了Autowired註解的使用方式。 在Spring3.0之後,有效的自動裝配策略分為byType、byName、constructor三種方式。註解Autowired預設使用byType來自動裝配,如果存在型別的多個例項就嘗試使用byName匹配,如果通過byName也確定不了,可以通過Primary和Priority註解來確定。

相關文章