Spring框架使用@Autowired自动装配引发的讨论
问题描述
有同事在开发新功能测试时,报了个错,大致就是,在使用 @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以后)
- Spring 3.x 之前Spring容器中只能有一个Bean,否则抛出 BeanCreationException 异常
- Spring 3.x以后,可以有多个Bean使用 @Autowired 时变量名一定要和该类型多个Bean 的其中一个相同
(即上文中的@Autowired private Student student;,student 就是多个Bean中其中一个bean的id) - 若违反第4条规则,会抛出 BeanCreationException 异常
假如我们想自定义变量名呢?
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 ; } }