Spring框架使用@Autowired自動裝配引起的討論

問題描述

有同事在開發新功能測試時,報了個錯,大體就是,在使用 @Autowired 注入時,某個類有兩個bean,一個叫a,一個叫b,Spring不知道該使用哪一個bean注入。java

通常這種狀況應該聲明注入哪一個bean,他沒有聲明,他不知道這個類有兩個bean,他說他和別人寫的同樣,別的都不報錯。web

OK,那來分析下吧。spring

問題分析

前提:@Autowired是根據類型(byType)進行自動裝配的apache

在默認狀況下只使用 @Autowired 註解進行自動注入時,Spring 容器中匹配的候選 Bean 數目必須有且僅有一個。tomcat

使用@Autowired 註解時要注意如下狀況:mvc

  • 當找不到一個匹配的 Bean 時,Spring 容器將拋出 BeanCreationException 異常,並指出必須至少擁有一個匹配的 Bean。app

  • 若是當 Spring 上下文中存在不止一個候選Bean時,就會拋出 BeanCreationException 異常;eclipse

  • 若是 Spring 上下文中不存在候選 Bean,也會拋出 BeanCreationException 異常。ide

因此在使用 @Autowired 註解時要知足如下條件:函數

  • 容器中有該類型的候選Bean

  • 容器中只含有一個該類型的候選Bean

問題探究

什麼意思呢?咱們用代碼說話。

建立實體

首先,咱們建一個實體類 Student :

public class Student{
    private String name;
    //getter and setter...
}
複製代碼

構造多個Bean

而後咱們在 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;
    }
}
複製代碼

固然這個 StudentConfiguration 配置類須要被註解掃描器掃描到,即要在掃包的配置里加上這個類所在的包。 因此如今Student 類有4個 Bean,student、student0二、student03和student04。

(注:這裏體現了用兩種方式來實現一個類型多個bean。)

使用 Student 的 bean

在controller聲明 Student 類型的變量,並使用 @Autowired 注入

@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來驗證這個問題。)

運行web項目,期間沒有報錯,訪問http://localhost:8080/AutowiredTest,控制檯輸出:

是否是很奇怪?和上面說的不符合啊!這裏 Student 類有4個實例,爲啥沒報錯。

非但沒有在調用時拋出 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,控制檯輸出:

一樣,改成 student0三、student04控制檯相應輸出小華、小玲。

因此咱們的大膽猜測是正確的!這裏使用的 Spring 版本爲 4.2.0.RELEASE。

本文永久連接:juejin.im/post/5d4163…

大膽的猜測:和上面說的不一致,那是否是版本兼容了這個問題?

驗證一下

把版本改低一點。首先,把 Spring 版本改成2.5(@Autowired第一次出如今該版本),這時候 @ResponseBody @Configuration 以及 @Bean 都不能用了(更高版本才能用),因此如今有兩個 Bean ,student 和 student02。

這時候啓動項目,不報錯,訪問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報錯(2.x的最後一個版本)。

所以能夠判定,從 Spring 3.x 開始兼容了這個問題,使更加人性化。

因此上述關於 @Autowired 的使用規則要發生變化了:

  • 容器中有該類型的候選Bean

  • 容器中能夠含有多個該類型的候選Bean(Spring 3.x之後)

  • Spring 3.x之後,單獨使用 @Autowired 時變量名必定要和該類型多個 Bean 的其中一個相同(即上文中的@Autowired private Student student;,student 就是多個Bean中其中一個Bean的id)

  • 若違反第三條規則,會拋出 BeanCreationException 異常

  • Spring 3.x 以前只能有一個 bean,不然拋出 BeanCreationException 異常

假如咱們想自定義變量名呢?

若是我建立變量的時候就是想自定義變量名,咋辦? 如:@Autowired private Student stu; 這樣的話對於一些 IDE 這樣寫完就直接回給出警告或直接報錯,請看:

idea 直接告訴你,如今有兩個 bean ,一個叫 student 另外一個叫 student02,你如今寫的變量名不是這倆種的任一個,你寫的不對,給你報錯!

而對於另一些 IDE 則是沒這麼智能,如 eclipse。那就只有等到測試的時候才能發現了。

回到正題,怎麼自定義變量名呢?

有2種方法:

一、@Autowired 和 @Qualifier

咱們可使用 @Qualifier 配合 @Autowired 來解決這些問題。 和找不到一個類型匹配 Bean 相反的一個錯誤是:若是 Spring 容器中擁有多個候選 Bean,Spring 容器在裝配時也會拋出 BeanCreationException 異常(Spring 3.x以後)。

Spring 容許咱們經過 @Qualifier 註釋指定注入 Bean 的名稱,這樣就消除歧義了。 因此 @Autowired 和 @Qualifier 結合使用時,自動注入的策略就從 byType 轉變成 byName 了。

@Autowired   
@Qualifier("student")   
private Student stu;  
複製代碼

這樣 Spring 會找到 id 爲 student 的 bean 進行裝配。

另外,當不能肯定 Spring 容器中必定擁有某個類的 Bean 時,能夠在須要自動注入該類 Bean 的地方可使用 @Autowired(required = false),這等於告訴 Spring:在找不到匹配 Bean 時也不報錯。(required默認是true) 如:

@Autowired(required = false)   
public Student stu 
複製代碼

可是idea可不慣着你,依舊給你報錯提示,雖然這時候能夠忽略它繼續啓動,但訪問時仍是會報 BeanCreationException:

固然,通常狀況下,使用 @Autowired 的地方都是須要注入 Bean 的,使用了自動注入而又容許不注入的狀況通常僅會在開發環境或測試時碰到(如爲了快速啓動 Spring 容器,僅引入一些模塊的 Spring 配置文件),因此 @Autowired(required = false) 會不多用到。

二、使用@Resource(name = "xxx") 註解

這個和 @Autowired 和 @Qualifier 的做用同樣,能夠理解爲是兩者的合併吧。

總結

分析了這麼多,我那個同事的問題是由於啥呢? 仔細看代碼,那兩個 bean 一個id是類名小寫,一個id是類名小寫並加了別的後綴。 而他使用時起的變量名不是類名小寫的,直接就是類名(很難發現寫的是大寫),因此匹配不到任何一個bean。 這是工做不細心致使的,同時也引起了對@Autowired注入時兼容性的研究分析。


附:爲何@Autowired 和 @Qualifier註解不合成一個?

@Autowired 能夠對成員變量、方法以及構造函數進行註釋,而 @Qualifier 的標註對象是成員變量、方法入參、構造函數入參。 正是因爲註釋對象的不一樣,因此 Spring 不將 @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 ;     
     }     
}
複製代碼
相關文章
相關標籤/搜索