目录

Spring 如何解决循环依赖

在开发过程中,有遇到过Spring循环依赖问题吗?怎么解决的?

什么是 Spring 循环依赖?

简单来说,Spring 循环依赖 就是两个或多个 Bean 之间互相持有对方的引用,形成闭环依赖关系。比如:

  • A Bean 的创建需要依赖 B Bean,而 B Bean 的创建又需要依赖 A Bean;

  • 更复杂的情况:A → B → C → A 这样的环形依赖。

举个直观的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// A类依赖B类
@Component
public class A {
    @Autowired
    private B b;
}

// B类依赖A类
@Component
public class B {
    @Autowired
    private A a;
}

当 Spring 容器初始化时,创建 A 会先去创建 B,创建 B 又要先创建 A,形成 “死循环”,如果没有 Spring 的特殊处理,就会抛出 BeanCurrentlyInCreationException 异常。

实际开发中遇到的循环依赖场景

我在实际项目(包括基于若依框架的开发)中多次遇到循环依赖,常见场景有:

  • 业务层互相依赖:比如用户服务(UserService)依赖订单服务(OrderService),订单服务又需要查询用户信息,反向依赖 UserService;

  • 控制器与服务层循环依赖:若依框架中,某个 Controller 注入了 Service,而该 Service 又通过某些方式(如事件监听、工具类)反向注入了 Controller;

  • 跨模块依赖:微服务拆分不彻底时,模块 A 的 Bean 依赖模块 B 的 Bean,模块 B 又依赖模块 A 的 Bean;

  • 自定义 BeanPostProcessor 引发的循环依赖:自定义后置处理器时注入了业务 Bean,而业务 Bean 又依赖后置处理器。

Spring 对循环依赖的默认处理机制

首先要明确:Spring 仅能解决「单例 Bean」的「setter 注入 / 字段注入」的循环依赖,其他情况无法自动解决。

Spring 解决循环依赖的核心是「三级缓存」:

  • 一级缓存(singletonObjects):存放完全初始化完成的单例 Bean;

  • 二级缓存(earlySingletonObjects):存放提前曝光的、未完全初始化的 Bean(仅实例化,未填充属性和执行初始化方法);

  • 三级缓存(singletonFactories):存放 Bean 的工厂对象,用于生成提前曝光的代理对象。

核心流程(以 A ↔ B 为例):

  1. 创建 A Bean:实例化 A → 将 A 的工厂放入三级缓存 → 填充属性(需要 B);
  2. 创建 B Bean:实例化 B → 将 B 的工厂放入三级缓存 → 填充属性(需要 A);
  3. B 从三级缓存获取 A 的提前曝光对象 → 填充到 B 中 → B 初始化完成,放入一级缓存;
  4. A 获取到初始化完成的 B → 填充到 A 中 → A 初始化完成,放入一级缓存。

循环依赖的解决方案(分场景)

场景 1:Spring 能自动解决的情况(单例 + setter / 字段注入)

无需手动处理,Spring 三级缓存会自动解决,这也是若依框架中最常见的情况。

场景 2:Spring 无法自动解决的情况(需要手动处理)

方案 1:使用 @Lazy 延迟加载(推荐) 通过延迟加载打破循环依赖的闭环,在注入时不立即创建依赖 Bean,而是创建代理对象,直到首次使用时才初始化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Component
public class A {
    // 延迟加载 B,首次调用 B 的方法时才初始化 B
    @Autowired
    @Lazy
    private B b;
}

@Component
public class B {
    @Autowired
    private A a;
}

方案 2:改用构造器注入 + @Autowired (required = false)

构造器注入默认不支持循环依赖,但可以通过设置非必须注入来规避:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Component
public class A {
    private B b;

    // 设置 required = false,允许 B 为 null,后续手动赋值
    @Autowired(required = false)
    public A(B b) {
        this.b = b;
    }

    // 手动注入 B(可通过 @PostConstruct 初始化时赋值)
    @Autowired
    public void setB(B b) {
        this.b = b;
    }
}

方案 3:通过 ApplicationContext 获取 Bean(兜底方案)

放弃自动注入,手动从容器中获取依赖 Bean,打破直接依赖:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Component
public class A implements ApplicationContextAware {
    private B b;
    private ApplicationContext context;

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

    // 首次使用时获取 B
    public B getB() {
        if (b == null) {
            b = context.getBean(B.class);
        }
        return b;
    }
}

方案 4:重构代码(根本解决方案)

循环依赖往往是代码设计不合理的体现,比如:

提取公共逻辑到新的服务类(如把 A 和 B 都依赖的逻辑抽成 C 服务,A 和 B 都依赖 C,而非互相依赖);

若依框架中,可将 Service 之间的循环依赖拆分为「基础服务 + 业务服务」,基础服务不依赖其他业务服务。

方案 5:设置 Bean 为原型(prototype)

原型 Bean 每次获取都会新建,不会存入三级缓存,因此不会有循环依赖问题,但会增加内存开销,仅适用于非单例场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Component
@Scope("prototype")
public class A {
    @Autowired
    private B b;
}

@Component
@Scope("prototype")
public class B {
    @Autowired
    private A a;
}

五、若依框架中处理循环依赖的实战示例 若依框架中最常见的是 Service 层循环依赖,比如 SysUserService 依赖 SysRoleService,SysRoleService 又依赖 SysUserService,解决方案如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 若依框架中的 SysUserService
@Service
public class SysUserServiceImpl implements SysUserService {
    // 延迟加载 SysRoleService,打破循环依赖
    @Autowired
    @Lazy
    private SysRoleService sysRoleService;

    // 业务方法
    @Override
    public SysUser getUserById(Long userId) {
        // 首次调用 sysRoleService 时才会初始化
        SysRole role = sysRoleService.getRoleById(1L);
        // ...
    }
}

// SysRoleService
@Service
public class SysRoleServiceImpl implements SysRoleService {
    @Autowired
    private SysUserService sysUserService;

    // ...
}

总结

核心定义:Spring 循环依赖是 Bean 之间互相引用形成闭环,Spring 仅自动解决「单例 + setter / 字段注入」的循环依赖;

常见场景:业务层互相依赖、跨模块依赖,若依框架中 Service 层循环依赖最典型;

解决方案:优先用 @Lazy 延迟加载,其次重构代码优化设计,兜底方案是手动从 ApplicationContext 获取 Bean。