本文不会详细讲解 Spring 循环依赖的基础问题。我相信能阅读到本文的,对 Spring 循环依赖已经有一定了解,但可能存在一些疑惑。本文就是尝试来解决这些疑惑的。
我们都知道 Spring 是利用了 三级缓存 来解决循环依赖的,其实现本质是通过提前暴露已经实例化但尚未初始化的 bean
来完成的。
但是呢,我们仍然会想,这里为什么要使用三级缓存?而且,我相信,不少人都曾手写过代码来解决循环依赖的问题,那时候,他们也只用了二级缓存,参考下图:
我们可以仔细跟踪序号,理清整个流程。所以,二级缓存是能够解决循环依赖,这也符合它的本质:“提前暴露对象”。这个流程图并没有描述接下来的流程,这里使用文字简单描述下:
- 对象A获取到已创建完成的对象B注入;
- 对象A完成字段注入以及初始化,并放入一级缓存;
- 对象A从二级缓存中移除;
既然二级缓存能够解决循环依赖了,那为什么要使用三级缓存呢?网上的说法是,那是因为 Spring 中存在替换注入对象的问题。通俗地来说就是:“一个半成品对象有可能在被对象b注入以后,被更改为其它的实例对象,那么对象b注入的就是一个过期的对象了”。
这种情况会导致对象b注入了一个并不存在于容器中的对象A(因为被更改后的对象注入了容器,替换掉了原来的对象)。所以,大多数人会认为三级缓存是为了解决这个问题的,让我们来看看真的是如此嘛?上代码:
@Component
public class ServiceA {
@Autowired
private ServiceB serviceB;
}
@Component
public class ServiceB {
@Autowired
private ServiceA serviceA;
}
@Component
public class ResetServiceABeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if(beanName.equals("serviceA")){
return new ServiceA();
}
return bean;
}
}
ServiceA
和 ServiceB
互相依赖,ResetServiceABeanPostProcessor
则是为了在ServiceB
注入了原来的ServiceA
后,将原来的 ServiceA
给替换掉。我们模拟了上述场景,但最后的运行结果却是得到了一个 BeanCurrentlyInCreationException
异常,异常在图中的 620 行抛出。
可以发现,似乎它并没有解决这个“注入了过期对象”的问题,可是它至少检测出了这个问题。所以,我个人认为,三级缓存并不是来解决这个问题,而是来在启动时检测这个问题的。
文章写到这里似乎也差不多了,但我还想纠正一点,网上有很多对于三级缓存的描述如下:
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
...
// 从上至下 分表代表这“三级缓存”
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); // 三级缓存
...
/** Names of beans that are currently in creation. */
// 这个缓存也十分重要:它表示bean创建过程中都会在里面呆着~
// 它在Bean开始创建时放值,创建完成时会将其移出~
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));
/** Names of beans that have already been created at least once. */
// 当这个Bean被创建完成后,会标记为这个 注意:这里是set集合 不会重复
// 至少被创建了一次的 都会放进这里~~~~
private final Set<String> alreadyCreated = Collections.newSetFromMap(new ConcurrentHashMap<>(256));
}
但源码中的顺序却不是如此(我使用的是 5.1.5 版本):
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
但我想三级缓存的分层并不是依赖上面的源码顺序来分为一二三的,而应该是根据从缓存中获取对象的顺序来分层的:
@Nullable
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;
}
最后,我结合上述的场景来分析下这三个“缓存”中元素的变化(省略了其它无关流程):
红色线条代表取出元素,虚线代表最终将不存在。
这里做下总结:
- 二级缓存也是能解决循环依赖的,使用三级缓存是为了帮助检测提前暴露的对象在后期被修改的这种情况;
- 通过
earlySingletonObjects
持有被暴露的对象,然后在最终返回对象时进行比对。如果不是同一个对象,则代表发生了对象后期被修改的情况。
我与风来
如果你觉得我的文章对你有所帮助的话,欢迎关注我的公众号。赞!
认认真真学习,做思想的产出者,而不是文字的搬运工。错误之处,还望指出!