思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
Spring
的依赖注入(Dependency Injection,DI
)是 Spring
框架的核心概念之一,其主要用于管理和解耦组件之间的依赖关系。而依赖注入
的主要目标是将类之间的依赖关系从类内部硬编码改为外部配置,从而避免程序中出现太多的硬编码方式。
进一步,谈及依赖注入无法避开的一个注解就是@Autowired
。虽然@Autowired
注解的使用极大的简化了日常程序的编写,但是当@Autowired
使用不当时也导致一些问题,接下来我们列举一些笔者工作中遇到的一些错误使用的@Autowired
例子。
前言
@Autowired
注解是 Spring 框架提供的一种依赖注入方式,其作用在于告诉 Spring
在那里注入依赖项。当你在一个类的字段、构造函数或者方法
上使用 @Autowired
注解时,Spring
将会尝试自动注入相关的依赖项。进一步,其大致有如下三种使用场景
- 字段注入:你可以将
@Autowired
注解应用到字段上,Spring
将会自动在容器中查找并注入匹配类型的 bean。 - 构造函数注入:你可以将
@Autowired
注解应用到构造函数上,Spring
将会尝试通过构造函数注入相关的依赖项。构造函数注入通常被认为是最佳的依赖注入方式,因为它可以确保对象在创建时拥有完整的依赖关系。 - 方法注入:你可以将
@Autowired
注解应用到Spring
方法上,Spring
将会通过调用Spring
方法来注入相关的依赖项。
总之,@Autowired
注解是Spring
框架中的一种依赖注入方式,它与依赖注入的核心理念密切相关,用于自动注入依赖项,减少紧耦合,提高可维护性和可测试性。
@Autowired
使用不当导致循环依赖无法解决
尝试用IDEA
的Java
开发者一定注意到当我们使用@Autowired
注入字段时,IDEA
会提示:"Spring
团队官方建议使用构造器
注入的方式来注入的依赖关系"。所以,可能有些开发者在后续的开发中都会使用构造器
的方式来注入bean
,但这种情况容易导致Spring
内部无法解决循环依赖的问题。
我们先来看如下代码:
@Slf4j
@Service
public class UserManageService {
public UserService userService;
@Autowired
public UserManageService(UserService userService) {
this.userService = userService;
}
}
@Service
@Slf4j
public class UserService {
private UserManageService userManageService;
@Autowired
public UserService(UserManageService userManageService) {
this.userManageService = userManageService;
}
}
可以看到在上述代码中 UserManageService
中需要注入一个 UserService
,同样的 UserService
也需要注入一个UserManageService
。从而形成了依赖关系间的循环依赖,如果尝试运行上述代码会提示如下的错误:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| userManageService defined in file UserManageService.class]
↑ ↓
| userService defined in file UserService.class]
└─────┘
不难发现,SpringBoot
在启动时已经将两者间的循环依赖关系标出,可能你会疑惑Spring
内部不是可以处理循环依赖吗?怎么还能无法注入bean
呢? 要想知道问题的答案我们先来看下Spring
内部究竟是如何来解决循环依赖的。
Spring
解决循环依赖问题的方式是通过**“提前暴露中间对象”** 的方式。当Spring容器在构造bean对象时发现循环依赖时,它会将其中一个bean对象提前暴露为“中间对象”,并完成该对象的属性注入。然后再继续构建其他bean对象,直到所有的bean对象都被构建完成。最后,Spring
容器将已经完成属性注入的“中间对象”注入到其他bean对象中,完成整个bean对象的构建过程。
进一步,这个 “提前暴露中间对象” 的过程是通过三级缓存
实现的。当Spring
容器构建bean
对象时,会将正在构建的bean
对象放入一级缓存中。如果在构建该bean
对象的过程中,如果发现依赖的另一个bean
对象也在构建中,则会将该bean
对象放入二级缓存中,并在二级缓存中标记该bean对象已经被提前暴露为中间对象。
进一步,只有当依赖的另一个bean
对象构建完成后,Spring容器会将该bean对象放入三级缓存中,并对二级缓存中的所有标记为 提前暴露 bean
对象进行属性注入。
熟悉Spring
对于循环依赖的处理方式后,我们再来看为什么构造器
形式的循环依赖无法解决。Spring
中bean
处理流程一般是完成实例化,并且将其放到singletonFactories(三级缓存)
中。
此后在执行populate
方法进行属性填充,如果填充过程中需要依赖其他对象在进行创建。而构造器依赖注入,由于构造器是在实例化时调用的,此时bean
还没有实例化完成,如果此时出现了循环依赖,一、二、三级缓存并没有Bean实例的任何相关信息。因为bean
需要在实例化之后才放入三级缓存中,因此当getBean的时候缓存并没有命中,这样就抛出了循环依赖的异常。
如下这张图详细的说明了Spring
内部对于不同方式循环依赖问题的支持情况。
其实使用@Autowired
的构造器注入导致循环依赖
问题无法解决,一定程度上可以归因于对Spring
循环依赖解决理解的不够透彻所导致的。
容器中有重名bean
无法注入
当使用@Autowired
注入bean
时,恰好当前容器中有多个待候选的bean
时,此时会提示
required a single bean, but 2 were found
什么意思呢?你可以大致理解为:此时自动注入时只需要一个 bean
,但Spring
容器内部却提供了多个。那这个问题该如何解决呢?在分析问题执行前,你首先需要知道@Autowired
是被AutowiredAnnotationBeanPostProcessor
所处理。后续我们的讨论也主要围绕AutowiredAnnotationBeanPostProcessor
来进行讨论。
在Spring
中,当一个 bean
被构建时,核心包括两个基本步骤:
AbstractAutowireCapableBeanFactory#createBeanInstance
方法,利用反射机制
构造出这个 Bean
AbstractAutowireCapableBeanFactory#populate
方法, 对这个bean
进行属性设置。而AutowiredAnnotationBeanPostProcessor
在主要在属性填充中populate
方法内进行执行。具体来看populateBean
protected void populateBean(String beanName,
RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
// 省略无关代码
pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrapped
}
}
}
}
可以看到,在populateBean
内部,其会遍历所有的BeanPostProcessors
然后执行其中的postProcessProperties
方法。此时,或许你已经明白,@Autowired
的自动注入一定会在AutowiredAnnotationBeanPostProcessor
中的postProcessPropertyValues
所有体现。
AutowiredAnnotationBeanPostProcessor # postProcessPropertyValues
public PropertyValues postProcessPropertyValues(
PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) {
return postProcessProperties(pvs, bean, beanName);
}
可以看到,postProcessPropertyValues
又会将逻辑委托给postProcessProperties
来完成。
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
// 寻找带注入的bean
InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
try {
// 注入
metadata.inject(bean, beanName, pvs);
}
catch (BeanCreationException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
}
return pvs;
}
进一步,可以看到在postProcessProperties
主要有两步逻辑
findAutowiringMetadata
inject
此处,我们重点关注其中的inject
方法。
public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
Collection checkedElements = this.checkedElements;
Collection elementsToIterate =
(checkedElements != null ? checkedElements : this.injectedElements);
if (!elementsToIterate.isEmpty()) {
for (InjectedElement element : elementsToIterate) {
// 元素注入
element.inject(target, beanName, pvs);
}
}
}
AutowiredFieldElement#inject
protected void inject(Object bean, @Nullable String beanName,
@Nullable PropertyValues pvs) throws Throwable {
Field field = (Field) this.member;
Object value;
// 解析待注入的bean信息
value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
// 省略无关逻辑
if (value != null) {
// 反射破坏访问权限
ReflectionUtils.makeAccessible(field);
// 设定内容
field.set(bean, value);
}
}
}
可以注意到,其中注入bean
的逻辑主要通过beanFactory.resolveDependency
完成。其解析逻辑大致如下:
@Qualifier
注解,如果有则根据名称精确匹配;@Qualifier
注解,则调用 determineAutowireCandidate
方法来选出优先级最高的依赖作为待注入的bean
,而优先级可以通过@Primary
、@Priority
来控制。bean
名字的严格匹配来决策即,无法匹配到则返回 null
。至此,相信你对@Autowired
注解的自动原理已经有了深刻的认识,那对于required a single bean, but 2 were found
这一问题其实有如下三种手段
@Qualifier
进行精确匹配@Priority
设定优先级bean
标注@Primary
注解总结
@Autowired
注解可以说是Spring
开发中最常用的一个注解了,但是不正确的使用也可能导致很多问题,本文主要对使用@Autowired
过程中出现的:
@Autowired
使用不当导致循环依赖无法解决bean
无法注入等问题进行了阐述分析。这也是笔者最近调试项目时所遇到的问题,希望能帮助你更好的使用@Autowired
注解。当然,如果你在使用@Autowired
过程中还遇到过其他问题,也可在评论区留言,让更多的开发者看到。