spring循環(huán)依賴與三級(jí)緩存

什么是循環(huán)依賴钠龙?
循環(huán)依賴其實(shí)就是循環(huán)引用炬藤,也就是兩個(gè)或則兩個(gè)以上的bean互相持有對(duì)方,最終形成閉環(huán)碴里。比如A依賴于B沈矿,B依賴于C,C又依賴于A咬腋。

image.png

可以設(shè)想一下這個(gè)場景:如果在日常開發(fā)中我們用new對(duì)象的方式羹膳,若構(gòu)造函數(shù)之間發(fā)生這種循環(huán)依賴的話,程序會(huì)在運(yùn)行時(shí)一直循環(huán)調(diào)用最終導(dǎo)致內(nèi)存溢出根竿,示例代碼如下:

public class Main {
    public static void main(String[] args) throws Exception {
        System.out.println(new A());
    }
}

class A {
    public A() {
        new B();
    }
}

class B {
    public B() {
        new A();
    }
}

運(yùn)行結(jié)果會(huì)拋出Exception in thread "main" java.lang.StackOverflowError異常
這是一個(gè)典型的循環(huán)依賴問題陵像。本文說一下Spring是如果巧妙的解決平時(shí)我們會(huì)遇到的三大循環(huán)依賴問題的~

Spring Bean的循環(huán)依賴

談到Spring Bean的循環(huán)依賴,有的小伙伴可能比較陌生寇壳,畢竟開發(fā)過程中好像對(duì)循環(huán)依賴這個(gè)概念無感知醒颖。其實(shí)不然,你有這種錯(cuò)覺壳炎,權(quán)是因?yàn)槟愎ぷ髟赟pring的襁褓中泞歉,從而讓你“高枕無憂”~
我十分堅(jiān)信,小伙伴們?cè)谄綍r(shí)業(yè)務(wù)開發(fā)中一定一定寫過如下結(jié)構(gòu)的代碼:
field屬性注入(setter方法注入)循環(huán)依賴
這種方式是我們最為常用的依賴注入方式

@Service
class A {
    @Autowired
    private B b;
}

@Service
class B {
    @Autowired
    private A a;
}

這其實(shí)就是Spring環(huán)境下典型的循環(huán)依賴場景冕广。但是很顯然疏日,這種循環(huán)依賴場景,Spring已經(jīng)完美的幫我們解決和規(guī)避了問題撒汉。所以即使平時(shí)我們這樣循環(huán)引用沟优,也能夠整成進(jìn)行我們的coding之旅~

Spring中構(gòu)造器依賴場演示

在Spring環(huán)境中,因?yàn)槲覀兊腂ean的實(shí)例化睬辐、初始化都是交給了容器挠阁,因此它的循環(huán)依賴主要表現(xiàn)為下面三種場景。為了方便演示溯饵,我準(zhǔn)備了如下兩個(gè)類:

@Service
public class A {
    public A(B b) {
    }
}
@Service
public class B {
    public B(A a) {
    }
}

結(jié)果:項(xiàng)目啟動(dòng)失敗拋出異常BeanCurrentlyInCreationException

構(gòu)造器注入構(gòu)成的循環(huán)依賴侵俗,此種循環(huán)依賴方式是無法解決的,只能拋出BeanCurrentlyInCreationException異常表示循環(huán)依賴丰刊。這也是構(gòu)造器注入的最大劣勢(shì)隘谣。

根本原因:Spring解決循環(huán)依賴依靠的是Bean的“中間態(tài)”這個(gè)概念,而這個(gè)中間態(tài)指的是已經(jīng)實(shí)例化,但還沒初始化的狀態(tài)寻歧。而構(gòu)造器是完成實(shí)例化的掌栅,所以構(gòu)造器的循環(huán)依賴無法解決

對(duì)Bean的創(chuàng)建最為核心三個(gè)方法解釋如下:

  • createBeanInstance:例化,其實(shí)也就是調(diào)用對(duì)象的構(gòu)造方法實(shí)例化對(duì)象
  • populateBean:填充屬性码泛,這一步主要是對(duì)bean的依賴屬性進(jìn)行注入(@Autowired)
  • initializeBean:回到一些形如initMethod猾封、InitializingBean等方法

