Java框架中循环依赖的解决方案

Java容器中的循环依赖及其解决方案
本文讨论了Java容器(如IOC/DI容器)中的循环依赖问题,通过实例展示了循环依赖的场景以及如何通过构造函数参数检查和栈机制来检测并避免这种问题。
部署运行你感兴趣的模型镜像

微信公众号: 线程
在这里插入图片描述

在一些Java容器技术中,比如IOC容器和DI容器,通常需要处理一种叫作循环依赖的情况。

循环依赖出现的场景

循环依赖指的是两个或更多的bean彼此相互持有,最终形成一个环。

比如在下面这个例子中ComponentWithInjectConstructor是依赖于Dependency的, 同时DependencyDependedOnComponent依赖于component

public class ComponentWithInjectConstructor implements Component {
    private Dependency dependency;
    //表示注入
    @Inject
    public ComponentWithInjectConstructor(Dependency dependency) {
        this.dependency = dependency;
    }
}

public class DependencyDependedOnComponent implements Dependency {
    private Component component;

    @Inject
    public DependencyDependedOnComponent(Component component) {
        this.component = component;
    }
}

DIcontainer的对外接口是这样的:

context.bind(Component.class,  ComponentWithInjectConstructor.class);
context.bind(Dependency.class, DependencyDependedOnComponent.class);

context.get(Component.class);

在实现绑定bind时,我们使用了一个映射(map),以注入的接口作为键。接口的实例被封装在一个类中,这个类实现了Provider接口。

要获取已注入的实例,首先需要创建Provider类的实例,然后调用其get方法。

Map<Class<?>, Provider<?>> providers = new HashMap<>();

//bind注入
providers.put(type, new ConstructorInjectionProvider(type, injectConstructor));

//get提取
//根据注入的接口获得provider接口
Optional.ofNullable(providers.get(type)).map(provider -> provider.get());

//providers接口的get方法
//获取provider的实例, 然后调用get方法, 创建实例, 返回注入的实例
Object[] dependencies = stream(injectConstructor.getParameters())
    .map(p -> get(p.getType()))
    .toArray(Object[]::new);

在处理循环构造时,我们会遇到以下情况:
首先,根据Component查找类,找到了ComponentWithInjectConstructor。然后遍历其构造函数的参数,发现参数类型为Dependency。
接着,根据Dependency查找类,找到了DependencyDependedOnComponent。再次遍历其构造函数的参数,发现参数类型为Component,形成了一个循环依赖的环。最终程序会一种处在一种死循环中。

循环依赖的解决方案

如前所述,循环依赖就是在对象关系图中形成了一个环。我们需要采取措施来打破这个循环。

解决方法相对简单,在构造对象时,我们首先将当前正在构造的对象压入栈中。在下一次判断时,检查对象是否已经在构造中,如果是,则说明存在循环依赖,应该抛出异常。
若构造完成,则将对象从栈中弹出。这一机制能有效防止循环依赖的问题。

对应的代码逻辑如下, 这里我们直接使用一个全局条件变量, 也可以达到上面的效果。

if (constructing) throw new CyclicDependenciesFoundException(componentType);

try {
    //表示当前的provider正在构造
    constrcuting = true;
    Object[] dependencies = stream(injectConstructor.getParameters())
         .map(p -> get(p.getType()))
        .toArray(Object[]::new);

    return injectConstructor.newInstance(dependencies);
} finally {
    constructing = false;
}

因为不断的查找try中出现了死循环,finally中的东西永久不会执行, 并且当在一个依赖中查找到component的时候, 会发现constructing为true, 所以就会抛出异常。

您可能感兴趣的与本文相关的镜像

Dify

Dify

AI应用
Agent编排

Dify 是一款开源的大语言模型(LLM)应用开发平台,它结合了 后端即服务(Backend as a Service) 和LLMOps 的理念,让开发者能快速、高效地构建和部署生产级的生成式AI应用。 它提供了包含模型兼容支持、Prompt 编排界面、RAG 引擎、Agent 框架、工作流编排等核心技术栈,并且提供了易用的界面和API,让技术和非技术人员都能参与到AI应用的开发过程中

