1. 程式人生 > >Spring常用的三種注入方式

Spring常用的三種注入方式

Spring通過DI(依賴注入)實現IOC(控制反轉),常用的注入方式主要有三種:構造方法注入,setter注入,基於註解的注入。

構造方法注入

先簡單瞭解一下測試專案的結構,用maven構建的,四個包:

  • entity:儲存實體,裡面只有一個User類
  • dao:資料訪問,一個介面,兩個實現類
  • service:服務層,一個介面,一個實現類,實現類依賴於IUserDao
  • test:測試包

在spring的配置檔案中註冊UserService,將UserDaoJdbc通過constructor-arg標籤注入到UserService的某個有引數的構造方法

<!-- 註冊userService -->
<bean id="userService" class="com.lyu.spring.service.impl.UserService"> <constructor-arg ref="userDaoJdbc"></constructor-arg> </bean> <!-- 註冊jdbc實現的dao --> <bean id="userDaoJdbc" class="com.lyu.spring.dao.impl.UserDaoJdbc"></bean>

如果只有一個有引數的構造方法並且引數型別與注入的bean的型別匹配,那就會注入到該構造方法中。

public class UserService implements IUserService {

	private IUserDao userDao;
	
	public UserService(IUserDao userDao) {
		this.userDao = userDao;
	}
	
	public void loginUser() {
		userDao.loginUser();
	}

}
@Test
public void testDI() {
	ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"
); // 獲取bean物件 UserService userService = ac.getBean(UserService.class, "userService"); // 模擬使用者登入 userService.loginUser(); }

測試列印結果:jdbc-登入成功

注:模擬使用者登入的loginUser方法其實只是列印了一條輸出語句,jdbc實現的類輸出的是:jdbc-登入成功,mybatis實現的類輸出的是:mybatis-登入成功。

問題一:如果有多個有引數的構造方法並且每個構造方法的引數列表裡面都有要注入的屬性,那userDaoJdbc會注入到哪裡呢?

public class UserService implements IUserService {

	private IUserDao userDao;
	private User user;
	
	public UserService(IUserDao userDao) {
		System.out.println("這是有一個引數的構造方法");
		this.userDao = userDao;
	}
	
	public UserService(IUserDao userDao, User user) {
		System.out.println("這是有兩個引數的構造方法");
		this.userDao = userDao;
		this.user = user;
	}
	
	public void loginUser() {
		userDao.loginUser();
	}

}


結果:會注入到只有一個引數的構造方法中,並且經過測試注入哪一個構造方法與構造方法的順序無關

這裡寫圖片描述

問題二:如果只有一個構造方法,但是有兩個引數,一個是待注入的引數,另一個是其他型別的引數,那麼這次注入可以成功嗎?

public class UserService implements IUserService {

	private IUserDao userDao;
	private User user;
	
	public UserService(IUserDao userDao, User user) {
		this.userDao = userDao;
		this.user = user;
	}
	
	public void loginUser() {
		userDao.loginUser();
	}

}

結果:失敗了,即使在costract-arg標籤裡面通過name屬性指定要注入的引數名userDao也會失敗.

這裡寫圖片描述

問題三:如果我們想向有多個引數的構造方法中注入值該在配置檔案中怎麼寫呢?

public class UserService implements IUserService {

	private IUserDao userDao;
	private User user;
	
	public UserService(IUserDao userDao, User user) {
		this.userDao = userDao;
		this.user = user;
	}
	
	public void loginUser() {
		userDao.loginUser();
	}

}

參考寫法:通過name屬性指定要注入的值,與構造方法引數列表引數的順序無關。

<!-- 註冊userService -->
<bean id="userService" class="com.lyu.spring.service.impl.UserService">
	<constructor-arg name="userDao" ref="userDaoJdbc"></constructor-arg>
	<constructor-arg name="user" ref="user"></constructor-arg>
</bean>

<!-- 註冊實體User類,用於測試 -->
<bean id="user" class="com.lyu.spring.entity.User"></bean>

