前言
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家: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工厂,用于生成提前暴露的Bean | Bean实例化后、属性填充前 |
- 构造函数注入无法解决循环依赖: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. 工具与最佳实践
循环依赖检测工具
-
Spring Actuator:
启用beans
端点,查看Bean依赖关系:management: endpoints: web: exposure: include: beans
访问:
http://localhost:8080/actuator/beans
-
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?
- 修复步骤:
- 提取公共验证逻辑到独立模块(ValidationService)
- 将部分方法改为Setter注入
- 添加单元测试验证启动流程
结果:启动时间减少30%,模块职责更清晰。
总结
- 循环依赖是设计警钟:重构优于技术规避
- 作用域需精确控制:单例与原型混用要慎防状态污染
- 注入方式影响扩展性:构造函数注入促进不可变性与清晰依赖
- 工具辅助事半功倍:利用Spring生态工具快速定位问题
下期预告:《技术债累积警告:复制粘贴代码的连锁反应》——从代码重复率到维护成本,揭秘如何用设计模式和工具重构腐化代码。
联系作者
职场经验分享,Java面试,简历修改,求职辅导尽在科技泡泡
思维导图面试视频合集