不正确使用@Autowired导致的这些问题你知道吗?

2023年 10月 2日 50.4k 0

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜

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使用不当导致循环依赖无法解决

尝试用IDEAJava开发者一定注意到当我们使用@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对于循环依赖的处理方式后,我们再来看为什么构造器形式的循环依赖无法解决。Springbean处理流程一般是完成实例化,并且将其放到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过程中还遇到过其他问题,也可在评论区留言,让更多的开发者看到。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论