JAVA Bean 循环依赖问题如何解决?深入理解 Spring 依赖注入机制
大家好,今天我们来聊聊Java Bean的循环依赖问题,以及Spring如何巧妙地解决这类问题。循环依赖是依赖注入中一个常见且复杂的问题,理解其本质和解决方案对于构建健壮的Spring应用至关重要。
1. 什么是循环依赖?
简单来说,循环依赖发生在两个或多个Bean之间,它们相互依赖,形成一个闭环。例如,Bean A依赖Bean B,而Bean B又依赖Bean A。
用代码示例来说明:
// Bean A
@Component
public class BeanA {
    private BeanB beanB;
    @Autowired
    public BeanA(BeanB beanB) {
        this.beanB = beanB;
    }
    public void doSomething() {
        beanB.doSomethingElse();
    }
}
// Bean B
@Component
public class BeanB {
    private BeanA beanA;
    @Autowired
    public BeanB(BeanA beanA) {
        this.beanA = beanA;
    }
    public void doSomethingElse() {
        beanA.doSomething();
    }
}
在这个例子中,BeanA 依赖 BeanB,而 BeanB 又依赖 BeanA,形成了一个典型的循环依赖。如果Spring容器直接尝试创建这两个Bean,就会陷入无限循环,最终抛出异常。
2. 循环依赖的类型
循环依赖可以分为两种主要类型:
- 构造器注入的循环依赖: 就像上面的例子一样,Bean A 和 Bean B 都通过构造器注入彼此的依赖。
 - Setter注入的循环依赖: Bean A 和 Bean B 通过 Setter 方法注入彼此的依赖。
 
// Setter 注入的循环依赖示例
@Component
public class BeanC {
    private BeanD beanD;
    @Autowired
    public void setBeanD(BeanD beanD) {
        this.beanD = beanD;
    }
    public void doSomething() {
        beanD.doSomethingElse();
    }
}
@Component
public class BeanD {
    private BeanC beanC;
    @Autowired
    public void setBeanC(BeanC beanC) {
        this.beanC = beanC;
    }
    public void doSomethingElse() {
        beanC.doSomething();
    }
}
3. Spring如何解决循环依赖?
Spring 解决循环依赖的关键在于使用三级缓存。这三级缓存分别是:
- 一级缓存 (Singleton Bean Pool): 存放完全初始化好的 Bean 实例。
 - 二级缓存 (Early Singleton Objects): 存放提前暴露的 Bean 实例,这些实例已经完成了创建,但可能尚未完成属性注入和初始化。
 - 三级缓存 (Singleton Factories): 存放 Bean 工厂,用于创建 Bean 实例。这个工厂通常是一个 Lambda 表达式或者一个实现了 
