spring bean循环依赖问题分析

前言

循环依赖最根本的解决之道仍是重构设计 ,避免双向依赖。如果无法重构,优先考虑Spring提供的@Lazy或字段/Setter注入(配合三级缓存)。其他手动方式应作为备选,因为它们增加了代码与容器的耦合。

介绍

从单例Bean的初始化来看,循环依赖发生在第二步,也就是填充属性的一步。


通过spring三级缓存机制解决

1、三级缓存原理

在Spring容器的整个生命周期中,只有Scope为singleton(单例)才会注入到spring工厂中,由于单例Bean只有一个对象,于是可以使用缓存来管理它。



在Spring框架中,两个单例Bean互相依赖(即循环依赖)时,可以使用字段注入 (如@Autowired标注在字段上)或Setter注入 (如@Autowired标注在Setter方法上)来解决,Spring通过三级缓存 机制能够自动处理这种循环依赖。这种机制依赖于对象先实例化、后注入属性的顺序,因此字段注入和Setter注入都支持循环依赖。

2、通过字段注入@Autowired 解决示例

使用字段注入 (如@Autowired标注在字段上)来解决,Spring通过三级缓存 机制能够自动处理这种循环依赖。这种机制依赖于对象先实例化、后注入属性的顺序,因此字段注入支持循环依赖。

复制代码
@Component
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
}

3、两个Bean使用Setter注入解决示例

在Spring中,两个单例Bean互相依赖(即循环依赖)时,可以使用Setter注入来解决。Setter注入属于属性注入的一种,Spring容器会先实例化Bean,再通过Setter方法注入依赖,因此可以利用三级缓存提前暴露早期对象引用,从而打破循环。

复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class ClassA {
    private ClassB classB;

    // Setter注入,标注@Autowired
    @Autowired
    public void setClassB(ClassB classB) {
        this.classB = classB;
    }

    public void doSomething() {
        System.out.println("ClassA 使用了 ClassB:" + classB);
    }
}

@Component
public class ClassB {
    private ClassA classA;

    @Autowired
    public void setClassA(ClassA classA) {
        this.classA = classA;
    }

    public void doSomething() {
        System.out.println("ClassB 使用了 ClassA:" + classA);
    }
}

4、字段注入和Setter注入的区别

  • 字段注入 :在Bean实例化后,通过反射直接设置字段值(即使字段是private)。这绕过了正常的访问控制,依赖于Spring内部机制。
  • Setter注入:通过调用公共的Setter方法完成注入,符合JavaBean规范,是更标准的依赖注入方式。

虽然字段注入和Setter注入都能解决单例Bean的循环依赖,但循环依赖本身往往意味着设计不够合理(如高度耦合)。建议通过重构消除循环依赖

通过延迟初始化解决

1、@Lazy什么时候需要在去将对象注入。

@Lazy注解并不是通过Spring的三级缓存来解决循环依赖的,它采用的是延迟初始化策略。

@Lazy注解用于指示Spring延迟初始化一个Bean,或者延迟注入一个依赖。当标注在依赖注入点上时(如字段、setter、构造器参数),Spring会注入一个代理对象,而真正的目标Bean只有在第一次使用该代理时才会被创建和初始化。

java 复制代码
@Component
public class A {
    @Lazy
    @Autowired
    private B b;  // 注入的是B的代理,B实际未创建
}

@Component
public class B {
    @Autowired
    private A a;  // 正常注入A
}
java 复制代码
@Component
public class A {
    private final B b;

    public A(@Lazy B b) {  // 注入的是B的代理
        this.b = b;
    }

    public void useB() {
        b.doSomething(); // 首次调用时触发B的创建
    }
}

@Component
public class B {
    private final A a;

    public B(A a) {       // 注入真实的A(此时A已创建)
        this.a = a;
    }
}

2、在使用时从容器获取依赖

​ 可以在需要时直接从容器中获取依赖。

java 复制代码
@Component
public class A implements ApplicationContextAware {
    private ApplicationContext context;
    private B b;  // 可以缓存,避免重复获取

    @Override
    public void setApplicationContext(ApplicationContext context) {
        this.context = context;
    }

    public void doSomething() {
        // 延迟获取B(首次获取后可以缓存)
        if (b == null) {
            b = context.getBean(B.class);
        }
        b.help();
    }
}

@Component
public class B {
    private final A a;

    public B(A a) {
        this.a = a;
    }

    public void help() {
        System.out.println("B帮助A完成工作");
    }
}

无法通过构造函数解决

这段代码展示的是典型的构造器循环依赖,Spring无法自动解决

java 复制代码
@Component
public class A {

    // B成员变量
    private B b;

    public A(B b){
        System.out.println("A的构造方法执行了...");
        this.b = b;
    }
}

@Component
public class B {

    // A成员变量
    private A a;

    public B(A a){
        System.out.println("B的构造方法执行了...");
        this.a = a;
    }
}
相关推荐
错把套路当深情几秒前
Java 全方向开发技术栈指南
java·开发语言
han_hanker9 分钟前
springboot 一个请求的顺序解释
java·spring boot·后端
2501_921649499 分钟前
原油期货量化策略开发:历史 K 线获取、RSI、MACD 布林带计算到多指标共振策略回测
后端·python·金融·数据分析·restful
杰克尼10 分钟前
SpringCloud_day05
后端·spring·spring cloud
MaCa .BaKa10 分钟前
44-校园二手交易系统(小程序)
java·spring boot·mysql·小程序·maven·intellij-idea·mybatis
ServBay15 分钟前
阿里超强编程模型Qwen 3.6 -Plus 发布,国产编程AI的春天?
后端·ai编程
用户83562907805121 分钟前
使用 Python 自动生成 Excel 柱状图的完整指南
后端·python
希望永不加班28 分钟前
SpringBoot 静态资源访问(图片/JS/CSS)配置详解
java·javascript·css·spring boot·后端
Soofjan32 分钟前
Go 内存管理(3):内存分配源码
后端
oh LAN44 分钟前
RuoYi-Vue-master:Spring Boot 4.x (JDK 17+) (环境搭建)
java·vue.js·spring boot