Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

有代理对象解决循环依赖这节内容有些不理解的内容 #51

Open
ccl125 opened this issue Apr 19, 2023 · 5 comments
Open

Comments

@ccl125
Copy link

ccl125 commented Apr 19, 2023

作者大大好,整个项目快看到尾声了,在这节内容上有些问题。
分支:circular-reference-with-proxy-bean

  1. 解决有代理对象的循环依赖问题,因为上一节中,singletonObjects(一级缓存)和earlySingletonObjects(二级缓存)没办法解决有代理对象循环依赖的问题,所以这节中引入了singletonFactory(三级缓存),在代码中的体现就是AbstractAutowireCapableBeanFactory#doCreateBean()方法,这个方法在75行的时候,通过bean复制一个finalBean对象,然后放入singletonFactories三级缓存中,接着在第104行通过getSingleton(beanName)这个方法就是会先从一级缓存找,然后接着二级缓存找,最后再三级缓存中找,对应到项目实际debug的过程中,实际上从三级缓存中去获取,就是调用了singletonFactory.getObject()该方法。也就是上面75行存入的三级对象的getObject方法,最终返回这个代理对象。这是我的理解。那这样的话问题就来了,就是在整个bean的生命周期,还有其他很多的操作步骤,比如BeanPostProcessor修改属性值,填充属性,以及初始化前后的处理,那么实际上对应的代码就是刚刚上面提到75行和104行中间的很多操作步骤。也就是说我们最终获取到的代理bean是根据一开始75行存入三级缓存中的finalBean。而在这个之后,代码中的很多步骤都是对原先bean去做的操作。那75-104行在bean生命周期内所做的操作岂不是对代理对象都不生效了?那代理对象的属性什么的设置是不是就都有问题了。
  2. 另一个问题还是在doCreateBean()方法内,就是获取到代理对象后,105行的代码addSingleton(beanName, exposedObject),这里紧接着将对象放入一级缓存中,并将二级三级缓存进行清空,结合104行的代码,整个过程看起来像是获取到代理对象后,先放入二级缓存中,然后将三级缓存中的清空,接着再放入一级缓存中,再将二级三级缓存清空。所以这里看起来好像放入二级缓存中什么也没操作,那不如直接放入一级缓存,因为放入二级缓存后什么都没操作就又取出来放入一级缓存中了,这里不是太理解。

这节内容debug过程中很多嵌套的,所以理解起来不是很容易,可能自己陷入了某个单独流程上,没办法从整体上去理解。再次感谢作者,学习到了很多东西

@DerekYRC
Copy link
Owner

@ccl125 特别开心你这么用心地仔细地看完项目!!!
1、75行和104行中间的很多操作步骤是针对原始bean的,比如属性设置,如果在代理对象上执行非代理方法,最终会执行到原始bean的对应方法。比如在单测类CircularReferenceWithProxyBeanTest中,bean A被代理,a设置属性的时候是针对原始bean a操作的,如果执行代理对象a的getB方法,最终会执行到原始bean a的getBean方法,因为该方法没有被代理,见下图中的逻辑。

image

2、我也有同样的疑惑,其实两级缓存就能解决有代理对象时的循环依赖问题,就像上一节中所说的,只使用一级缓存就能解决没有代理对象时的循环依赖问题(只需要调整一下顺序,当bean实例化后就放进singletonObjects单例缓存中,提前暴露引用,然后再设置属性)。

@ccl125
Copy link
Author

ccl125 commented May 5, 2023

感谢作者的解答,我最近又看了些资料,还是有些问题要请教下。
1、 只使用一级的缓存是否能解决没有代理对象的循环依赖?

没有代理对象的情况下,在bean实例化后就放入singletonObjects中,提前暴露。这样做可以解决循环依赖带来的闭环调用问题。但是这样能否解决没有代理对象的循环依赖问题,我感觉不可以解决,因为如果这样做的话,相当于没有半成品与成品对象的区分了,singletonObjects这一个缓存中可能既存在一个bean对象的完整态,也存在bean的半成品状态(未设置属性),那么如果恰好在某个bean处于半成品的时机,其他地方调用了getBean()方法想获取到完整的bean对象,获取到的bean对象却是一个未设置属性的bean,这就有问题了吧。这是我的理解。

