問題描述
有同事在開發新功能測試時,報了個錯,大致就是,在使用 @Autowired 注入時,某個類有兩個bean,一個叫a,一個叫b,Spring不知道該使用哪個bean注入。
一般這種情況應該宣告注入哪個bean,他沒有宣告,他不知道這個類有兩個bean,他說他和別人寫的一樣,別的都不報錯。
OK,那來分析下吧。
問題分析
前提:@Autowired是根據型別(byType)進行自動裝配的。
- 當找不到一個匹配的 Bean 時,Spring 容器將丟擲 BeanCreationException 異常,並指出必須至少擁有一個匹配的 Bean。
- 如果當Spring上下文中存在不止一個候選Bean時,就會丟擲BeanCreationException異常;
- 如果Spring上下文中不存在候選Bean,也會丟擲BeanCreationException異常。
- 容器中有該型別的候選Bean
- 容器中只含有一個該型別的候選Bean
問題探究
public class Student{ private String name; //getter and setter... }
然後我們在 Spring 容器中建立多個 Student 的例項,如下:
我們通過 XML 配置檔案的方式在 Spring 的配置檔案裡實現一個型別多個 bean。
如下,建立了兩個 Student 的 bean ,id 分別為 student 和 student02,對應的bean 的name 屬性 分別為小紅和小明。
<bean id="student" class="com.autowiredtest.entity.Student"> <property name="name" value="小紅"/> </bean> <bean id="student02" class="com.autowiredtest.entity.Student"> <property name="name" value="小明"/> </bean>
我們也可以通過使用 配置類+註解 的方式實現一個型別多個 bean:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class StudentConfiguration{ @Bean Student student03(){ Student student = new Student(); student.setName("小華"); return student; } @Bean Student student04(){ Student student = new Student(); student.setName("小玲"); return student; } }
@Controller public class AutowiredTestController{ @Autowired private Student student; @RequestMapping("/AutowiredTest") @ResponseBody public String loanSign(){ String docSignUrl = "ok"; System.out.println("--------------要列印了------------"); System.out.println(student.getName()); System.out.println("--------------列印結束------------"); return docSignUrl; } }
(這裡就是用一個簡單的spring mvc的小demo來驗證這個問題。)
是不是很奇怪?和上面說的不符合啊!這裡 Student 類有4個例項,分別為 student、student02、student03和student04。
非但沒有在呼叫時丟擲 BeanCreationException 異常,反而正常執行,輸出【小紅】,說明注入的是 id 為 student 的 bean。
大膽的猜想:多個 bean 時,是根據 Student 的變數名自動匹配 bean id!
即 :當@Autowired private Student student; 時
我們的 Student 變數名是 student ,那麼在 Spring 為其注入時,如果有多個 bean 的話就預設去容器中找 bean id 為 student 得那個 bean。
驗證一下
把 Student 的變數名改為 student02,@Autowired private Student student02
重啟,並訪問http://localhost:8080/AutowiredTest,控制檯輸出:
同樣,改為 student03、student04控制檯相應輸出小華、小玲。
所以我們的大膽猜想是正確的!這裡使用的 Spring 版本為 4.2.0.RELEASE。
本文永久連結:https://www.cnblogs.com/ibigboy/p/11236729.html
大膽的猜想,和上面說的不一致,那是不是版本相容了這個問題?
驗證一下
把版本改低一點。首先,把 Spring 版本改為2.5(@Autowired第一次出現在該版本),這時候 @ResponseBody @Configuration 以及 @Bean都不能用了(更高版本才能用)。
這時候啟動專案,不報錯,訪問http://localhost:8080/AutowiredTest,報錯:
控制檯錯誤資訊:
1 嚴重: Context initialization failed 2 org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'autowiredTestController': Autowiring of fields failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.autowiredtest.entity.Student com.autowiredtest.controller.AutowiredTestController.student02; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [com.autowiredtest.entity.Student] is defined: expected single matching bean but found 2: [student, student02] 3 at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessAfterInstantiation(AutowiredAnnotationBeanPostProcessor.java:231) 4 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:978) 5 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:539) 6 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory$1.run(AbstractAutowireCapableBeanFactory.java:485) 7 at java.security.AccessController.doPrivileged(Native Method) 8 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:455) 9 at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:251) 10 at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:169) 11 at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:248) 12 at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:170) 13 at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:413) 14 at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:735) 15 at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:369) 16 at org.springframework.web.servlet.FrameworkServlet.createWebApplicationContext(FrameworkServlet.java:332) 17 at org.springframework.web.servlet.FrameworkServlet.initWebApplicationContext(FrameworkServlet.java:266) 18 at org.springframework.web.servlet.FrameworkServlet.initServletBean(FrameworkServlet.java:236) 19 at org.springframework.web.servlet.HttpServletBean.init(HttpServletBean.java:126) 20 at javax.servlet.GenericServlet.init(GenericServlet.java:158) 21 at org.apache.catalina.core.StandardWrapper.initServlet(StandardWrapper.java:1279) 22 at org.apache.catalina.core.StandardWrapper.loadServlet(StandardWrapper.java:1192) 23 at org.apache.catalina.core.StandardWrapper.allocate(StandardWrapper.java:864) 24 at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:134) 25 at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122) 26 at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:501) 27 at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:170) 28 at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:98) 29 at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:950) 30 at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116) 31 at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408) 32 at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1040) 33 at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:607) 34 at org.apache.tomcat.util.net.AprEndpoint$SocketProcessor.doRun(AprEndpoint.java:2441) 35 at org.apache.tomcat.util.net.AprEndpoint$SocketProcessor.run(AprEndpoint.java:2430) 36 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) 37 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) 38 at java.lang.Thread.run(Thread.java:745) 39 Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.autowiredtest.entity.Student com.autowiredtest.controller.AutowiredTestController.student02; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [com.autowiredtest.entity.Student] is defined: expected single matching bean but found 2: [student, student02] 40 at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredElement.inject(AutowiredAnnotationBeanPostProcessor.java:375) 41 at org.springframework.beans.factory.annotation.InjectionMetadata.injectFields(InjectionMetadata.java:61) 42 at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessAfterInstantiation(AutowiredAnnotationBeanPostProcessor.java:228) 43 ... 35 more 44 Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [com.autowiredtest.entity.Student] is defined: expected single matching bean but found 2: [student, student02] 45 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.resolveDependency(AbstractAutowireCapableBeanFactory.java:425) 46 at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredElement.inject(AutowiredAnnotationBeanPostProcessor.java:361) 47 ... 37 more
控制檯錯誤輸出圖:
關鍵的異常資訊:
這時候報了預期的錯了。
我們再增大版本號,去測試一下到底是哪個版本號開始相容了這個問題。
這是版本的釋出情況,採用二分法逼近。
先從當前用的版本 4.2.0.RELEASE 換到 3.2.18.RELEASE(Spring 3.x的最後一個版本)也是沒問題的,3.0.0.RELEASE也沒問題,2.5.5報錯,2.5.6報錯,2.5.6.SEC03報錯。
因此可以斷定,從 Spring 3.x 開始相容了這個問題,更加人性化。
所以上述關於 @Autowired 的使用規則要發生變化了:
- 容器中有該型別的候選Bean
- 容器中可以含有多個該型別的候選Bean
- Spring 3.x以後,使用 @Autowired 時變數名一定要和該型別多個 Bean 的其中一個相同(即上文中的@Autowired private Student student;,student 就是多個Bean中其中一個bean的id)
- 若違反第三條規則,會丟擲 BeanCreationException 異常
- Spring 3.x 之前只能有一個 bean
假如我們想自定義變數名呢?
idea 直接告訴你,現在有兩個 bean ,一個叫 student 另一個叫 student02,你現在寫的變數名不是這倆種的任一個,你寫的不對,給你報錯!
而對於另外一些 IDE 則是沒這麼只能,如 eclipse。那就只有等到測試的時候才能發現了。
回到正題,怎麼自定義變數名呢?
有2種方法:
@Autowired @Qualifier("student") private Student stu;
這樣 Spring 會找到 id 為 student 的 bean 進行裝配。
@Autowired(required = false) public Student stu
但是idea可不慣著你,依舊給你報錯提示,雖然這時候可以忽略它繼續啟動,但訪問時還是會報 BeanCreationException:
這個和 @Autowired 和 @Qualifier 的作用一樣,可以理解為是二者的合併吧。
總結
附:為什麼@Autowired 和 @Qualifier註解不合成一個?
//對成員變數使用 @Qualifier 註釋 public class Boss { @Autowired private Car car; @Autowired @Qualifier("office") private Office office; … }
//對建構函式變數使用 @Qualifier 註釋 public class Boss { private Car car; private Office office; @Autowired public Boss(Car car , @Qualifier("office")Office office){ this.car = car; this.office = office ; } }