<!-- 註冊jdbc實現的dao -->
<bean id="userDaoJdbc" class="com.lyu.spring.dao.impl.UserDaoJdbc"></bean>

問題四:如果有多個構造方法,每個構造方法只有引數的順序不同,那通過構造方法注入多個引數會注入到哪一個呢?

public class UserService implements IUserService {

	private IUserDao userDao;
	private User user;
	
	public UserService(IUserDao userDao, User user) {
		System.out.println("這是第二個構造方法");
		this.userDao = userDao;
		this.user = user;
	}
	
	public UserService(User user, IUserDao userDao) {
		System.out.println("這是第一個構造方法");
		this.userDao = userDao;
		this.user = user;
	}
	
	public void loginUser() {
		userDao.loginUser();
	}

}

結果:哪個構造方法在前就注入哪一個,這種情況下就與構造方法順序有關。

這裡寫圖片描述

setter注入

配置檔案如下:
<!-- 註冊userService -->
<bean id="userService" class="com.lyu.spring.service.impl.UserService">
	<!-- 寫法一 -->
	<!-- <property name="UserDao" ref="userDaoMyBatis"></property> -->
	<!-- 寫法二 -->
	<property name="userDao" ref="userDaoMyBatis"></property>
</bean>

<!-- 註冊mybatis實現的dao -->
<bean id="userDaoMyBatis" class="com.lyu.spring.dao.impl.UserDaoMyBatis"></bean>

注:上面這兩種寫法都可以,spring會將name值的每個單詞首字母轉換成大寫,然後再在前面拼接上"set"構成一個方法名,然後去對應的類中查詢該方法,通過反射呼叫,實現注入。

切記:name屬性值與類中的成員變數名以及set方法的引數名都無關,只與對應的set方法名有關,下面的這種寫法是可以執行成功的

public class UserService implements IUserService {

	private IUserDao userDao1;
	
	public void setUserDao(IUserDao userDao1) {
		this.userDao1 = userDao1;
	}
	
	public void loginUser() {
		userDao1.loginUser();
	}

}

還有一點需要注意:如果通過set方法注入屬性,那麼spring會通過預設的空參構造方法來例項化物件,所以如果在類中寫了一個帶有引數的構造方法,一定要把空引數的構造方法寫上,否則spring沒有辦法例項化物件,導致報錯。
這裡寫圖片描述

基於註解的注入

在介紹註解注入的方式前,先簡單瞭解bean的一個屬性autowire,autowire主要有三個屬性值:constructor,byName,byType。
  • constructor:通過構造方法進行自動注入,spring會匹配與構造方法引數型別一致的bean進行注入,如果有一個多引數的構造方法,一個只有一個引數的構造方法,在容器中查詢到多個匹配多引數構造方法的bean,那麼spring會優先將bean注入到多引數的構造方法中。

  • byName:被注入bean的id名必須與set方法後半截匹配,並且id名稱的第一個單詞首字母必須小寫,這一點與手動set注入有點不同。

  • byType:查詢所有的set方法,將符合符合引數型別的bean注入。


下面進入正題:註解方式註冊bean,注入依賴

主要有四種註解可以註冊bean,每種註解可以任意使用,只是語義上有所差異:

  1. @Component:可以用於註冊所有bean
  2. @Repository:主要用於註冊dao層的bean
  3. @Controller:主要用於註冊控制層的bean
  4. @Service:主要用於註冊服務層的bean