<think> 嗯,用户这次明确询问Java中解决循环依赖的解决方法,看来是个有实际开发经验的程序员,可能正在被依赖注入问题困扰。从ta引用的资料看,已经查阅过Spring循环依赖的相关文章,但需要系统化的解决方案梳理。 用户引用的4篇资料其实很有代表性:引用1提到循环依赖会导致启动失败和NullPointerException,这正是典型症状;引用2展示了自己实现IOC时遇到的循环依赖异常;引用3给出最简单的双向依赖代码示例;引用4则提供了工厂模式解法。这些恰好覆盖了问题的背景、现象和部分解决方案。 不过用户需要的是完整方法论。结合Spring官方文档和实战经验,应该从三个层面组织答案:设计层面(根本解法)、技术层面(Spring特性)和特殊方案(工厂模式等)。尤其要强调Spring三级缓存的实现原理,因为这是面试高频考点。 注意到用户上次对话是关于LaTeX格式要求,这次突然切到Java技术问题,可能是不同场景需求。回复时要严格遵循ta设定的格式规则:所有技术术语如@Lazy、setter注入都要用$...$包裹,代码示例必须用```code```块。 用户提供的引用3中那句"重构不是一时的事情"很有意思,说明ta需要的是能快速上手的临时方案。因此要把@Lazy注解这种见效快的方法放在前面,而把代码重构这类长期方案放在最后。另外引用4提到的工厂模式虽然不常用,但作为备选方案值得收录。 代码示例部分要特别注意:不能直接复制引用里的ClassA/ClassB,应该优化字段命名和注释。比如把classA改成orderService会更贴近实际业务场景,这样用户更容易理解。 </think> 在Java开发中,**循环依赖**是指两个或多个组件互相直接或间接依赖导致无法初始化的设计问题。以下是系统化的解决方案,结合Spring框架特性和设计模式: --- ### **一、Spring框架解决方案** #### 1. **使用三级缓存机制(默认支持)** Spring通过三级缓存自动解决单例Bean的循环依赖: - **一级缓存**:存放完整初始化的Bean - **二级缓存**:存放半成品Bean(已实例化但未注入属性) - **三级缓存**:存放Bean的工厂对象 **流程**: ```mermaid graph LR A[创建Bean A] --> B[实例化A, 放入三级缓存] B --> C[注入A的依赖B] C --> D[创建Bean B] D --> E[实例化B, 放入三级缓存] E --> F[注入B的依赖A] F --> G[从三级缓存获取A的半成品] G --> H[完成B的初始化] H --> I[完成A的初始化] ``` #### 2. **`@Lazy` 延迟加载** 在依赖属性上添加`@Lazy`,使依赖在首次使用时才初始化: ```java @Service public class ServiceA { @Autowired @Lazy private ServiceB serviceB; // 延迟加载 } ``` #### 3. **Setter/构造器注入替代字段注入** - **Setter注入**: ```java public class ServiceA { private ServiceB serviceB; @Autowired public void setServiceB(ServiceB serviceB) { this.serviceB = serviceB; // 先创建A再注入B } } ``` - **构造器注入**:需结合`@Lazy`(否则会报错): ```java public class ServiceA { private final ServiceB serviceB; @Autowired public ServiceA(@Lazy ServiceB serviceB) { this.serviceB = serviceB; } } ``` --- ### **二、设计层面的解决方案** #### 1. **接口抽象解耦** 将依赖抽取为接口,通过中间层隔离: ```java public interface IServiceB { void action(); } @Service public class ServiceBImpl implements IServiceB { ... } @Service public class ServiceA { @Autowired private IServiceB serviceB; // 依赖接口而非实现类 } ``` #### 2. **工厂模式** 手动控制对象创建顺序(参考引用[^4]): ```java public class ServiceFactory { private static ServiceA a; private static ServiceB b; public static ServiceA getServiceA() { if (a == null) { a = new ServiceA(getServiceB()); // 优先创建B } return a; } public static ServiceB getServiceB() { if (b == null) { b = new ServiceB(); } return b; } } ``` #### 3. **重构代码结构** - 合并相关逻辑到同一类中 - 提取公共功能到新组件 - 避免双向依赖(改为单向依赖) --- ### **三、其他应急方案** #### 1. **`ApplicationContext` 手动获取Bean** 在初始化完成后通过上下文获取依赖: ```java public class ServiceA implements ApplicationContextAware { private ApplicationContext context; @Override public void setApplicationContext(ApplicationContext ctx) { this.context = ctx; } public void execute() { ServiceB serviceB = context.getBean(ServiceB.class); // 使用时获取 } } ``` #### 2. **调整Bean初始化顺序** 使用`@DependsOn`强制指定顺序: ```java @Service @DependsOn("serviceB") // 确保B先初始化 public class ServiceA { ... } ``` --- ### **代码示例(Setter注入解决)** ```java @Service public class OrderService { private UserService userService; @Autowired public void setUserService(UserService userService) { this.userService = userService; // 安全注入 } } @Service public class UserService { private OrderService orderService; @Autowired public void setOrderService(OrderService orderService) { this.orderService = orderService; } } ``` > **关键点**: > - Spring对**单例Bean**的循环依赖有原生支持(通过三级缓存) > - **原型(prototype) Bean**的循环依赖仍需代码设计解决 > - 优先选择接口解耦和代码重构,其次考虑框架特性[^1][^3][^4] --- ### 相关问题 1. Spring的三级缓存具体如何实现循环依赖的解决? 2. 使用`@Lazy`注解在分布式环境中会有哪些潜在问题? 3. 如何通过字节码增强技术(如ASM)动态解决循环依赖? 4. 循环依赖在微服务架构下有何不同?如何跨服务解决? [^1]: Java后端开发中的依赖循环问题分析 [^2]: Spring模拟IOC时循环依赖的异常场景 [^3]: Springboot中循环依赖的紧急解决方案 [^4]: 工厂模式解决循环依赖的实践示例
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值