Spring框架下的依赖注入坑:循环依赖与作用域混淆

本文介绍了Apollo自动驾驶开放平台如何助力汽车行业革新,包括云端仿真环境、DreamView的使用、离线数据包播放等关键步骤,旨在帮助开发者快速搭建自动驾驶系统。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

请添加图片描述

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家https://www.captainbed.cn/z
在这里插入图片描述
请添加图片描述


1. 错误场景复现

场景1:构造函数循环依赖

@Service
public class ServiceA {
    private final ServiceB serviceB;
    public ServiceA(ServiceB serviceB) { // 构造函数注入
        this.serviceB = serviceB;
    }
}

@Service
public class ServiceB {
    private final ServiceA serviceA;
    public ServiceB(ServiceA serviceA) { // 构造函数注入
        this.serviceA = serviceA;
    }
}

后果:应用启动时抛出BeanCurrentlyInCreationException,提示循环依赖。


场景2:单例Bean中的原型Bean失效

@Scope("prototype")
@Service
public class PrototypeBean {
    private int count = 0;
    public void add() { count++; }
    public int getCount() { return count; }
}

@Service
public class SingletonBean {
    @Autowired
    private PrototypeBean prototypeBean; // 单例中注入原型Bean

    public void increment() {
        prototypeBean.add();
        System.out.println(prototypeBean.getCount()); // 每次输出递增,而非期望的1
    }
}

问题:单例Bean只初始化一次,导致注入的原型Bean实例被复用,失去“原型”特性。


场景3:多级循环依赖(A→B→C→A)

@Service
public class ServiceA {
    @Autowired private ServiceB serviceB;
}

@Service
public class ServiceB {
    @Autowired private ServiceC serviceC;
}

@Service
public class ServiceC {
    @Autowired private ServiceA serviceA; // 三级循环依赖
}

现象:Spring可能正常启动,但某些情况下(如AOP代理)仍会触发循环依赖异常。


2. 原理解析

Spring解决循环依赖的三级缓存

缓存层级存储内容作用阶段
一级缓存(singletonObjects)完全初始化的单例Bean应用运行期
二级缓存(earlySingletonObjects)提前暴露的未完成初始化的Bean(仅对象引用)Bean创建中
三级缓存(singletonFactories)Bean工厂,用于生成提前暴露的BeanBean实例化后、属性填充前
  • 构造函数注入无法解决循环依赖:Bean实例化前需先获取依赖对象,而构造函数注入必须在实例化时完成依赖注入,导致死锁。
  • Setter注入/字段注入可解决:实例化后通过反射注入依赖。

原型Bean在单例中的失效原因

  • 单例Bean初始化仅一次:依赖的原型Bean在初始化时注入,后续方法调用复用同一实例。
  • 解决方案:通过ApplicationContext实时获取,或使用@Lookup注解。

3. 正确解决方案

方案1:打破循环依赖

方法1.1:代码重构(优先推荐)
// 提取公共逻辑到第三个类
@Service
public class CommonService { /* 公共方法 */ }

@Service
public class ServiceA {
    private final CommonService commonService;
    public ServiceA(CommonService commonService) { ... }
}

@Service
public class ServiceB {
    private final CommonService commonService;
    public ServiceB(CommonService commonService) { ... }
}
方法1.2:使用@Lazy延迟加载
@Service
public class ServiceA {
    public ServiceA(@Lazy ServiceB serviceB) { // 延迟注入代理对象
        this.serviceB = serviceB;
    }
}

注意@Lazy可能导致调试困难,需谨慎使用。


方案2:原型Bean的正确使用

方法2.1:ApplicationContext实时获取
@Service
public class SingletonBean {
    @Autowired
    private ApplicationContext context;

    public void increment() {
        PrototypeBean prototypeBean = context.getBean(PrototypeBean.class);
        prototypeBean.add();
        System.out.println(prototypeBean.getCount()); // 每次输出1
    }
}
方法2.2:使用ObjectFactory或Provider
@Service
public class SingletonBean {
    @Autowired
    private ObjectFactory<PrototypeBean> prototypeFactory;

    public void increment() {
        PrototypeBean prototypeBean = prototypeFactory.getObject();
        prototypeBean.add();
        System.out.println(prototypeBean.getCount());
    }
}
方法2.3:@Scope注解设置代理模式
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Service
public class PrototypeBean { ... }

// 单例Bean直接注入
@Service
public class SingletonBean {
    @Autowired 
    private PrototypeBean prototypeBean; // 每次调用时动态生成新实例
}

4. 工具与最佳实践

循环依赖检测工具

  1. Spring Actuator
    启用beans端点,查看Bean依赖关系:

    management:
      endpoints:
        web:
          exposure:
            include: beans
    

    访问:http://localhost:8080/actuator/beans

  2. IDE插件

    • IntelliJ的Spring插件可视化Bean依赖图
    • Eclipse的Spring Tools Suite

最佳实践

  • 优先使用构造函数注入:强制依赖明确,避免NullPointerException
  • 避免循环依赖:是设计问题而非技术问题,重构模块职责
  • 谨慎使用原型作用域:仅在需要状态隔离时使用(如用户会话)

5. Code Review检查清单

检查项正确做法
是否存在构造函数循环依赖?优先重构代码,其次使用@Lazy
原型Bean是否在单例中失效?检查是否使用ObjectFactory或@Scope代理
是否滥用字段注入?优先选择构造函数注入或Setter注入
是否误用@Primary注解?确保不会掩盖其他Bean的正确注入

6. 真实案例

某微服务项目因循环依赖导致启动失败:

  • 背景:订单服务(OrderService)与库存服务(StockService)相互构造函数注入。
  • 错误日志Requested bean is currently in creation: Is there an unresolvable circular reference?
  • 修复步骤
    1. 提取公共验证逻辑到独立模块(ValidationService)
    2. 将部分方法改为Setter注入
    3. 添加单元测试验证启动流程

结果:启动时间减少30%,模块职责更清晰。


总结

  • 循环依赖是设计警钟:重构优于技术规避
  • 作用域需精确控制:单例与原型混用要慎防状态污染
  • 注入方式影响扩展性:构造函数注入促进不可变性与清晰依赖
  • 工具辅助事半功倍:利用Spring生态工具快速定位问题

下期预告:《技术债累积警告:复制粘贴代码的连锁反应》——从代码重复率到维护成本,揭秘如何用设计模式和工具重构腐化代码。

联系作者

职场经验分享,Java面试,简历修改,求职辅导尽在科技泡泡
思维导图面试视频合集
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雪碧有白泡泡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值