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;
    }
}
相关推荐
半瓶榴莲奶^_^1 小时前
jvm java虚拟机
java·jvm
invicinble7 小时前
这里对java的知识体系做一个全域的介绍
java·开发语言·python
小码哥_常7 小时前
MyBatis-Plus:让数据库操作飞起来的神器
后端
wbs_scy7 小时前
【Linux 线程进阶】进程 vs 线程资源划分 + 线程控制全详解
java·开发语言
ss2737 小时前
食谱推荐系统功能测试如何写?
java·数据库·spring boot·功能测试
2301_811274318 小时前
基于SpringBoot的智能家居管理系统
spring boot·后端·智能家居
AI人工智能+电脑小能手8 小时前
【大白话说Java面试题】【Java基础篇】第15题:JDK1.7中HashMap扩容为什么会发生死循环?如何解决
java·开发语言·数据结构·后端·面试·哈希算法
舒一笑8 小时前
我把设备指纹生成逻辑拆开了:它到底凭什么区分不同设备?
后端·程序员·掘金技术征文
try2find8 小时前
打印ascii码报错问题
java·linux·前端
014-code8 小时前
CompletableFuture 实战模板(超时、组合、异常链处理)
java·数据库