從對(duì)單例Bean的初始化可以看出,循環(huán)依賴主要發(fā)生在第二步(populateBean)噪珊,也就是field屬性注入的處理晌缘。

Spring容器的三級(jí)緩存

在Spring容器的整個(gè)聲明周期中,單例Bean有且僅有一個(gè)對(duì)象痢站。這很容易讓人想到可以用緩存來加速訪問磷箕。
從源碼中也可以看出Spring大量運(yùn)用了Cache的手段,在循環(huán)依賴問題的解決過程中甚至不惜使用了“三級(jí)緩存”瑟押,這也便是它設(shè)計(jì)的精妙之處~

三級(jí)緩存其實(shí)它更像是Spring容器工廠的內(nèi)的術(shù)語搀捷,采用三級(jí)緩存模式來解決循環(huán)依賴問題,這三級(jí)緩存分別指:

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    ...
    // 從上至下 分表代表這“三級(jí)緩存”
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); //一級(jí)緩存
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 二級(jí)緩存
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 三級(jí)緩存
    ...
    
    /** Names of beans that are currently in creation. */
    // 這個(gè)緩存也十分重要:它表示bean創(chuàng)建過程中都會(huì)在里面呆著~
    // 它在Bean開始創(chuàng)建時(shí)放值多望,創(chuàng)建完成時(shí)會(huì)將其移出~
    private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));

    /** Names of beans that have already been created at least once. */
    // 當(dāng)這個(gè)Bean被創(chuàng)建完成后,會(huì)標(biāo)記為這個(gè) 注意:這里是set集合 不會(huì)重復(fù)
    // 至少被創(chuàng)建了一次的  都會(huì)放進(jìn)這里~~~~
    private final Set<String> alreadyCreated = Collections.newSetFromMap(new ConcurrentHashMap<>(256));

注:AbstractBeanFactory繼承自DefaultSingletonBeanRegistry~

singletonObjects:用于存放完全初始化好的 bean氢烘,從該緩存中取出的 bean 可以直接使用
earlySingletonObjects:提前曝光的單例對(duì)象的cache怀偷,存放原始的 bean 對(duì)象(尚未填充屬性),用于解決循環(huán)依賴
singletonFactories:單例對(duì)象工廠的cache播玖,存放 bean 工廠對(duì)象椎工,用于解決循環(huán)依賴

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    ...
    @Override
    @Nullable
    public Object getSingleton(String beanName) {
        return getSingleton(beanName, true);
    }
    @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;
    }
    ...
    public boolean isSingletonCurrentlyInCreation(String beanName) {
        return this.singletonsCurrentlyInCreation.contains(beanName);
    }
    protected boolean isActuallyInCreation(String beanName) {
        return isSingletonCurrentlyInCreation(beanName);
    }
    ...
}

先從一級(jí)緩存singletonObjects中去獲取。(如果獲取到就直接return)
如果獲取不到或者對(duì)象正在創(chuàng)建中(isSingletonCurrentlyInCreation())蜀踏,那就再從二級(jí)緩存earlySingletonObjects中獲取维蒙。(如果獲取到就直接return)
如果還是獲取不到,且允許singletonFactories(allowEarlyReference=true)通過getObject()獲取果覆。就從三級(jí)緩存singletonFactory.getObject()獲取颅痊。(如果獲取到了就從singletonFactories中移除,并且放進(jìn)earlySingletonObjects局待。其實(shí)也就是從三級(jí)緩存移動(dòng)(是剪切斑响、不是復(fù)制哦~)到了二級(jí)緩存)
加入singletonFactories三級(jí)緩存的前提是執(zhí)行了構(gòu)造器,所以構(gòu)造器的循環(huán)依賴沒法解決

getSingleton()從緩存里獲取單例對(duì)象步驟分析可知钳榨,Spring解決循環(huán)依賴的訣竅:就在于singletonFactories這個(gè)三級(jí)緩存舰罚。這個(gè)Cache里面都是ObjectFactory,它是解決問題的關(guān)鍵薛耻。

為什么要用三級(jí)緩存而不是二級(jí)緩存

image.png