ObjectFactory接口的类,它持有创建 Bean 实例的逻辑。 
接下来我们来详细解释Spring处理循环依赖的流程,并结合代码来说明:
处理流程(以构造器注入的循环依赖为例):
- 
创建 BeanA: Spring 容器首先尝试创建
BeanA的实例。由于BeanA依赖BeanB,容器会暂停BeanA的创建,转而尝试创建BeanB。 - 
创建 BeanB: Spring 容器尝试创建
BeanB的实例。由于BeanB依赖BeanA,容器发现已经有一个正在创建的BeanA(但尚未完成)。 - 
暴露半成品 BeanA: 为了解决循环依赖,Spring 会将正在创建的
BeanA的ObjectFactory放入三级缓存中。然后,Spring 会将BeanA的半成品(未完全初始化)放入二级缓存中。注意,此时BeanA还没有完成属性注入,因此它是一个半成品。 - 
注入 BeanA 到 BeanB: Spring 容器现在可以从二级缓存中获取
BeanA的半成品,并将其注入到BeanB中。BeanB完成创建。 - 
注入 BeanB 到 BeanA: Spring 容器回到
BeanA的创建过程,现在可以注入已经创建好的BeanB到BeanA中。 - 
完成 BeanA 的初始化:
BeanA完成所有属性注入和初始化。 - 
将 BeanA 和 BeanB 移动到一级缓存:
BeanA和BeanB现在都完全初始化好了,它们会被移动到一级缓存中,供后续使用。 
代码示例 (简化版,模拟 Spring 容器的行为):
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
public class SimpleSpringContainer {
    private final Map<String, Object> singletonObjects = new HashMap<>(); // 一级缓存
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(); // 二级缓存
    private final Map<String, Supplier<Object>> singletonFactories = new HashMap<>(); // 三级缓存
    public <T> T getBean(String beanName, Supplier<Object> factory) {
        // 1. 先从一级缓存中查找
        Object singletonObject = singletonObjects.get(beanName);
        if (singletonObject != null) {
            return (T) singletonObject;
        }
        // 2. 再从二级缓存中查找
        singletonObject = earlySingletonObjects.get(beanName);
        if (singletonObject != null) {
            return (T) singletonObject;
        }
        // 3. 如果都没有,则创建 Bean
        Supplier<Object> objectFactory = singletonFactories.computeIfAbsent(beanName, k -> factory);
        singletonObject = objectFactory.get();
        if (singletonObject != null) {
            earlySingletonObjects.put(beanName, singletonObject);
            singletonFactories.remove(beanName); //从三级缓存移除
        }
        return (T) singletonObject;
    }
    public void registerSingleton(String beanName, Object singletonObject) {
        singletonObjects.put(beanName, singletonObject);
        earlySingletonObjects.remove(beanName); // 从二级缓存移除
    }
    public static void main(String[] args) {
        SimpleSpringContainer container = new SimpleSpringContainer();
        // 模拟创建 BeanA 和 BeanB 的过程
        BeanB beanB = (BeanB) container.getBean("beanB", () -> new BeanB((BeanA) container.getBean("beanA", null)));
        BeanA beanA = (BeanA) container.getBean("beanA", () -> new BeanA(beanB));
        container.registerSingleton("beanA", beanA);
        container.registerSingleton("beanB", beanB);
        System.out.println("BeanA: " + beanA);
        System.out.println("BeanB: " + beanB);
    }
    static class BeanA {
        private BeanB beanB;
        public BeanA(BeanB beanB) {
            this.beanB = beanB;
        }
        public void doSomething() {
            System.out.println("BeanA is doing something, using BeanB: " + beanB);
        }
    }
    static class BeanB {
        private BeanA beanA;
        public BeanB(BeanA beanA) {
            this.beanA = beanA;
        }
        public void doSomethingElse() {
            System.out.println("BeanB is doing something else, using BeanA: " + beanA);
        }
    }
}
4. Spring对不同类型循环依赖的处理
- 
构造器注入的循环依赖: Spring 默认情况下 无法解决 构造器注入的循环依赖。因为在创建 Bean 实例时,构造器必须首先被调用,而构造器参数的依赖必须在调用构造器之前准备好。如果存在循环依赖,这个过程就无法启动。 Spring 会抛出
BeanCurrentlyInCreationException异常。- 如何解决构造器注入的循环依赖:  避免使用构造器注入的循环依赖是最佳实践。  如果必须使用,可以考虑使用 
@Lazy注解。@Lazy会延迟 Bean 的初始化,直到真正需要使用它时才创建。 这可以打破循环依赖的僵局。 但需要注意,这会引入运行时性能开销。 
// 使用 @Lazy 解决构造器注入的循环依赖 @Component public class BeanE { private final BeanF beanF; public BeanE(@Lazy BeanF beanF) { this.beanF = beanF; } public void doSomething() { beanF.doSomethingElse(); } } @Component public class BeanF { private final BeanE beanE; public BeanF(@Lazy BeanE beanE) { this.beanE = beanE; } public void doSomethingElse() { beanE.doSomething(); } } - 如何解决构造器注入的循环依赖:  避免使用构造器注入的循环依赖是最佳实践。  如果必须使用,可以考虑使用 
 - 
Setter注入的循环依赖: Spring 可以解决 Setter 注入的循环依赖。因为 Spring 可以先创建 Bean 的实例(通过默认构造器),然后再通过 Setter 方法注入依赖。 这样,Spring 就可以利用三级缓存来解决循环依赖。
 - 
Field 注入的循环依赖: Field 注入本质上也是 Setter 注入的一种形式,Spring 也可以解决 Field 注入的循环依赖。
 