2、 2个缓存,是否能解决循环依赖?

结合网上的资料。
第一种观点是说两级缓存不能解决循环依赖。

文章会首先说两级缓存可以解决普通的循环依赖,但是有AOP的情况,就无法解决,因为最终返回的对象与代理对象不一致(也就是本项目循环依赖两节内容所讲的问题)接着引入三级缓存,提前暴露singletonFactory解决有代理对象的循环依赖。

这个观点没有问题,Spring的源码就是这样,如果不使用三级缓存,只使用二级缓存就像本项目第一节循环依赖那样,显然是没办法解决的。

第二种观点是说两级缓存可以解决循环依赖(也是作者大大回复我的观点)。解决的办法就是提前将生成的代理对象放入缓存中,因为Spring解决循环依赖。三级缓存的设计实际上就是在1、实例化,2、设置属性、3初始化。第三步3初始化后通过BeanPostProcessor生成代理的。那么如果只用二级缓存的话,就是在实例化后不管有没有AOP都生成代理对象放入缓存中,对应代码其实就是在bean实例化后,直接调用getEarlyBeanReference()方法,生成代理对象后放入二级缓存,而不是采用三级缓存的方式再最后完成整个bean创建过程后再通过提前暴露的ObjectFactory去获取代理对象。采用这种方式,就是在步骤1,实例化后就生成代理对象,这样就可以用两级缓存解决循环依赖。接着再说为什么Spring采用三级缓存而不采用二级缓存的方式,实际上是Spring的一个设计理念所决定的,也就是说Spring用三级缓存这种方式去处理更加的“优雅”。
下面是我摘抄的比较有说服性的回答:
并不是说二级缓存如果存在aop的话就无法将代理对象注入的问题,本质应该说是初始spring是没有解决循环引用问题的,设计原则是 bean 实例化、属性设置、初始化之后 再 生成aop对象,但是为了解决循环依赖但又尽量不打破这个设计原则的情况下,使用了存储了函数式接口的第三级缓存; 如果使用二级缓存的话,可以将aop的代理工作提前到 提前暴露实例的阶段执行; 也就是说所有的bean在创建过程中就先生成代理对象再初始化和其他工作; 但是这样的话,就和spring的aop的设计原则相驳,aop的实现需要与bean的正常生命周期的创建分离; 这样只有使用第三级缓存封装一个函数式接口对象到缓存中, 发生循环依赖时,触发代理类的生成
其实总结就是Spring开发者不希望循环依赖的处理影响了原本创建bean的流程。就是初始化完后再通过后置处理去生成代理对象,而不是为了循环依赖把整个bean生命周期流程更改掉。

上面这两个观点一个说二级缓存不能解决,一个说可以解决,其实是不矛盾的,因为是从两个角度去看问题的。
我的疑问点其实在观点二这里。这种方式去解决循环依赖,假设我在实例化后,不管有没有AOP都先通过getEarlyBeanReference()方法提前去放入二级缓存中代理对象。那如果有不需要代理对象的情况怎么办?怎么去判断我要获取的对象到底是代理的对象还是实际的原对象呢。还是说我理解错了,没有这种即要代理又有其他地方需要真实对象的情况?因为我感觉如果就算提前放入二级缓存中代理对象,那如果不需要代理的地方是不是就没办法获取到真实对象了

总结:
其实就是两个问题:
1、不考虑AOP的情况下,一级缓存能否解决循环依赖?
2、二级缓存的设计(实例化后就将代理放入缓存中)到底能否解决循环依赖?如果可以,怎么区分缓存中的真实对象还是代理对象,getBean的时候从缓存中获取,可能有的地方是要代理对象,有的地方是要真实对象,怎么保证获取的就是我想要的呢?

@DerekYRC
Copy link
Owner

DerekYRC commented May 8, 2023

@ccl125 问:只使用一级的缓存是否能解决没有代理对象的循环依赖?我感觉不可以解决,因为如果这样做的话,相当于没有半成品与成品对象的区分了,singletonObjects这一个缓存中可能既存在一个bean对象的完整态,也存在bean的半成品状态(未设置属性),那么如果恰好在某个bean处于半成品的时机,其他地方调用了getBean()方法想获取到完整的bean对象,获取到的bean对象却是一个未设置属性的bean,这就有问题了吧。