描述依賴關係主要有兩種:

  • @Resource:java的註解,預設以byName的方式去匹配與屬性名相同的bean的id,如果沒有找到就會以byType的方式查詢,如果byType查詢到多個的話,使用@Qualifier註解(spring註解)指定某個具體名稱的bean。

    @Resource
    @Qualifier("userDaoMyBatis")
    private IUserDao userDao;
    
    public UserService(){
    	
    }
    
  • @Autowired:spring註解,預設是以byType的方式去匹配型別相同的bean,如果只匹配到一個,那麼就直接注入該bean,無論要注入的 bean 的 name 是什麼;如果匹配到多個,就會呼叫 DefaultListableBeanFactorydetermineAutowireCandidate 方法來決定具體注入哪個bean。determineAutowireCandidate 方法的內容如下:

    // candidateBeans 為上一步通過型別匹配到的多個bean,該 Map 中至少有兩個元素。
    protected String determineAutowireCandidate(Map<String, Object> candidateBeans, DependencyDescriptor descriptor) {
        //  requiredType 為匹配到的介面的型別
       Class<?> requiredType = descriptor.getDependencyType();
       // 1. 先找 Bean 上有@Primary 註解的,有則直接返回
       String primaryCandidate = this.determinePrimaryCandidate(candidateBeans, requiredType);
       if (primaryCandidate != null) {
           return primaryCandidate;
       } else {
           // 2.再找 Bean 上有 @Order,@PriorityOrder 註解的,有則返回
           String priorityCandidate = this.determineHighestPriorityCandidate(candidateBeans, requiredType);
           if (priorityCandidate != null) {
               return priorityCandidate;
           } else {
               Iterator var6 = candidateBeans.entrySet().iterator();
    
               String candidateBeanName;
               Object beanInstance;
               do {
                   if (!var6.hasNext()) {
                       return null;
                   }
    
                   // 3. 再找 bean 的名稱匹配的
                   Entry<String, Object> entry = (Entry)var6.next();
                   candidateBeanName = (String)entry.getKey();
                   beanInstance = entry.getValue();
               } while(!this.resolvableDependencies.values().contains(beanInstance) && !this.matchesBeanName(candidateBeanName, descriptor.getDependencyName()));
    
               return candidateBeanName;
           }
       }
    }
    

    determineAutowireCandidate 方法的邏輯是:

    1. 先找 Bean 上有@Primary 註解的,有則直接返回 bean 的 name。
    2. 再找 Bean 上有 @Order,@PriorityOrder 註解的,有則返回 bean 的 name。
    3. 最後再以名稱匹配(ByName)的方式去查詢相匹配的 bean。

    可以簡單的理解為先以 ByType 的方式去匹配,如果匹配到了多個再以 ByName 的方式去匹配,找到了對應的 bean 就去注入,沒找到就丟擲異常。

    還有一點要注意:如果使用了 @Qualifier 註解,那麼當自動裝配匹配到多個 bean 的時候就不會進入 determineAutowireCandidate 方法(親測),而是直接查詢與 @Qualifer 指定的 bean name 相同的 bean 去注入,找到了就直接注入,沒有找到則丟擲異常。

    tips:大家如果認真思考可能會發現 ByName 的注入方式和 @Qualifier 有點類似,都是在自動裝配匹配到多個 bean 的時候,指定一個具體的 bean,那它們有什麼不同呢?

    ByName 的方式需要遍歷,@Qualifier 直接一次定位。在匹配到多個 bean 的情況下,使用 @Qualifier 來指明具體裝配的 bean 效率會更高一下

    博主個人覺得:@Qualifer 註解出現的意義或許就是 Spring 為了解決 JDK 自帶的 ByName 遍歷匹配效率低下的問題。要不然也不會出現兩個容易混淆的匹配方式。

寫在最後:雖然有這麼多的注入方式,但是實際上開發的時候自己編寫的類一般用註解的方式註冊類,用@Autowired描述依賴進行注入,一般實現類也只有一種(jdbc or hibernate or mybatis),除非專案有大的變動,所以@Qualifier標籤用的也較少;但是在使用其他元件的API的時候用的是通過xml配置檔案來註冊類,描述依賴,因為你不能去改人家原始碼嘛。

另外,非常感謝 cdy1996 指出了我之前的錯誤(認為 ByName 是 @Autowired 預設的注入方式),歡迎大家指出我的錯誤和不足,但是,一定請帶上程式碼。