Spring 如何解决循环依赖?

1,324 阅读3分钟

这是我参与8月更文挑战的第31天,活动详情查看:8月更文挑战

前言

经典的面试题, 如何解决循环依赖?前面介绍了Spring IOC的生命周期,Bean容器的加载过程,那么我们从循环依赖的概念着手一步一步剖析Spring解决循环依赖的原理。

Spring循环依赖.png

1. 循环依赖场景

循环依赖, 顾名思义, 其实就是Bean对象之间引用存在循环.

1.1 单例Bean的属性注入

@Service
public class OrderService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private PayService payService;

    public void saveOrder() {
        // ...
        payService.pay(...)
    }
}

@Service
public class PayService {

    @Resource
    private OrderService orderService;

    public void pay(Long orderNo) {
        // 查询订单信息
        orderService.query(...)
    }
}

这是一个非常常见的循环依赖的例子, OrderService 和 PayService相互依赖运行正常, 具体原理下面分析.

1.2 多实例Bean的属性注入

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class ScopePrototypeService1 {

    @Resource
    private ScopePrototypeService2 service2;

    public void doSomething() {

    }
}

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class ScopePrototypeService2 {

    @Resource
    private ScopePrototypeService1 service1;

    public void doSomething() {
    }
}

直接写单元测试运行报错, 存在循环依赖. 但是如果没有单例引用这个多实例Bean的话,容器是可以启动成功的, 因为我们知道容器的启动过程是多实例Bean不能去初始化.

image.png

注意:这种循环依赖问题是无法解决的,因为它没有用缓存,每次都会生成一个新对象。

1.3 构造器注入

@Service
public class ConstructInjectService1 {

    public ConstructInjectService1(ConstructInjectService2 service2) {

    }

    public void execute() {

    }
}


@Service
public class ConstructInjectService2 {

    public ConstructInjectService2(ConstructInjectService1 service1) {

    }

    public void execute() {

    }
}

测试代码

@SpringBootTest
public class ConstructInjectTest {

    @Resource
    private ConstructInjectService1 service1;

    @Test
    public void test() {
        
    }
}

结果:

image.png

1.4 单例代理对象的属性注入

这个错误平时开发中可能比较常见, 比如@Async注解的场景,会通过AOP自动生成代理对象。平时我们开发的时候不注意的话可能会造成应用启动不起来.

先启用 @EnableAsync

image.png

@Service
public class PayService {

    @Resource
    private OrderService orderService;

    @Async
    public void pay(Long orderNo) {

    }
}

@Service
public class OrderService {

    @Resource
    private PayService payService;

    public void saveOrder() {

        payService.pay(1L);
    }
}

单元测试

@SpringBootTest
public class AsyncServiceTest {

    @Resource
    private OrderService orderService;

    @Test
    public void test() {

    }
}

报错信息:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'orderService': Bean with name 'orderService' has been injected into other beans [payService] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.

1.5 @DependsOn 注解循环依赖

还有一种有些特殊的场景,比如我们需要在实例化Bean A之前,先实例化Bean B,这个时候就可以使用@DependsOn注解。

2. 循环依赖解决方案

2.1 生成代理对象产生的循环依赖解决方案

    1. 使用@Lazy注解,延迟加载
    1. 使用@DependsOn注解,指定加载先后关系
    1. 修改文件名称,改变循环依赖类的加载顺序

2.2 使用@DependsOn产生的循环依赖 的解决方案

只能修改代码解决

2.3 多例循环依赖 的解决方案

看具体场景,是否可以改为单例Bean解决

2.4 构造器循环依赖的解决方案

lazy 或者改为属性注入的方式

3. Spring单例如何解决循环依赖的?

三级缓存:

  • singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的bean实例
  • earlySingletonObjects 二级缓存,用于保存实例化完成的bean实例
  • singletonFactories 三级缓存,用于保存bean创建工厂,以便于后面扩展有机会创建代理对象。

最后

以上就是Spring循环依赖的一些知识点, 主要从问题的场景分析, 然后产生循环依赖的解决方案, 避免大家在开发中踩坑