没人比我更懂循环依赖!让阿里面试官都赞同不已的Spring解读!

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文

try {
// 注册销毁逻辑
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
catch (BeanDefinitionValidationException ex) {
throw new BeanCreationException(…);
}

return exposedObject;
}

可以看到,初始化一个bean是,创建bean实例之后,如果这个bean是单例bean&&允许循环依赖&&当前bean正在创建,那么将会调用addSingletonFactory加入三级缓存:

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
// 加入三级缓存
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}

也就是说我们伪代码中的这一段有了:

// 创建实例
Object beanInstance = createBeanInstance(beanName);
// 实例创建之后,放入缓存
// 因为已经创建实例了,这个时候这个实例的引用暴露出去已经没问题了
// 之后的属性注入等逻辑还是在这个实例上做的
cacheMap.put(beanName, beanInstance);

那么接下来,完全实例化完成的bean又是什么时候塞入我们的实例Map(一级缓存)singletonObjects的呢?

这个时候我们就要回到调用createBean方法的这一块的逻辑了:

if (mbd.isSingleton()) {
// 我们回到这个位置
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

可以看到,我们的createBean创建逻辑是通过一个lamdba语法传入getSingleton方法了,我们进入这个方法看一下:

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// 一级缓存拿不到
// 注意一下这个方法,这里会标记这个bean正在创建
beforeSingletonCreation(beanName);
boolean newSingleton = false;
try {
// 调用外部传入的lamdba,即createBean逻辑
// 获取到完全实例化好的bean
// 需要注意的是,这个时候这个bean的实例已经在二级缓存或者三级缓存中了
// 三级缓存:bean实例创建后放入的,如果没有循环依赖/并发获取这个bean,那会一直在三级缓存中
// 二级缓存:如果出现循环依赖,第二次进入getBean->getSingleton的时候,会从三级缓存升级到二级缓存
singletonObject = singletonFactory.getObject();
// 标记一下
newSingleton = true;
}
catch (IllegalStateException ex) {
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
throw ex;
}
}
catch (BeanCreationException ex) {
throw ex;
}
finally {
// 这里是从正在创建的列表移除,到这里这个bean要么已经完全初始化完成了
// 要么就是初始化失败,都需要移除的
afterSingletonCreation(beanName);
}
if (newSingleton) {
// 如果是新初始化了一个单例bean,加入一级缓存
addSingleton(beanName, singletonObject);
}
}
return singletonObject;
}
}

哈哈,加入实例Map(一级缓存)singletonObjects的逻辑明显就是在这个addSingleton中了:

protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
// 这个逻辑应该一点也不意外吧
// 放入一级缓存,从二、三级缓存删除,这里就用判断当前bean具体是在哪个缓存了
// 反正都要删的
this.singletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}

也就是说,我们伪代码的这一块在spring里面也有对应的体现,完美:

// 初始化方法调用
initializeBean(beanName, beanInstance);
// 从缓存移除,放入实例map
singleMap.put(beanName, beanInstance);
cacheMap.remove(beanName)

就这样,spring通过缓存设计解决了循环依赖的问题。

2.1.2. 三级缓存解决循环依赖流程图

什么,看完代码之后还是有点模糊?那么把我们的流程图再改一下,按照spring的流程来:

2.1.3. 三级缓存解决循环依赖伪代码

看完图还觉得不清晰的话,我们把所有spring中三级缓存相关的代码汇总到一起,用伪代码的方式,拍平成一个方法,大家应该感觉会更清晰了:

// 一级缓存
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);

protected Object getBean(final String beanName) {
// !以下为getSingleton逻辑!
// 先从一级缓存获取
Object single = singletonObjects.get(beanName);
if (single != null) {
return single;
}
// 再从二级缓存获取
single = earlySingletonObjects.get(beanName);
if (single != null) {
return single;
}
// 从三级缓存获取objectFactory
ObjectFactory<?> objectFactory = singletonFactories.get(beanName);
if (objectFactory != null) {
single = objectFactory.get();
// 升到二级缓存
earlySingletonObjects.put(beanName, single);
singletonFactories.remove(beanName);
return single;
}
// !以上为getSingleton逻辑!

// !以下为doCreateBean逻辑
// 缓存完全拿不到,需要创建
// 创建实例
Object beanInstance = createBeanInstance(beanName);
// 实例创建之后,放入三级缓存
singletonFactories.put(beanName, () -> return beanInstance);
// 依赖注入,会触发依赖的bean的getBean方法
populateBean(beanName, beanInstance);
// 初始化方法调用
initializeBean(beanName, beanInstance);

// 依赖注入完之后,如果二级缓存有值,说明出现了循环依赖
// 这个时候直接取二级缓存中的bean实例
Object earlySingletonReference = earlySingletonObjects.get(beanName);
if (earlySingletonReference != null) {
beanInstance = earlySingletonObject;
}
// !以上为doCreateBean逻辑

// 从二三缓存移除,放入一级缓存
singletonObjects.put(beanName, beanInstance);
earlySingletonObjects.remove(beanName);
singletonFactories.remove(beanName);

return beanInstance;
}