可以看到三級(jí)緩存各自保存的對(duì)象营罢,這里重點(diǎn)關(guān)注二級(jí)緩存earlySingletonObjects和三級(jí)緩存singletonFactory,一級(jí)緩存可以進(jìn)行忽略饼齿。前面我們講過先實(shí)例化的bean會(huì)通過ObjectFactory半成品提前暴露在三級(jí)緩存中
所以如果沒有AOP的話確實(shí)可以兩級(jí)緩存就可以解決循環(huán)依賴的問題饲漾,如果加上AOP瘟滨,兩級(jí)緩存是無法解決的,不可能每次執(zhí)行singleFactory.getObject()方法都給我產(chǎn)生一個(gè)新的代理對(duì)象能颁,所以還要借助另外一個(gè)緩存來保存產(chǎn)生的代理對(duì)象

靜態(tài)代理

靜態(tài)代理的特點(diǎn)是, 為每一個(gè)業(yè)務(wù)增強(qiáng)都提供一個(gè)代理類, 由代理類來創(chuàng)建代理對(duì)象. 下面我們通過靜態(tài)代理來實(shí)現(xiàn)對(duì)轉(zhuǎn)賬業(yè)務(wù)進(jìn)行身份驗(yàn)證.

(1) 轉(zhuǎn)賬業(yè)務(wù)

public interface IAccountService {
    //主業(yè)務(wù)邏輯: 轉(zhuǎn)賬
    void transfer();
}
public class AccountServiceImpl implements IAccountService {
    @Override
    public void transfer() {
        System.out.println("調(diào)用dao層,完成轉(zhuǎn)賬主業(yè)務(wù).");
    }
}

(2) 代理類

public class AccountProxy implements IAccountService {
    //目標(biāo)對(duì)象
    private IAccountService target;

    public AccountProxy(IAccountService target) {
        this.target = target;
    }

    /**
     * 代理方法,實(shí)現(xiàn)對(duì)目標(biāo)方法的功能增強(qiáng)
     */
    @Override
    public void transfer() {
        before();
        target.transfer();
    }

    /**
     * 前置增強(qiáng)
     */
    private void before() {
        System.out.println("對(duì)轉(zhuǎn)賬人身份進(jìn)行驗(yàn)證.");
    }
}

(3) 測(cè)試

public class Client {
    public static void main(String[] args) {
        //創(chuàng)建目標(biāo)對(duì)象
        IAccountService target = new AccountServiceImpl();
        //創(chuàng)建代理對(duì)象
        AccountProxy proxy = new AccountProxy(target);
        proxy.transfer();
    }
}

結(jié)果: 
對(duì)轉(zhuǎn)賬人身份進(jìn)行驗(yàn)證.
調(diào)用dao層,完成轉(zhuǎn)賬主業(yè)務(wù).

動(dòng)態(tài)代理

靜態(tài)代理會(huì)為每一個(gè)業(yè)務(wù)增強(qiáng)都提供一個(gè)代理類, 由代理類來創(chuàng)建代理對(duì)象, 而動(dòng)態(tài)代理并不存在代理類, 代理對(duì)象直接由代理生成工具動(dòng)態(tài)生成.

JDK動(dòng)態(tài)代理

JDK動(dòng)態(tài)代理是使用 java.lang.reflect 包下的代理類來實(shí)現(xiàn). JDK動(dòng)態(tài)代理動(dòng)態(tài)代理必須要有接口.

(1) 轉(zhuǎn)賬業(yè)務(wù)

public interface IAccountService {
    //主業(yè)務(wù)邏輯: 轉(zhuǎn)賬
    void transfer();
}
public class AccountServiceImpl implements IAccountService {
    @Override
    public void transfer() {
        System.out.println("調(diào)用dao層,完成轉(zhuǎn)賬主業(yè)務(wù).");
    }
}

(2) 增強(qiáng)

因?yàn)檫@里沒有配置切入點(diǎn), 稱為切面會(huì)有點(diǎn)奇怪, 所以稱為增強(qiáng).

public class AccountAdvice implements InvocationHandler {
    //目標(biāo)對(duì)象
    private IAccountService target;

    public AccountAdvice(IAccountService target) {
        this.target = target;
    }

    /**
     * 代理方法, 每次調(diào)用目標(biāo)方法時(shí)都會(huì)進(jìn)到這里
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        return method.invoke(target, args);
    }

    /**
     * 前置增強(qiáng)
     */
    private void before() {
        System.out.println("對(duì)轉(zhuǎn)賬人身份進(jìn)行驗(yàn)證.");
    }
}

(3) 測(cè)試

public class Client {
    public static void main(String[] args) {
        //創(chuàng)建目標(biāo)對(duì)象
        IAccountService target = new AccountServiceImpl();
        //創(chuàng)建代理對(duì)象
        IAccountService proxy = (IAccountService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new AccountAdvice(target)
        );
        proxy.transfer();
    }
}
結(jié)果: 
對(duì)轉(zhuǎn)賬人身份進(jìn)行驗(yàn)證.
調(diào)用dao層,完成轉(zhuǎn)賬主業(yè)務(wù).

CGLIB動(dòng)態(tài)代理

JDK動(dòng)態(tài)代理必須要有接口, 但如果要代理一個(gè)沒有接口的類該怎么辦呢? 這時(shí)我們可以使用CGLIB動(dòng)態(tài)代理. CGLIB動(dòng)態(tài)代理的原理是生成目標(biāo)類的子類, 這個(gè)子類對(duì)象就是代理對(duì)象, 代理對(duì)象是被增強(qiáng)過的.

注意: 不管有沒有接口都可以使用CGLIB動(dòng)態(tài)代理, 而不是只有在無接口的情況下才能使用.

(1) 轉(zhuǎn)賬業(yè)務(wù)

public class AccountService {
    public void transfer() {
        System.out.println("調(diào)用dao層,完成轉(zhuǎn)賬主業(yè)務(wù).");
    }
}

(2) 增強(qiáng)

因?yàn)檫@里沒有配置切入點(diǎn), 稱為切面會(huì)有點(diǎn)奇怪, 所以稱為增強(qiáng).

public class AccountAdvice implements MethodInterceptor {
    /**
     * 代理方法, 每次調(diào)用目標(biāo)方法時(shí)都會(huì)進(jìn)到這里
     */
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        before();
        return methodProxy.invokeSuper(obj, args);
        //        return method.invoke(obj, args);  這種也行
    }

    /**
     * 前置增強(qiáng)
     */
    private void before() {
        System.out.println("對(duì)轉(zhuǎn)賬人身份進(jìn)行驗(yàn)證.");
    }
}

(3) 測(cè)試

public class Client {
    public static void main(String[] args) {
        //創(chuàng)建目標(biāo)對(duì)象
        AccountService target = new AccountService();
        //
        //創(chuàng)建代理對(duì)象
        AccountService proxy = (AccountService) Enhancer.create(target.getClass(),
                new AccountAdvice());
        proxy.transfer();
    }
}
結(jié)果: 
對(duì)轉(zhuǎn)賬人身份進(jìn)行驗(yàn)證.
調(diào)用dao層,完成轉(zhuǎn)賬主業(yè)務(wù).

參考地址:https://www.cnblogs.com/semi-sub/p/13548479.html
參考地址:https://blog.csdn.net/f641385712/article/details/92801300
參考地址:https://blog.csdn.net/litianxiang_kaola/article/details/85335700

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末杂瘸,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子伙菊,更是在濱河造成了極大的恐慌败玉,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件镜硕,死亡現(xiàn)場離奇詭異运翼,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)兴枯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門血淌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人财剖,你說我怎么就攤上這事悠夯。” “怎么了躺坟?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵沦补,是天一觀的道長。 經(jīng)常有香客問我咪橙,道長夕膀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任美侦,我火速辦了婚禮产舞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘菠剩。我一直安慰自己易猫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布赠叼。 她就那樣靜靜地躺著擦囊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嘴办。 梳的紋絲不亂的頭發(fā)上瞬场,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音涧郊,去河邊找鬼贯被。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的彤灶。 我是一名探鬼主播看幼,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼幌陕!你這毒婦竟也來了诵姜?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤搏熄,失蹤者是張志新(化名)和其女友劉穎棚唆,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體心例,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宵凌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了止后。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞎惫。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖译株,靈堂內(nèi)的尸體忽然破棺而出瓜喇,到底是詐尸還是另有隱情,我是刑警寧澤古戴,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布欠橘,位于F島的核電站,受9級(jí)特大地震影響现恼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜黍檩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一叉袍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧刽酱,春花似錦喳逛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至殿怜,卻和暖如春典蝌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背头谜。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來泰國打工骏掀, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓截驮,卻偏偏與公主長得像笑陈,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子葵袭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345