Administrator
发布于 2019-02-23 / 1133 阅读
31

Spring Bean 的生命周期与循环依赖解决

同事的循环依赖启动报错,我的没报错

2 月底做权限模块重构,同事写的代码一启动就挂:

org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'roleService': Requested bean is currently in creation:
Is there an unresolvable circular reference?

我看了一眼,他写的是构造器注入。而我在订单模块里也有一处循环依赖(OrderServiceStockService 互相引用),用的是 @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 的创建过程是这样交错的:

  1. getBean(A):A 不在缓存里,开始创建,标记 A 为"创建中"
  2. createBeanInstance(A):调用 A 的无参构造器,A 对象出生了(bService 还是 null)
  3. addSingletonFactory(A):三级缓存里放上 A 的工厂
  4. populateBean(A):发现需要注入 B → getBean(B)
  5. getBean(B):B 不在缓存,开始创建,createBeanInstance(B)addSingletonFactory(B)
  6. populateBean(B):发现需要注入 A → getBean(A)
  7. getSingleton(A):一级没有,但 A 处于"创建中",从三级缓存拿到工厂,产出 A 的半成品,放进二级缓存
  8. B 拿到 A 的引用,完成属性填充和初始化,进一级缓存
  9. 回到第 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() 执行

有两个点值得单独记:

  • @PostConstructInitializingBean 早,因为它是由 CommonAnnotationBeanPostProcessorpostProcessBeforeInitialization 阶段调用的,属于 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 的生命周期与循环依赖解决》这块我前前后后踩了不止一次。今天先写这些,后面想到新的再补。

参考