把所有逻辑放到一起之后会清晰很多,同学们只需要自行模拟一遍,再populateBean中再次调用getBean逻辑进行依赖注入,应该就能捋清楚了。

2.1.4. 标记当前bean正在创建

在我们刚刚看到的将bean实例封装成ObjectFactory并放入三级缓存的流程中,有一个判断是当前bean是正在创建,这个状态又是怎么判断的呢:

// 重点是这里了,如果是单例bean&&允许循环依赖&&当前bean正在创建
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
// 加入三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

我们看一下这个isSingletonCurrentlyInCreation的逻辑:

private final Set singletonsCurrentlyInCreation =
Collections.newSetFromMap(new ConcurrentHashMap<>(16));
public boolean isSingletonCurrentlyInCreation(String beanName) {
return this.singletonsCurrentlyInCreation.contains(beanName);
}

可以看到额,其实就是判断当前beanName是不是在这个singletonsCurrentlyInCreation容器中,那么这个容器中的值又是什么时候操作的呢?

希望同学们还记得getSingleton(beanName, singletonFactory)中有调用的beforeSingletonCreationafterSingletonCreation

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// 一级缓存拿不到
// 注意一下这个方法,这里会标记这个bean正在创建
beforeSingletonCreation(beanName);
boolean newSingleton = false;
try {
// 调用外部传入的lamdba,即createBean逻辑
singletonObject = singletonFactory.getObject();
// 标记一下
newSingleton = true;
}
catch (BeanCreationException ex) {
throw ex;
}
finally {
// 这里是从正在创建的列表移除,到这里这个bean要么已经完全初始化完成了
// 要么就是初始化失败,都需要移除的
afterSingletonCreation(beanName);
}
if (newSingleton) {
// 如果是新初始化了一个单例bean,加入一级缓存
addSingleton(beanName, singletonObject);
}
}
return singletonObject;
}
}

我们现在来看一下这两个方法的逻辑:

protected void beforeSingletonCreation(String beanName) {
// 加入singletonsCurrentlyInCreation,由于singletonsCurrentlyInCreation是一个set
// 如果加入失败的话,说明在创建两次这个bean
// 这个时候会抛出循环依赖异常
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
}

protected void afterSingletonCreation(String beanName) {
// 从singletonsCurrentlyInCreation中删除
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) {
throw new IllegalStateException(“Singleton '” + beanName + “’ isn’t currently in creation”);
}
}

可以看到,我们这两个方法主要就是对singletonsCurrentlyInCreation容器进行操作的,inCreationCheckExclusions这个容器可以不用管它,这名称一看就是一些白名单之类的配置。

这里需要主要的是beforeSingletonCreation中,如果singletonsCurrentlyInCreation.add(beanName)失败的话,是会抛出BeanCurrentlyInCreationException的,这代表spring遇到了无法解决的循环依赖问题,此时会抛出异常中断初始化流程,毕竟单例的bean不允许被创建两次。

2.2. 为什么要设计为三级结构?
2.2.1. 只做两级缓存会有什么问题?

其实到这里,我们已经清楚,三级缓存的设计已经成功的解决了循环依赖的问题。

可是按我们自己的设计思路,明明只需要两级缓存就可以解决,spring却使用了三级缓存,难道是为了炫技么?

这个时候,就需要我们再细致的看一下bean初始化过程了:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
// …
if (earlySingletonExposure) {
// 放入三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
// 这里这个引用被替换了
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
// …
return exposedObject;
}

仔细观察,initializeBean方法是可能返回一个新的对象,从而把createBeanInstance创建的bean实例替换掉的:

protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
// 调用aware接口
invokeAwareMethods(beanName, bean);
Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
// 埋点
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

try {
// 初始化方法
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(…);
}
if (mbd == null || !mbd.isSynthetic()) {
// 埋点
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

return wrappedBean;
}

