Spring IoC是如何進行依賴注入的
一、依賴注入(DI)
DI(Dependency Injection),Spring IoC 不是一種技術(shù),而是一種思想,通過這種思想,能夠指導(dǎo)我們設(shè)計出松耦合的程序代碼。而Spring IoC這個思想的作用體現(xiàn)在兩個方面,一是如何將Bean裝配到容器中去以及如何從容器中獲取Bean,二是如何解決Bean之間的依賴關(guān)系,換句話說,就是如果由IoC容器來管理依賴關(guān)系,當(dāng)一個Bean需要依賴另外一個Bean時,IoC容器如何實現(xiàn)這樣的依賴關(guān)系。
解決Spring中Bean之間的依賴的實現(xiàn)方式,在Spring的概念中就被稱之為依賴注入(Dependency Injection,DI)。普遍認(rèn)為的Spring依賴注入的實現(xiàn)方式有三種:構(gòu)造方法注入、setter方法注入、注解注入。但,就我而言,我認(rèn)為應(yīng)該劃分為兩種形式——基于XML注入和基于注解注入,然后再細(xì)分為下面的形式:
基于XML的注入方式是我們最先學(xué)習(xí)和使用的方式,也是最熟悉的方式,就簡單的做個介紹,舉個例子。
1. 通過構(gòu)造方法注入
- public class UserServiceImpl implements UserService {
- private UserDao userDao;
- public UserServiceImpl(UserDao userDao) {
- this.userDao = userDao;
- }
- /**繼承自UserService的方法**/
- }
首先定義一個服務(wù)層UserServiceImpl,然后在其內(nèi)部增加對dao層的引用userDao。
接下來就是添加一個構(gòu)造方法public UserServiceImpl(UserDao userDao)以待Spring通過這個方法為userDao注入實例。
- <!--注冊userDao-->
- <bean id="userDao" class="com.klasdq.sb.c1.di.dao.impl.UserDaoImpl"></bean>
- <!--注冊userService 并注入userDao-->
- <bean id="userService" class="com.klasdq.sb.c1.di.service.impl.UserServiceImpl">
- <constructor-arg name="userDao" ref="userDao"></constructor-arg>
- </bean>
最后在Spring XML配置文件中注入相應(yīng)的bean實例。
通過構(gòu)造方法的注入,必須要注入類中具有對應(yīng)的構(gòu)造方法,若沒有對應(yīng)的構(gòu)造方法,會出現(xiàn)報錯。
2. 通過setter方法注入
修改UserServiceImpl.java為:
- public class UserServiceImpl implements UserService {
- private UserDao userDao;
- public void setUserDao(UserDao userDao) {
- this.userDao = userDao;
- }
- /**繼承自UserService的方法**/
- }
再修改XML文件內(nèi)容為:
- <!--注冊userDao-->
- <bean id="userDao" class="com.klasdq.sb.c1.di.dao.impl.UserDaoImpl"></bean>
- <!--注冊userService 并注入userDao-->
- <bean id="userService" class="com.klasdq.sb.c1.di.service.impl.UserServiceImpl">
- <property name="userDao" ref="userDao"></property>
- </bean>
這兩種方式的區(qū)別在于,一、UserServiceImpl.java可以不用添加構(gòu)造方法,但是必須存在一個無參構(gòu)造方法(如public UserServiceImpl(),示例里面沒寫,是因為java默認(rèn)會提供一個無參構(gòu)造方法)以供Spring 容器注冊生成Bean(如userService)。二、XML文件中,采用構(gòu)造方法注入時,需要使用
在XML注入過程中,除了使用ref=""引用之外,還可以使用value=""設(shè)定具體的值,其效果和使用注解@Value差不多。
二、基于注解的依賴注入
三、Autowired
源碼:
- @Target({ElementType.CONSTRUCTOR,
- ElementType.METHOD,
- ElementType.PARAMETER,
- ElementType.FIELD,
- ElementType.ANNOTATION_TYPE})
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface Autowired {
- boolean required() default true;
- }
@Autowired是基于注解的依賴注入的關(guān)鍵點,它的源碼非常簡單,只有一個參數(shù)request(),這個參數(shù)的作用是標(biāo)識注入Bean是否一定要注入,也就是說,在Spring容器沒有找到相應(yīng)Bean時,如果其值為true,就會報出異常;如果其值為false,就不會出現(xiàn)異常,但在使用過程中,如果容器一直不對Bean進行注入,那么有可能出現(xiàn)空指針異常。
另外一點就是,源碼當(dāng)中的@Target所包含的參數(shù)正好就是基于注解的依賴注入的注入方式種類,@Target決定了@Autowired能夠標(biāo)注在哪些類型上面。
1. 通過構(gòu)造方法注入:
- @Service("userService")
- public class UserServiceImpl implements UserService {
- private UserDao userDao;
- @Autowired
- public UserServiceImpl(UserDao userDao) {
- this.userDao = userDao;
- }
- /**繼承自UserService的方法**/
- }
根據(jù)開發(fā)文檔的說法,這種只有一個構(gòu)造方法的情況,自Spring4.3以后,就不再需要添加@Autowired標(biāo)注,也可以。但是,如果有多個構(gòu)造方法時,是必須要對其中一個方法標(biāo)注@Autowired,不然Spring會報出異常。
2. 通過setter方法注入
- @Service("userService")
- public class UserServiceImpl implements UserService {
- private UserDao userDao;
- @Autowired
- public void setUserDao(UserDao userDao) {
- this.userDao = userDao;
- }
- /**繼承自UserService的方法**/
- }
3. 通過字段注入
- @Service("userService")
- public class UserServiceImpl implements UserService {
- @Autowired
- private UserDao userDao;
- /**繼承自UserService的方法**/
- }
4. 通過方法入?yún)⒆⑷?/strong>
上面三種注入方式,都是比較熟悉的就不再多做闡述了。重點說一下參數(shù)注入,其實方法入?yún)⒆⑷敕绞礁杏X上是和構(gòu)造方法、setter方法注入形式差不多,相當(dāng)于將構(gòu)造方法、setter方法上的注解@Autowired放到入?yún)⒌奈恢?。說起來可能有些抽象,直接看例子:
- @Component
- public class UserDaoImpl implements UserDao {
- //簡單返回一個User,模擬數(shù)據(jù)庫查找過程
- @Override
- public User getUser(Long id, String name){
- User user = new User();
- user.setId(id);
- user.setName(name);
- user.setAccount("12345678911");
- user.setPassword("******");
- user.setOtherInfo("this is a test account");
- return user;
- }
- }
- //UserService類
- @Service("userService")
- public class UserServiceImpl implements UserService {
- private UserDao userDao;
- public UserServiceImpl(@Autowired UserDao userDao,
- @Autowired User user) {
- System.out.println("UserServiceImpl: "+user);
- this.userDao = userDao;
- }
- @Override
- public User getUser(Long id, String name){
- return userDao.getUser(id,name);
- }
- }
- //簡單的配置類
- //作用就是為標(biāo)有@Componet(@Service也算)注解的類 生成Bean
- //同時 為@Autowired標(biāo)識下的Bean(對象) 注入實例
- @Configuration
- @ComponentScan
- public class DIConfig {
- //用于Service類中入?yún)ser的注入
- @Bean
- public User getUser(){
- User u = new User();
- u.setName("user inject into service");
- return u;
- }
- }
- //測試類
- //注意:使用JUnit4測試時,如果需要使用@Autowired注入那么必須添加
- //@RunWith 標(biāo)注使用Spring方式啟動(或者SpringBootRunner)
- //@ContextConfiguration 掃描配置類
- @RunWith(SpringRunner.class)
- @ContextConfiguration(classes = DIConfig.class)
- public class DITest {
- //如果不添加測試類上兩個注解,會注入失敗
- @Autowired
- private UserService userService;
- @Test
- public void testAutowired(){ System.out.println(userService.getUser(1L,"name"));
- }
- }
運行測試方法之后就得到以下結(jié)果:
public UserServiceImpl(@Autowired UserDao userDao,@Autowired User user)中的輸出結(jié)果:
public void testAutowired()測試方法中的輸出結(jié)果:
注意這里public UserServiceImpl(@Autowired UserDao userDao,@Autowired User user)的入?yún)ⅲ?/p>
userDao是UserServiceImpl的字段,但user不是。也就是說,我們可以在構(gòu)造方法中添加任意參數(shù),只要是我們需要的,不一定要求該參數(shù)是類中屬性字段。
此外還有需要注意的是,這里所說的方法,不是任意的方法,而是構(gòu)造方法或setter方法,這種public void initService(@Autowired UserDao userDao)自定義的方法是無法完成注入的。
四、@Primary 和 @Qualifier
在上面的例子中,我們注入使用到的bean,都只是容器中只有一個Bean實例的情況。那么當(dāng)容器當(dāng)中出現(xiàn)多個同類型的Bean時,如何處理呢?
修改配置類代碼如下:
- @Configuration
- @ComponentScan
- public class DIConfig {
- @Bean
- public User getUser(){
- User u = new User();
- u.setName("this is user");
- return u;
- }
- @Bean
- public User getUser2(){
- User u = new User();
- u.setName("this is user2");
- return u;
- }
- }
修改測試類:
- @RunWith(SpringRunner.class)
- @ContextConfiguration(classes = DIConfig.class)
- public class DITest {
- @Autowired
- private User user;
- @Test
- public void testAutowiredPriamry(){
- System.out.println(user);
- }
- }
當(dāng)不做其他處理時,結(jié)果為:
因為有兩個User Bean(getUser , getUser2 ,@Bean未注明的情況下,默認(rèn)方法名為Bean Name)的存在,所以Spring無法確定使用那個進行注入。
修改方式:
- 在@Bean中設(shè)置name,如@Bean(name="user"),當(dāng)名字能夠匹配上private User user;時,也能完成注入。
- 將private User user改寫成getUser或getUser2任意一個,也能完成注入。道理和上面一樣,Spring首先會按照type進行匹配,如果無法匹配,再按照名字匹配,都匹配不上時,自然拋出異常。
除此之外呢,Spring為我們提供了兩個注解來消除依賴注入時的歧義問題。
- @Primary
- @Target({ElementType.TYPE, // 類、接口、枚舉類型
- ElementType.METHOD})// 方法
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- public @interface Primary {
- }
@Primary是一個設(shè)定相同類型Bean優(yōu)先級的注解,也就是說,一旦在某個類型上添加@Priamry,當(dāng)注入時,沒有明確指定Bean時,就會注入被@Priamry標(biāo)識的Bean。
- @Configuration
- @ComponentScan
- public class DIConfig {
- @Primary
- @Bean
- public User getUser(){
- User u = new User();
- u.setName("this is user");
- return u;
- }
- @Bean
- public User getUser2(){
- User u = new User();
- u.setName("this is user2");
- return u;
- }
- }
比如上面這樣,在getUser()上添加相應(yīng)注解,測試方法也能正常運行。
但是這種方法的問題就在于@Priamry可以用在很多類上,如果同一類型有多個Bean被標(biāo)注了@Primary,那么@Priamry就失去了應(yīng)有的效果。
- @Qualifier
因此,Spring又提供了@Qualifier這個注解,直接標(biāo)注在@Autowired注入的Bean上,為其明確指定注入某個Bean。
- @Target({ElementType.FIELD,
- ElementType.METHOD,
- ElementType.PARAMETER,
- ElementType.TYPE,
- ElementType.ANNOTATION_TYPE})
- @Retention(RetentionPolicy.RUNTIME)
- @Inherited
- @Documented
- public @interface Qualifier {
- String value() default "";
- }
@Qualifier可以出現(xiàn)任何@Autowired能夠出現(xiàn)的地方,與之配套使用。比如下面這樣:
- @RunWith(SpringRunner.class)
- @ContextConfiguration(classes = DIConfig.class)
- public class DITest {
- //直接指定使用getUser2進行注入
- @Autowired
- @Qualifier("getUser2")
- private User user;
- @Test
- public void testAutowiredPriamry(){
- System.out.println(user);
- }
- }
這兩種注解都可以消除歧義,推薦使用@Bean(name="xxx")和@Qualifier(value="xxx")組合使用的方式`。但是如果開發(fā)環(huán)境中沒有歧義的存在,自然也就不需要使用這些了。
當(dāng)然,上面只是對于@Autowired一些常用介紹,如果想要了解更多,可以查看Annotation-based Container Configuration。這個參考文檔當(dāng)中有著更加詳細(xì)、豐富的介紹。
總結(jié)
總得來說,Spring是如何實現(xiàn)IoC的呢?首先,Spring提供了一個獲取和管理Bean的IoC容器。然后,再提供了一套依賴注入的機制去幫助IoC容器更好地管理各個Bean之間的依賴關(guān)系,從而更好地實現(xiàn)IoC的思想。一個Bean不可能完全脫離其他Bean的依賴關(guān)系而獨立存在,當(dāng)一個Bean需要其他Bean的引入才能初始化時,就需要依賴注入這個機制。
舉例來說,假如存在一個A類想要去調(diào)用B接口的方法或者說需要B接口的一個實例。
傳統(tǒng)的程序流程是,使用一個C類實現(xiàn)B接口,然后A類創(chuàng)建一個C類的實例,從而調(diào)用其方法。
在Spring的依賴注入過程中就變成了,A類只需要在自己的內(nèi)部添加一個注入接口(廣義上的接口,不是interface這個接口),這個接口可以是構(gòu)造方法,也可以是setter方法或者說其他形式;同時添加一個對B接口的引用(private B b;)。
當(dāng)真正需要生成A類的實例時,Spring IoC容器根據(jù)A類提供的接口,為其注入相應(yīng)的Bean,而這個Bean可以是C類(class C implements B{}),也可以D類(class D implements B{})等等;具體是誰,根據(jù)Bean的裝配策略和IoC容器中的Bean來確定,不再由開發(fā)人員管理。
本文授權(quán)轉(zhuǎn)載自公眾號「良許Linux」。良許,世界500強外企Linux開發(fā)工程師,公眾號里分享大量Linux干貨,歡迎關(guān)注!