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;
    }
}
相关推荐
期待のcode1 小时前
SpringBoot连接Redis
spring boot·redis·后端
Coder_Boy_2 小时前
Java(Spring AI)传统项目智能化改造——商业化真实案例(含完整核心代码+落地指南)
java·人工智能·spring boot·spring·微服务
五阿哥永琪2 小时前
1. 为什么java不能用is开头来做布尔值的参数名,会出现反序列化异常。
java·开发语言
笑我归无处2 小时前
Springboot+mybatisplus配置多数据源+分页
spring boot·后端·mybatis
lizhongxuan3 小时前
AI 从工具调用到自主进化:SkillSMP 与 EvoMap
后端
暴力袋鼠哥3 小时前
基于 Spring Boot 3 + Vue 3 的农产品在线销售平台设计与实现
vue.js·spring boot·后端
canonical_entropy3 小时前
DDD 概念澄清:那些教程不会告诉你的事
后端·低代码·领域驱动设计
chilavert3183 小时前
技术演进中的开发沉思-371:final 关键字(中)
java·前端·数据库
海边的Kurisu4 小时前
Mybatis-Plus | 只做增强不做改变——为简化开发而生
java·开发语言·mybatis