可以看到,我们的postProcessBeforeInitializationpostProcessAfterInitialization的埋点方法都是有可能把我们的bean替换掉的。

那么结合整个流程来看,由于我们放入缓存之后,initializeBean方法中可能存在替换bean的情况,如果只有两级缓存的话:

这会导致B中注入的A实例与singletonObjects中保存的AA实例不一致,而之后其他的实例注入a时,却会拿到singletonObjects中的AA实例,这样肯定是不符合预期的。

2.2.2. 三级缓存是如何解决问题的

那么这个问题应该怎么解决呢?

这个时候我们就要回到添加三级缓存的地方看一下了。addSingletonFactory的第二个参数就是一个ObjectFactory,并且这个ObjectFactory最终将会放入三级缓存,现在我们再回头看调用addSingletonFactory的地方:

// 加入三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

熟悉lamdba语法的同学都知道,getEarlyBeanReference其实就是放入三级缓存中的ObjectFactorygetObject方法的逻辑了,那我们一起来看一下,这个方法是做了什么:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
// 调用了beanPostProcessor的一个埋点方法
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
// 返回的是埋点替换的bean
return exposedObject;
}

咦,这里也有个埋点,可以替换掉bean的引用。

原来为了解决initializeBean可能替换bean引用的问题,spring就设计了这个三级缓存,他在第三级里保存了一个ObjectFactory,其实具体就是getEarlyBeanReference的调用,其中提供了一个getEarlyBeanReference的埋点方法,通过这个埋点方法,它允许开发人员把需要替换的bean,提早替换出来。

比如说如果在initializeBean方法中希望把A换成AA(这个逻辑肯定是通过某个beanPostProcessor来做的),那么你这个beanPostProcessor可以同时提供getEarlyBeanReference方法,在出现循环依赖的时候,可以提前把A->AA这个逻辑做了,并且initializeBean方法不再做这个A->AA的逻辑,并且,当我们的循环依赖逻辑走完,A创建->注入B->触发B初始化->注入A->执行缓存逻辑获取AA实例并放入二级缓存->B初始化完成->回到A的初始化逻辑时,通过以下代码:

protected Object doCreateBean(…) {
populateBean(beanName, mbd, instanceWrapper);
Object exposedObject = initializeBean(beanName, exposedObject, mbd);

if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
// 如果二级缓存存在,直接使用二级缓存
exposedObject = earlySingletonReference;
}
}
}
return exposedObject;
}

这样就能保证当前bean中注入的AAsingletonObjects中的AA实例是同一个对象了。

将会把二级缓存中的AA直接返回,这是就能保证B中注入的AA实例与spring管理起来的最终的AA实例是同一个了。

整个流程梳理一下就是这样:

2.2.3. 三级缓存的实际应用

既然设计了这个三级缓存,那么肯定是有实际需求的,我们上面分析了一大堆,现在正好举一个例子看一下,为什么spring需要三级缓存。

我们都知道,SpringAOP功能,是通过生成动态代理类来实现的,而最后我们使用的也都是代理类实例而不是原始类实例。而AOP代理类的创建,就是在initializeBean方法的postProcessAfterInitialization埋点中,我们直接看一下getEarlyBeanReferencepostProcessAfterInitialization这两个埋点吧(具体类是AbstractAutoProxyCreator,之后讲AOP的时候会细讲):

public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {

private final Map<Object, Object> earlyProxyReferences = new ConcurrentHashMap<>(16);
// 如果出现循环依赖,getEarlyBeanReference会先被调用到
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
// 这个时候把当前类放入earlyProxyReferences
this.earlyProxyReferences.put(cacheKey, bean);
// 直接返回了一个代理实例
return wrapIfNecessary(bean, beanName, cacheKey);
}

public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
// 注意这个判断,如果出现了循环依赖,这个if块是进不去的
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
// 如果没有出现循环依赖,会在这里创建代理类
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
}

就这样,Spring巧妙的使用三级缓存来解决了这个不同实例的问题。当然,如果我们需要自己开发类似代理之类的可能改变bean引用的功能时,也需要遵循getEarlyBeanReference方法的埋点逻辑,学习AbstractAutoProxyCreator中的方式,才能让spring按照我们的预期来工作。

四、三级缓存无法解决的问题

1. 构造器循环依赖

刚刚讲了很多三级缓存的实现,以及它是怎么解决循环依赖的问题的。

