各位老铁好,我是那个在 Spring 坑里摸爬滚打了七年的老油条。还记得刚接手祖传项目时,启动服务器突然报BeanCurrentlyInCreationException,百度半小时才知道这玩意儿叫 “循环依赖”。当时老员工丢来一句:“Spring 默认能解决 setter 循环依赖,但构造器的救不了”,听得我云里雾里。今天就把这七年踩过的坑、啃过的源码、装过的 X 全抖出来,咱用人话聊技术,顺便穿插点当年被 PM 催上线的辛酸史。
假设我们有三个类:A依赖B,B依赖C,C又依赖A,这就是三级循环依赖。不过日常最多遇到的是二级循环,比如UserService依赖UserDao,UserDao又依赖UserService。Spring 里循环依赖分三种类型,就像三种借橡皮的方式:
我第一次遇到的是 setter 循环依赖,当时在两个工具类里互相注入,启动时没报错,跑了半小时突然空指针 —— 原来是其中一个对象还没完全初始化就被用了,吓得我连夜买了杯奶茶压惊。
Spring 有三个 “快递柜”(缓存)来存对象的不同状态,存在DefaultSingletonBeanRegistry类里:
流程大概是这样的(以A依赖B,B依赖A为例):
这就像 A 和 B 互相借工具:先给对方一个 “临时工具”(半成品对象),等对方完全准备好了,再把 “正式工具” 换上。Spring 通过三级缓存,让对象在没完全初始化时就能被引用,避免了死锁。
因为构造器注入必须在对象创建时就拿到完整的依赖,而 Spring 无法在构造器阶段提供一个还没创建好的对象。就像你去奶茶店点单,店员说 “先付钱才能做奶茶”,你说 “等奶茶做好我才有钱”,两边僵住了。这时候只能改代码,比如用 setter 注入,或者加@Lazy懒加载(后面会讲)。
假设你遇到这样的代码:
// UserService.java
@Service
public class UserService {
private final UserDao userDao; // 构造器注入,致命!
@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}
}
// UserDao.java
@Service
public class UserDao {
private final UserService userService; // 构造器注入,循环了!
@Autowired
public UserDao(UserService userService) {
this.userService = userService;
}
}
启动直接报错Requested bean is currently in creation,这时候有三种解法:
@Service
public class UserService {
private UserDao userDao;
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
就像把 “必须当场给工具” 改成 “等会儿给也行”,Spring 就能介入处理。
@Autowired
@Lazy
public UserService(UserDao userDao) {
this.userDao = userDao;
}
但小心!如果过早调用代理对象的方法,还是会出问题,我曾因为在初始化方法里调了代理对象,导致半夜被叫起来改 bug。
如果你的类有 AOP 代理(比如加了@Transactional),三级缓存会生成代理对象。假设A依赖B的代理,B依赖A的代理,Spring 会在二级缓存里存代理对象,而不是原始对象。我曾遇到过这种情况:调用对象的方法时,发现是代理类的方法,而不是原始类的,debug 了两小时才发现是循环依赖导致的代理提前生成。这时候记住:三级缓存主要是为了处理 AOP 代理,正常情况下不用关心,除非你在玩自定义 BeanPostProcessor。
刚学 Spring 时,我迷信 “构造器注入更安全”,结果在两个 Service 里互相用构造器注入,启动直接 GG。记住:构造器注入适合依赖必须存在的场景,而 setter 注入适合可选依赖或可能循环的场景。就像买电脑,电源是构造器注入(必须有),键盘是 setter 注入(可有可无)。
有一次我在@PostConstruct方法里调用了依赖对象的方法,结果因为对象还是半成品,导致数据没加载完就被用了。记住:循环依赖中对象是分阶段初始化的,先保证能创建出来,再考虑初始化逻辑,别在半成品上动太多手脚。
搞懂循环依赖后,我现在看代码就像老中医把脉:看到两个类互相@Autowired,先判断是构造器还是 setter 注入,再想 Spring 能不能处理。总结几个核心点:
最后送大家一句打油诗:循环依赖别慌张,先看注入啥模样,构造器错 Setter 扛,三级缓存来帮忙,设计问题早解决,代码整洁才是王!
如果你在项目里遇到诡异的 Bean 创建问题,不妨想想是不是循环依赖在搞鬼。有啥问题欢迎留言,咱们一起聊聊那些年被 Spring 坑过的日子~