5. 为什么要避免循环依赖?
虽然 Spring 可以解决 Setter 和 Field 注入的循环依赖,但最佳实践是尽量避免循环依赖。原因如下:
- 代码可读性降低: 循环依赖使得代码的依赖关系变得复杂,难以理解和维护。
 - 测试困难: 循环依赖使得单元测试变得更加困难,因为你需要模拟整个依赖链。
 - 潜在的运行时问题: 即使 Spring 可以解决循环依赖,但仍然可能存在运行时问题,例如在 Bean 的初始化过程中出现意外情况。
 - 设计不良的信号: 循环依赖往往是代码设计不良的信号,表明类的职责划分不清晰。
 
6. 如何避免循环依赖?
- 重新思考设计: 仔细审查你的代码设计,尝试找到打破循环依赖的方法。这可能需要重新划分类的职责,或者引入新的类来解耦。
 - 使用接口: 使用接口可以降低类之间的耦合度,从而减少循环依赖的可能性。
 - 组合优于继承: 尽量使用组合而不是继承,因为继承会增加类之间的耦合度。
 - 事件驱动架构: 使用事件驱动架构可以解耦组件之间的依赖关系,从而避免循环依赖。
 
7. 使用@Lazy注解的注意事项
虽然 @Lazy 注解可以解决构造器注入的循环依赖,但需要注意以下几点:
- 性能影响: 
@Lazy会延迟 Bean 的初始化,直到真正需要使用它时才创建。 这会带来运行时性能开销,因为每次使用 Bean 时都需要进行初始化。 - 异常处理: 如果延迟初始化的 Bean 在使用时发生异常,可能会导致难以调试的问题。
 - 谨慎使用: 
@Lazy应该谨慎使用,只在确实需要解决循环依赖的情况下才使用。 
8. 循环依赖检测
Spring Boot Actuator 提供了循环依赖检测的功能,可以帮助你发现应用中的循环依赖。  你可以通过添加 spring-boot-starter-actuator 依赖,并启用 health 端点来使用这个功能。  Actuator 会在应用启动时检测循环依赖,并在 health 端点中报告。
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
9. Spring 依赖注入的其他机制
除了构造器注入、Setter 注入和 Field 注入,Spring 还提供了其他依赖注入的机制:
- 
@Resource 注解:
@Resource注解是 JSR-250 规范的一部分,用于注入 Bean。 与@Autowired不同,@Resource默认按照 name 进行匹配,如果找不到匹配的 name,则按照 type 进行匹配。@Component public class MyService { @Resource(name = "myRepository") // 优先按照 name 匹配 private MyRepository myRepository; // ... } - 
@Value 注解:
@Value注解用于注入属性值,可以从 properties 文件、环境变量或者 SpEL 表达式中获取值。@Component public class MyConfig { @Value("${my.property}") private String myProperty; // ... } - 
FactoryBean 接口:
FactoryBean接口允许你自定义 Bean 的创建过程。 你可以通过实现FactoryBean接口来创建复杂的 Bean 实例,或者对 Bean 进行额外的配置。@Component("myFactoryBean") public class MyFactoryBean implements FactoryBean<MyBean> { @Override public MyBean getObject() throws Exception { // 自定义 Bean 的创建逻辑 MyBean myBean = new MyBean(); myBean.setName("Custom Bean"); return myBean; } @Override public Class<?> getObjectType() { return MyBean.class; } @Override public boolean isSingleton() { return true; // 返回 true 表示是单例 Bean } }@Component public class MyClient { @Autowired @Qualifier("myFactoryBean") // 使用 @Qualifier 指定 FactoryBean 的 name private MyBean myBean; // ... } - 
Aware 接口: Spring 提供了一系列 Aware 接口,允许 Bean 访问 Spring 容器的内部状态。 例如,
ApplicationContextAware接口允许 Bean 访问ApplicationContext实例,BeanNameAware接口允许 Bean 获取自己的 Bean 名称。@Component public class MyApplicationService implements ApplicationContextAware { private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } // ... } 
10. 总结:避免循环依赖,优化代码设计
理解 Spring 的依赖注入机制,特别是三级缓存的原理,对于解决循环依赖问题至关重要。 虽然 Spring 能够处理某些类型的循环依赖,但最佳实践是尽量避免循环依赖,并通过重新思考设计、使用接口、组合优于继承等方法来优化代码结构。 这样可以提高代码的可读性、可维护性和可测试性,最终构建出更加健壮的 Spring 应用。