但是,是不是使用了三级缓存,就能解决所有的循环依赖问题呢?

当然不是的,有一个特殊的循环依赖,由于java语言特性的原因,是永远无法解决的,那就是构造器循环依赖。

比如以下两个类:

public class A {
private final B b;
public A(final B b) {
this.b = b;
}
}
public class B {
private final A a;
public B(final A a) {
this.a = a;
}
}

抛开Spring来讲,同学们你们有办法让这两个类实例化成功么?

该不会有同学说,这有何难看我的:

// 你看,这样不行么~
final A a = new A(new B(a));

不好意思,这个真的不行,不信可以去试试。从语法上来讲,java的语言特性决定了不允许使用未初始化完成的变量。我们只能无限套娃:

// 这样明显就没有解决问题,是个无限套娃的死循环
final A a = new A(new B(new A(new B(new A(new B(…))))));

所以,连我们都无法解决的问题,就不应该强求spring来解决了吧~

@Service
public class A {
private final B b;
public A(final B b) {
this.b = b;
}
}
@Service
public class B {
private final A a;
public B(final A a) {
this.a = a;
}
}

启动之后,果然报错了:

Error creating bean with name ‘a’: Requested bean is currently in creation: Is there an unresolvable circular reference?

2. Spring真的对构造器循环依赖束手无策么?

难道,spring对于这种循环依赖真的束手无策了么?其实不是的,spring还有@Lazy这个大杀器…只需要我们对刚刚那两个类小小的改造一下:

@Service
public class A {
private final B b;
public A(final B b) {
this.b = b;
}
public void prt() {
System.out.println(“in a prt”);
}
}
@Service
public class B {
private final A a;
public B(@Lazy final A a) {
this.a = a;
}
public void prt() {
a.prt();
}
}
// 启动
@Test
public void test() {
applicationContext = new ClassPathXmlApplicationContext(“spring.xml”);
B bean = applicationContext.getBean(B.class);
bean.prt();
}

都说了成功了,运行结果同学们也能猜到了吧:

in a prt

(同学们也可以自己尝试一下~

3. @Lazy原理

这个时候我们必须要想一下,spring是怎么通过 @Lazy来绕过我们刚刚解决不了的无限套娃问题了。

因为这里涉及到之前没有细节的参数注入时候的参数解析问题,我这边就不带大家从入口处一步一步深入了,这边直接空降到目标代码DefaultListableBeanFactory#resolveDependency

public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
@Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
// 跳过…
// 这个地方是我们获取依赖的地方
// 尝试获取一个懒加载代理
Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
descriptor, requestingBeanName);
if (result == null) {
// 如果没获取到懒加载代理,就直接去获取bean实例了,这里最终会调用getBean
result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
}
return result;
}

我们直接看一下这个getLazyResolutionProxyIfNecessary,这个方法就是获取LazyProxy的地方了:

public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotationAutowireCandidateResolver {

@Override
@Nullable
public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) {
// 如果是懒加载的,就构建一个懒加载的代理
return (isLazy(descriptor) ? buildLazyResolutionProxy(descriptor, beanName) : null);
}
// 判断是否是懒加载的,主要就是判断@Lazy注解,简单看下就好了
protected boolean isLazy(DependencyDescriptor descriptor) {
for (Annotation ann : descriptor.getAnnotations()) {
Lazy lazy = AnnotationUtils.getAnnotation(ann, Lazy.class);
if (lazy != null && lazy.value()) {
return true;
}
}
MethodParameter methodParam = descriptor.getMethodParameter();
if (methodParam != null) {
Method method = methodParam.getMethod();
if (method == null || void.class == method.getReturnType()) {
Lazy lazy = AnnotationUtils.getAnnotation(methodParam.getAnnotatedElement(), Lazy.class);

最后

金三银四到了,送上一个小福利!

image.png

image.png

专题+大厂.jpg

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
r.getMethodParameter();
if (methodParam != null) {
Method method = methodParam.getMethod();
if (method == null || void.class == method.getReturnType()) {
Lazy lazy = AnnotationUtils.getAnnotation(methodParam.getAnnotatedElement(), Lazy.class);

最后

金三银四到了,送上一个小福利!

[外链图片转存中…(img-DjD3tkiX-1713343481531)]

[外链图片转存中…(img-owcTobPh-1713343481531)]

[外链图片转存中…(img-kHyzNocn-1713343481531)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-xrpBrmIs-1713343481532)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 17
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值