同事的循环依赖启动报错,我的没报错
2 月底做权限模块重构,同事写的代码一启动就挂:
org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'roleService': Requested bean is currently in creation:
Is there an unresolvable circular reference?
我看了一眼,他写的是构造器注入。而我在订单模块里也有一处循环依赖(OrderService 和 StockService 互相引用),用的是 @Autowired 字段注入,启动得好好的,上线三个月没出过问题。
同样是循环依赖,为什么待遇不一样?这个问题我一直没深究过,趁这次机会把 DefaultSingletonBeanRegistry 那段代码读了一遍。
先复现两种写法
最小复现代码,两个 Service 互相依赖:
// 写法一:字段注入,启动成功
@Service
public class AService {
@Autowired private BService bService;
}
@Service
public class BService {
@Autowired private AService aService;
}
// 写法二:构造器注入,启动失败
@Service
public class AService {
private final BService bService;
public AService(BService bService) { this.bService = bService; }
}
@Service
public class BService {
private final AService aService;
public BService(AService aService) { this.aService = aService; }
}
Spring Boot 2.1.3(对应 Spring 5.1.5)上跑,写法一正常启动,写法二抛 BeanCurrentlyInCreationException。
三级缓存长什么样
Spring 解决单例循环依赖靠的是三个 Map,都在 DefaultSingletonBeanRegistry 里:
// 一级缓存:完整的、已经走完整个生命周期的单例
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:提前暴露的半成品对象(已实例化,还没填属性)
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 三级缓存:能产出半成品的工厂
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
取单例的逻辑,注意 allowEarlyReference 这个开关:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory =
this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject(); // 工厂产出半成品
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName); // 升级到二级
}
}
}
}
return singletonObject;
}
三级缓存存在的意义是 AOP:如果 A 需要被代理,那么 B 拿到的应该是 A 的代理对象而不是原始对象。工厂里调的 getEarlyBeanReference 会去找 SmartInstantiationAwareBeanPostProcessor 提前生成代理。如果没有 AOP,工厂直接返回原始对象,三级缓存退化成二级也能工作。这也是为什么网上有人说"三级缓存其实是设计上的取舍,主要是为了延迟代理的创建"。
走一遍 A ↔ B 的完整流程
字段注入能成功,是因为 Spring 在实例化之后、属性填充之前,就把这个半成品对象的工厂塞进了三级缓存:
// AbstractAutowireCapableBeanFactory.doCreateBean(节选)
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args); // 1. 实例化
final Object bean = instanceWrapper.getWrappedInstance();
boolean earlySingletonExposure = (mbd.isSingleton()
&& this.allowCircularReferences
&& isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
addSingletonFactory(beanName,
() -> getEarlyBeanReference(beanName, mbd, bean)); // 2. 提前暴露工厂
}
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper); // 3. 填属性
exposedObject = initializeBean(beanName, exposedObject, mbd); // 4. 初始化
}
按这个顺序,A 和 B 的创建过程是这样交错的:
- getBean(A):A 不在缓存里,开始创建,标记 A 为"创建中"
createBeanInstance(A):调用 A 的无参构造器,A 对象出生了(bService 还是 null)addSingletonFactory(A):三级缓存里放上 A 的工厂populateBean(A):发现需要注入 B → getBean(B)- getBean(B):B 不在缓存,开始创建,
createBeanInstance(B)→addSingletonFactory(B) populateBean(B):发现需要注入 A → getBean(A)- getSingleton(A):一级没有,但 A 处于"创建中",从三级缓存拿到工厂,产出 A 的半成品,放进二级缓存
- B 拿到 A 的引用,完成属性填充和初始化,进一级缓存
- 回到第 4 步,A 拿到完整的 B,完成属性填充和初始化,进一级缓存
关键在于第 2 步和第 3 步之间:A 已经被 new 出来了,只是属性还没填。这个"半成品"足够满足 B 的引用需求,因为 B 只需要一个指向 A 的指针。
为什么构造器注入不行
把上面的流程套到构造器注入上,卡在第 1 步:
protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) {
// 构造器注入要从容器里拿参数
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(...);
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR
|| mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
return autowireConstructor(beanName, mbd, ctors, args); // 在这里就要解析 B
}
return instantiateBean(beanName, mbd);
}
构造器注入要求在实例化之前就拿到 B,而实例化之前 A 连对象都还没生成,根本没有东西可以放进三级缓存。于是:
- 实例化 A 需要 B → 去创建 B
- 实例化 B 需要 A → 去创建 A
- A 已经在"创建中"了,但三级缓存里没有 A 的工厂(还没走到
addSingletonFactory) - 返回 null,构造参数解析失败,抛
BeanCurrentlyInCreationException
还有几种情况三级缓存救不了
- prototype 作用域的循环依赖。三级缓存只对单例生效,prototype 每次都新建,
isSingletonCurrentlyInCreation判断过不去,直接报错。 - 构造器注入,如上所述。
- 关闭了
allowCircularReferences。这个开关默认是 true,可以用AbstractAutowireCapableBeanFactory#setAllowCircularReferences(false)关掉,关掉之后所有循环依赖一律报错。我们没关,怕老代码起不来。 - depends-on 形成的循环,这个连字段注入都不行。
生命周期回调到底什么顺序
查这个问题的过程中我把回调顺序也理了一遍,写了个测试 Bean 打印,实际输出:
1. LifecycleBean 构造器执行
2. setUserService 被调用(属性注入)
3. BeanNameAware.setBeanName:lifecycleBean
4. BeanFactoryAware.setBeanFactory
5. BeanPostProcessor.postProcessBeforeInitialization:lifecycleBean
6. @PostConstruct 标注的 init() 执行
7. InitializingBean.afterPropertiesSet 执行
8. init-method 指定的 doInit() 执行
9. BeanPostProcessor.postProcessAfterInitialization:lifecycleBean
10. 容器启动完成,业务方法被调用
11. @PreDestroy 标注的 destroy() 执行
12. DisposableBean.destroy 执行
13. destroy-method 指定的 doDestroy() 执行
有两个点值得单独记:
@PostConstruct比InitializingBean早,因为它是由CommonAnnotationBeanPostProcessor在postProcessBeforeInitialization阶段调用的,属于 BPP 的一部分。- AOP 代理对象是在第 9 步生成的,也就是初始化完成之后。这意味着初始化期间
this指向的是原始对象,如果在@PostConstruct里调用本类的其他方法,那些方法上的切面不会生效。我之前写过一篇 AOP 失效场景的笔记,其中"内部调用"那条的根因就在这里。
最后是怎么改的
同事那个权限模块的循环依赖,我们没有用三级缓存绕过,而是把耦合的那部分逻辑抽了出去:
// 原本:RoleService 和 PermissionService 互相持有
// 改成:都依赖一个无状态的 PermissionCache
@Service
public class RoleService {
private final PermissionCache permissionCache; // 单向依赖
}
@Service
public class PermissionCache {
// 只读 Redis,不反向依赖任何 Service
}
如果实在改不动,有两个权宜之计:
// 方案一:@Lazy,注入的是代理,真正用到时才去容器取
@Service
public class AService {
public AService(@Lazy BService bService) { this.bService = bService; }
}
// 方案二:用 ObjectProvider / ApplicationContext 延迟获取
@Service
public class AService {
@Autowired private ObjectProvider<BService> bServiceProvider;
public void doSomething() {
BService b = bServiceProvider.getObject(); // 用到时才解析
}
}
@Lazy 我们只在老代码改造期间用过一次,它治标不治本,而且会让依赖关系更难看懂。
先到这
《Spring Bean 的生命周期与循环依赖解决》这块我前前后后踩了不止一次。今天先写这些,后面想到新的再补。