答:可以的。A对象依赖B对象,a对象持有的是b对象的引用,虽然getBean(b)时获取的b没有设置属性,但最终b会设置属性,因此b最终会是成品。

问:二级缓存的设计(实例化后就将代理放入缓存中)到底能否解决循环依赖?如果可以,怎么区分缓存中的真实对象还是代理对象,getBean的时候从缓存中获取,可能有的地方是要代理对象,有的地方是要真实对象,怎么保证获取的就是我想要的呢?

答:二级缓存可以解决有代理对象时的循环依赖问题。放进缓存中的都是代理对象。

源码面前无秘密,可以把一级缓存去除,再跑单测debug有无代理对象时的循环依赖加深理解,去除一级缓存的代码如下:

package org.springframework.beans.factory.support;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.SingletonBeanRegistry;

/**
 * @author derekyi
 * @date 2020/11/22
 */
public class DefaultSingletonBeanRegistry implements SingletonBeanRegistry {

    //一级缓存
//	private Map<String, Object> singletonObjects = new HashMap<>();

    //二级缓存
    private Map<String, Object> earlySingletonObjects = new HashMap<>();

    //三级缓存
    private Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>();

    private final Map<String, DisposableBean> disposableBeans = new HashMap<>();

    @Override
    public Object getSingleton(String beanName) {
//		Object singletonObject = singletonObjects.get(beanName);
        Object singletonObject = null;
        if (singletonObject == null) {
            singletonObject = earlySingletonObjects.get(beanName);
            if (singletonObject == null) {
                ObjectFactory<?> singletonFactory = singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    //从三级缓存放进二级缓存
                    earlySingletonObjects.put(beanName, singletonObject);
                    singletonFactories.remove(beanName);
                }
            }
        }
        return singletonObject;
    }

    @Override
    public void addSingleton(String beanName, Object singletonObject) {
//		singletonObjects.put(beanName, singletonObject);
//		earlySingletonObjects.remove(beanName);
        earlySingletonObjects.put(beanName, singletonObject);
        singletonFactories.remove(beanName);
    }

    protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
        singletonFactories.put(beanName, singletonFactory);
    }

    public void registerDisposableBean(String beanName, DisposableBean bean) {
        disposableBeans.put(beanName, bean);
    }

    public void destroySingletons() {
        ArrayList<String> beanNames = new ArrayList<>(disposableBeans.keySet());
        for (String beanName : beanNames) {
            DisposableBean disposableBean = disposableBeans.remove(beanName);
            try {
                disposableBean.destroy();
            } catch (Exception e) {
                throw new BeansException("Destroy method on bean with name '" + beanName + "' threw an exception", e);
            }
        }
    }
}

@ccl125
Copy link
Author

ccl125 commented May 11, 2023

谢谢作者的解答,我已经理解了。
1、第一个问题我在纠结没有AOP的情况下只用一级缓存会造成没办法区分半成品和成品的bean。实际上框架加载是单线程的,也就是框架加载完成后bean就也创建好了,所以不会有半成品和成品混到一块的情况。
2、第二个问题是用二级缓存去解决循环依赖会存在代理对象和原始对象无法区分的情况。实际上这种情况也是没有的,因为说到底,代理就是对对象方法的增强,对应到框架内就是比如AOP的应用,所以不可能出现又要一个bean对象的代理对象又要这个bean的原始对象,这两张情况是不会同时发生的,而使用二级缓存去解决就是getEarlyBeanReference()这个方法,需要代理对象的时候返回代理对象,需要原始对象的时候就返回原始的对象。

再次感谢作者开源的本项目,学习到了很多知识,进步很多。从一开始的阅读源码不知道从哪里入手到现在已经能根据整个bean的生命周期独立的阅读spring的源码,当然学无止境,后续还要不断的学习消化

@DerekYRC
Copy link
Owner

@ccl125 奉献得到认可的感觉真的很棒,谢谢啦!!!学完本项目再去阅读原始spring/springboot源码会轻松很多,mini-spring-cloud也请支持下,有问题及时沟通。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants