【问题】容器部署场景Spring Bean偶尔循环依赖问题

问题描述

在本地开发中不会发生循环依赖问题,但是在容器场景下,制作成镜像启动后异常出现Bean的循环依赖。

问题原因

开发者在代码中使用构造函数注入来引用依赖的 Bean,这种方式可能导致循环依赖问题。虽然 Spring 框架具备循环依赖的处理机制,但它仅适用于通过 @Resource 或 @Autowired 注解进行的 Setter 方法注入或字段注入。如果开发者使用构造函数注入,当Bean的初始化未发生循环依赖,则启动没问题,对应日常开发中不会有循环依赖问题,但是在一些Docker容器场景,则会偶发抛出循环依赖异常。

深入剖析原理

在这里插入图片描述

进一步深入分析为什么本地没有循环依赖问题,但是容器里或服务器里偶发会出现循环依赖问题,问题的根源在于 Spring Boot 在扫描和实例化 Bean 时的顺序并非固定,而是可能受到 Jar 包中文件列表顺序 的影响。

  1. Jar 包文件列表的顺序
  • 在 Java 中,Jar 文件实际上是一个压缩包,内部包含了许多类文件和资源文件。当应用程序运行时,可能需要遍历这些文件。例如,Spring Boot 在启动时需要扫描特定路径下的类和资源,以识别需要创建的 Bean。
  • 在 Java 中,可以使用 JarFile.entries() 方法获取 Jar 包中的所有条目。然而,需要注意的是:JarFile.entries() 返回的并非是一个稳定有序的列表。根据 Java 官方文档,entries() 返回一个 Enumeration,但并未保证返回的顺序
  • 不同的平台或工具在打包 Jar 文件时,可能导致文件列表的顺序不同。例如,在 Windows 和 Linux 上生成的 Jar 文件,即使内容完全相同,但内部文件的排列顺序可能不同。
  1. Spring Boot 的资源匹配逻辑
    Spring Boot 使用 PathMatchingResourcePatternResolver 类来处理资源的匹配和加载。其中,doFindPathMatchingJarResources() 方法负责在 Jar 文件中查找与指定模式匹配的资源。
protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource, String subPattern) throws IOException {
    ...
    JarFile jarFile = ((JarURLConnection) rootDirURL.openConnection()).getJarFile();
    Enumeration<JarEntry> entries = jarFile.entries();
    while (entries.hasMoreElements()) {
        JarEntry entry = entries.nextElement();
        String entryPath = entry.getName();
        // 资源匹配逻辑
        if (matcher.match(subPattern, entryPath)) {
            // 处理匹配的资源
        }
    }
    ...
}

如上所示,jarFile.entries() 返回的文件列表顺序不定。这意味着,Spring Boot 在扫描资源并识别 Bean 时,可能因为文件顺序的不同而导致 Bean 的加载顺序发生变化。

  1. Bean 加载顺序对循环依赖的影响
    在大多数情况下,Bean 的加载顺序并不会影响应用程序的启动。然而,当存在循环依赖时,Bean 的加载顺序可能决定了 Spring 能否成功地解决这种依赖关系。

举个例子,假设存在两个 Bean:BeanA 和 BeanB,它们互相依赖。如果 BeanA 先被加载,Spring 可能能够通过代理或其他机制解决依赖。然而,如果 BeanB 先被加载,可能就会导致无法解决的循环依赖,进而抛出异常。

由于 Jar 包中文件列表的顺序不定,导致 Bean 的加载顺序在不同的环境或不同的启动中可能有所不同,这解释了为什么循环依赖问题会 偶尔 发生。

参考链接

解决方案

方案1:干掉循环依赖

最根本的解决方案是 重新设计 Bean 的依赖关系,避免循环依赖的出现。

开发规范:默认禁用构造器注入的循环依赖
PS:在升级SpringBoot3.0后,对应Spring6.0 开始,默认情况下不再允许通过构造器注入的方式解决循环依赖。如果两个 Bean 之间通过构造器注入存在循环依赖,Spring 将会直接抛出 BeanCurrentlyInCreationException,而不再试图通过懒加载代理等方式来解决这个问题。

使用 @DependsOn 注解

Spring 提供了 @DependsOn 注解,允许我们显式地指定 Bean 的加载顺序。

@Component
@DependsOn("beanB")
public class BeanA {
    // ...
}

@Component
public class BeanB {
    // ...
}

通过这种方式,可以确保 BeanB 在 BeanA 之前被初始化。然而,需要谨慎使用该注解,避免引入新的依赖问题。

代码中定义某个Bean延迟加载
@Component
public class BeanA {
    private final BeanB beanB;

    public BeanA(@Lazy BeanB beanB) {
        this.beanB = beanB;
    }
    // ...
}

方案3:启用延迟加载

spring.main.lazy-initialization=true 是 Spring Boot 应用中的一个配置选项,它用于启用 延迟初始化功能。
在 Spring Boot 2.2 及以上版本中,lazy-initialization 的默认值是 false。这意味着默认情况下,Spring Boot 应用中的所有 Bean 都是在应用启动时立即初始化的,而不是在第一次使用时才进行初始化。

延迟初始化 (Lazy Initialization) 的概念

在 Spring 应用中,默认情况下,所有的 @Bean 和组件(如 @Component, @Service, @Repository 等)在应用启动时都会被立即创建和初始化。这意味着在应用程序启动时,所有这些 Bean 都会被加载到 Spring 应用上下文中,无论它们何时在应用的生命周期中被使用。

启用延迟初始化 (lazy initialization) 后,**只有在第一次需要使用某个 Bean 的时候,该 Bean 才会被创建和初始化。**这可以加快应用启动的速度,尤其是在有许多不需要立即初始化的 Bean 时。

延迟初始化的优缺点

优点

  • 减少应用启动时间:对于大型应用来说,减少不必要的 Bean 初始化可以显著提高启动速度。
  • 资源节约:只有在需要时才会创建 Bean,节省了内存和 CPU 资源。

缺点

  • 潜在的延迟:由于 Bean 在第一次使用时才会被创建,这可能导致在应用运行过程中首次调用某个服务时出现轻微的延迟。
  • 调试复杂度:延迟初始化可能会导致某些问题(如配置错误、Bean 的依赖问题)直到运行时才暴露出来,这可能增加调试的复杂性。

注意事项

  • 延迟初始化适用于不需要立即加载的服务和组件,但对于关键服务(如启动时需要立即使用的 Bean),你可能希望保持默认的非延迟加载方式。
  • 延迟初始化可以通过在特定 Bean 上使用 @Lazy(false) 来排除那些需要立即初始化的 Bean。
启用延迟初始化的场景

开发环境:加快开发过程中应用的启动速度,减少等待时间。
测试环境:在单元测试时,只加载特定的 Bean,而不是所有的 Bean,减少测试的开销。
微服务:在某些微服务架构中,可能希望某些 Bean 仅在真正需要时才加载,以节省资源。

如何启用延迟初始化

可以在 application.properties 或 application.yml 中配置 spring.main.lazy-initialization=true 来启用延迟初始化。
application.properties 示例

spring.main.lazy-initialization=true

application.yml 示例

spring:
  main:
    lazy-initialization: true
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值