日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

互相引用 spring_巧夺天工,这样理解Spring的IOC、DI下来,真的很清晰了

發(fā)布時間:2025/3/20 javascript 19 豆豆
生活随笔 收集整理的這篇文章主要介紹了 互相引用 spring_巧夺天工,这样理解Spring的IOC、DI下来,真的很清晰了 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

前言

你可能會有如下問題:

  • 想看Spring源碼,但是不知道應(yīng)當(dāng)如何入手去看,對整個Bean的流程沒有概念,碰到相關(guān)問題也沒有頭緒如何下手
  • 看過幾遍源碼,沒辦法徹底理解,沒什么感覺,沒過一陣子又忘了本文將結(jié)合實際問題,由問題引出源碼,并在解釋時會盡量以圖表的形式讓你一步一步徹底理解Spring Bean的IOC、DI、生命周期、作用域等。
  • 循環(huán)依賴問題

    循環(huán)依賴其實就是循環(huán)引用,也就是兩個或則兩個以上的bean互相持有對方,最終形成閉環(huán)。比如A依賴于B,B依賴于C,C又依賴于A。如下圖:

    如何理解“依賴”呢,在Spring中有:

    • 構(gòu)造器循環(huán)依賴
    • field屬性注入循環(huán)依賴

    代碼:

    1、構(gòu)造器循環(huán)依賴

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

    結(jié)果: 項目啟動報錯

    2、field屬性注入循環(huán)依賴問題

    @Service public class A1 { @Autowired private B1 b1; } @Service public class B1 { @Autowired public C1 c1; } @Service public class C1 { @Autowired public A1 a1; }

    項目啟動成功,如圖:

    3、field屬性注入循環(huán)依賴(prototype)

    @Service @Scope("prototype") public class A1 { @Autowired private B1 b1; } @Service @Scope("prototype") public class B1 { @Autowired public C1 c1; } @Service @Scope("prototype") public class C1 { @Autowired public A1 a1; }

    項目啟動失敗,發(fā)現(xiàn)了一個cycle。

    總結(jié): 同樣對于循環(huán)依賴的場景,構(gòu)造器注入和prototype類型的屬性注入都會初始化Bean失敗。因為@Service默認(rèn)是單例的,所以單例的屬性注入是可以成功的。

    分析

    分析原因也就是在發(fā)現(xiàn)SpringIOC的過程,如果不想閱讀源碼可以閱讀分析之后的總結(jié)和循環(huán)依賴問題的分析即可。

    SpringBean的加載流程(源碼分析)

    入口:

    ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml"); ac.getBean(XXX.class);


    ClassPathXmlApplicationContext是一個加載XML配置文件的類,與之相對的還有
    AnnotationConfigWebApplicationContext,這兩個類大差不差的,只是
    ClassPathXmlApplicationContext的Resource是XML文件而
    AnnotationConfigWebApplicationContext是Scan注解獲得的。

    第二行就已經(jīng)可以直接獲取bean的實例了,所以第一行構(gòu)造方法時,就已經(jīng)完成了對所有bean的加載。 以
    ClassPathXmlApplicationContext舉例,他里面儲存的東西如下:

    構(gòu)造方法如下:

    refresh方法:

    先看看refresh方法的結(jié)構(gòu):

  • 方法為什么加鎖? 是為了避免多線程的場景下同時刷新Spring上下文
  • 雖然整個方法是加鎖的,但是卻用了Synchronized關(guān)鍵字的對象鎖startUpShutdownMonitor,這樣做有兩個好處: (1)關(guān)閉資源的時候會調(diào)用close()方法,close()方法也使用了同樣的對象鎖,而關(guān)閉資源的close和refresh的兩個沖突的方法,這樣可以避免沖突 (2)此處對象鎖相對于整個方法加鎖的話,同步的范圍更小了,鎖的粒度更小,效率更高
  • 這個方法refresh定義了整個Spring IOC的流程,每一個方法名字都清晰易懂,可維護性、可讀性很強
  • 總結(jié)一下:看源碼需要找準(zhǔn)入口,看的時候多思考,學(xué)習(xí)Spring的巧妙的設(shè)計。ApplicationContext的構(gòu)造方法中最關(guān)鍵是方法是refresh,其中有一些比價好的設(shè)計。

    obtainFreshBeanFactory方法

    這個方法作用是獲取刷新Spring上下文的Bean工廠:

    protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { this.refreshBeanFactory(); return this.getBeanFactory(); } protected final void refreshBeanFactory() throws BeansException { if (this.hasBeanFactory()) { this.destroyBeans(); this.closeBeanFactory(); } try { DefaultListableBeanFactory beanFactory = this.createBeanFactory(); beanFactory.setSerializationId(this.getId()); this.customizeBeanFactory(beanFactory); this.loadBeanDefinitions(beanFactory); synchronized(this.beanFactoryMonitor) { this.beanFactory = beanFactory; } } catch (IOException var5) { throw new ApplicationContextException("I/O error parsing bean definition source for " + this.getDisplayName(), var5); } }

    這斷代碼的核心是
    DefaultListableBeanFactory,核心類我們再整理一下,以圖表格式:

    下面有三個加粗的Map,這些個Map是解決問題的關(guān)鍵。。。我們之后詳細(xì)分析

    BeanDefinition在IOC容器中的注冊

    接下來簡要分析一下loadBeanDefinitions。 對于這個BeanDefinition,我是這么理解的: 它是SpringIOC過程中間的一個產(chǎn)物,可以看成是對Bean定義的抽象,里面封裝的數(shù)據(jù)都是與Bean定義相關(guān)的,封裝了一些基本的bean的Property、initi-method、destroy-method等。這里的主要方法是loadBeanDefinitions,這里不詳細(xì)展開說,它主要做了幾件事:

  • 初始化了BeanDefinitionReader
  • 通過BeanDefinitionReader獲取Resource,也就是xml配置文件的位置,并且把文件轉(zhuǎn)換成一個叫Document的對象.
  • 接下來需要將Document對象轉(zhuǎn)化成容器內(nèi)部的數(shù)據(jù)結(jié)構(gòu)(也就是BeanDefinition),也即是將Bean定義的List、Map、Set等各種元素進行解析,轉(zhuǎn)換成Managed類(Spring對BeanDefinition數(shù)據(jù)的封裝)放在BeanDefinition中;這個方法是RegisterBeanDefinition(),也就是解析的過程。
  • 解析完成后,會把解析的結(jié)果放到BeanDefinition對象中并設(shè)置到一個Map中 以上這個過程就是BeanDefinition在IOC容器中的注冊。 再回到Refresh方法,總結(jié)每一步如下圖: 總結(jié):這一部分步驟主要是Spring如何加載Xml文件或者注解,并把它解析成BeanDefinition
  • Spring創(chuàng)建Bean的過程 先回到之前的refresh方法(也就是在構(gòu)造ApplicationContext時的方法),我們跳過不重要的部分:

    我們直接看
    finishBeanFactoryInitialization里面的preInstantiateSingletons方法,顧名思義初始化所有的單例bean,截取部分如下: 現(xiàn)在來看核心的getBean方法,對于所有獲取Bean對象是實例,都是用這個getBean方法,這個方法最終調(diào)用的是doGetBean方法,這個方法就是所謂的DI(依賴注入)發(fā)生的地方。 程序=數(shù)據(jù)+算法,之前的BeanDefinition就是“數(shù)據(jù)”,依賴注入也就是在BeanDefinition準(zhǔn)備好情況下進行進行的,這個過程不簡單,因為Spring提供了很多參數(shù)配置,每一個參數(shù)都代表了IOC容器的特性,這些特性的實現(xiàn)需要在Bean的生命周期中完成。

    代碼比較多,就不貼了,大家可以自行查看AbstractBeanFactory里面的doGetBean方法,這里直接上圖,這個圖就是依賴注入的整個過程:

    總結(jié):Spring創(chuàng)建好了BeanDefinition之后呢,會開始實例化Bean,并且對Bean的依賴屬性進行填充。實例化時底層使用了CGLIB或Java反射技術(shù)。上圖中instantiateBean核PupulateBean方法很重要!

    循環(huán)依賴問題分析 總結(jié):

  • 構(gòu)造器注入和prototype類型的field注入發(fā)生循環(huán)依賴時都無法初始化
  • field注入單例的bean時,盡管有循環(huán)依賴,但bean仍然可以被成功初始化針對這幾個結(jié)論,提出問題 1、單例的設(shè)值注入bean是如何解決循環(huán)依賴問題呢?如果A中注入了B,那么他們初始化的順序是什么樣子的? 2、為什么prototype類型的和構(gòu)造器類型的Spring無法解決循環(huán)依賴呢?
  • 一級緩存: /** 保存所有的singletonBean的實例 */ private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);二級緩存: /** 保存所有早期創(chuàng)建的Bean對象,這個Bean還沒有完成依賴注入 */ private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16); 三級緩存: /** singletonBean的生產(chǎn)工廠*/ private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);/** 保存所有已經(jīng)完成初始化的Bean的名字(name) */ private final Set<String> registeredSingletons = new LinkedHashSet<String>(64);/** 標(biāo)識指定name的Bean對象是否處于創(chuàng)建狀態(tài) 這個狀態(tài)非常重要 */ private final Set<String> singletonsCurrentlyInCreation =Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>(16));

    前面三個Map,我們稱為單例初始化的三級緩存,理解這個問題,我們目前只需關(guān)注“三級”,也就是singletonFactories

    分析:

    對于問題1,單例的設(shè)值注入,如果A中注入了B,B應(yīng)該是A中的一個屬性,那么猜想應(yīng)該是A已經(jīng)被instantiate(實例化)之后,在populateBean(填充A中的屬性)時,對B進行初始化。

    對于問題2,instantiate(實例化)其實就是理解成new一個對象的過程,而new的時候肯定要執(zhí)行構(gòu)造方法,所以猜想對于應(yīng)該是A在instantiate(實例化)時,進行B的初始化。

    有了分析和猜想之后呢,圍繞關(guān)鍵的屬性,根據(jù)從上圖的doGetBean方法開始到populateBean所有的代碼,我整理了如下圖:

    上圖是整個過程中關(guān)鍵的代碼路徑,感興趣的可以自己debug幾回,最關(guān)鍵的解決循環(huán)依賴的是如上的兩個標(biāo)紅的方法,第一個方法getSingleton會從singletonFactories里面拿Singleton,而addSingletonFactory會把Singleton放入singletonFactories。

    對于問題1:單例的設(shè)值注入bean是如何解決循環(huán)依賴問題呢?如果A中注入了B,那么他們初始化的順序是什么樣子的?

    假設(shè)循環(huán)注入是A-B-A:A依賴B(A中autowire了B),B又依賴A(B中又autowire了A):

    本質(zhì)就是三級緩存發(fā)揮作用,解決了循環(huán)。

    對于當(dāng)時問題2,instantiate(實例化)其實就是理解成new一個對象的過程,而new的時候肯定要執(zhí)行構(gòu)造方法,所以猜想對于應(yīng)該是A在instantiate(實例化)時,進行B的初始化。

    答案也很簡單,因為A中構(gòu)造器注入了B,那么A在關(guān)鍵的方法addSingletonFactory()之前就去初始化了B,導(dǎo)致三級緩存中根本沒有A,所以會發(fā)生死循環(huán),Spring發(fā)現(xiàn)之后就拋出異常了。至于Spring是如何發(fā)現(xiàn)異常的呢,本質(zhì)上是根據(jù)Bean的狀態(tài)給Bean進行mark,如果遞歸調(diào)用時發(fā)現(xiàn)bean當(dāng)時正在創(chuàng)建中,那么久拋出循環(huán)依賴的異常即可。

    那么prototype的Bean是如何初始化的呢? prototypeBean有一個關(guān)鍵的屬性:

    /** Names of beans that are currently in creation */ private final ThreadLocal<Object> prototypesCurrentlyInCreation =new NamedThreadLocal<Object>("Prototype beans currently in creation");

    保存著正在創(chuàng)建的prototype的beanName,在流程上并沒有暴露任何factory之類的緩存。并且在beforePrototypeCreation(String beanName)方法時,把每個正在創(chuàng)建的prototype的BeanName放入一個set中:

    protected void beforePrototypeCreation(String beanName) {Object curVal = this.prototypesCurrentlyInCreation.get();if (curVal == null) {this.prototypesCurrentlyInCreation.set(beanName);}else if (curVal instanceof String) {Set<String> beanNameSet = new HashSet<String>(2);beanNameSet.add((String) curVal);beanNameSet.add(beanName);this.prototypesCurrentlyInCreation.set(beanNameSet);}else {Set<String> beanNameSet = (Set<String>) curVal;beanNameSet.add(beanName);} }

    并且會循環(huán)依賴時檢查beanName是否處于創(chuàng)建狀態(tài),如果是就拋出異常:

    protected boolean isPrototypeCurrentlyInCreation(String beanName) {Object curVal = this.prototypesCurrentlyInCreation.get();return (curVal != null &&(curVal.equals(beanName) || (curVal instanceof Set && ((Set<?>) curVal).contains(beanName)))); }

    從流程上就可以查看,無論是構(gòu)造注入還是設(shè)值注入,第二次進入同一個Bean的getBean方法是,一定會在校驗部分拋出異常,因此不能完成注入,也就不能實現(xiàn)循環(huán)引用。

    總結(jié):Spring在InstantiateBean時執(zhí)行構(gòu)造器方法,構(gòu)造出實例,如果是單例的話,會將它放入一個singletonBeanFactory的緩存中,再進行populateBean方法,設(shè)置屬性。通過一個singletonBeanFactory的緩存解決了循環(huán)依賴的問題。

    再解決一個問題

    現(xiàn)在大家已經(jīng)對Spring整個流程有點感覺了,我們再來解決一個簡單的常見的問題:

    考慮一下如下的singleton代碼:

    @Servicepublic class SingletonBean{@Autowired private PrototypeBean prototypeBean;public void doSomething(){System.out.println(prototypeBean.toString()); }} @Component @Scope(value="prototype")public class PrototypeBean{}

    一個Singleton的Bean中Autowired了一個prototype的Bean,那么問題來了,每次調(diào)用SingletonBean.doSomething()時打印的對象是不是同一個呢?有了之前的知識儲備,我們簡單分析一下:因為Singleton是單例的,所以在項目啟動時就會初始化,prototypeBean本質(zhì)上只是它的一個Property,那么ApplicationContex中只存在一個SingletonBean和一個初始化SingletonBean時創(chuàng)建的一個prototype類型的PrototypeBean。那么每次調(diào)用SingletonBean.doSomething()時,Spring會從ApplicationContex中獲取SingletonBean,每次獲取的SingletonBean是同一個,所以即便PrototypeBean是prototype的,但PrototypeBean仍然是同一個。每次打印出來的內(nèi)存地址肯定是同一個。

    那這個問題如何解決呢? 解決辦法也很簡單,這種情況我們不能通過注入的方式注入一個prototypeBean,只能在程序運行時手動調(diào)用getBean("prototypeBean")方法,我寫了一個簡單的工具類:

    @Service public class SpringBeanUtils implements ApplicationContextAware { private static ApplicationContext appContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringBeanUtils.appContext=applicationContext; } public static ApplicationContext getAppContext() { return appContext; } public static Object getBean(String beanName) { checkApplicationContext(); return appContext.getBean(beanName); } private static void checkApplicationContext() { if (null == appContext) { throw new IllegalStateException("applicaitonContext未注入"); } } @SuppressWarnings("unchecked") public static <T> T getBean(Class<T> clazz) { checkApplicationContext(); Map<?, ?> map = appContext.getBeansOfType(clazz); return map.isEmpty() ? null : (T) map.values().iterator().next(); }}

    對于這個ApplicationContextAware接口:在某些特殊的情況下,Bean需要實現(xiàn)某個功能,但該功能必須借助于Spring容器才能實現(xiàn),此時就必須讓該Bean先獲取Spring容器,然后借助于Spring容器實現(xiàn)該功能。為了讓Bean獲取它所在的Spring容器,可以讓該Bean實現(xiàn)ApplicationContextAware接口。感興趣的讀者自己可以試試。

    總結(jié):

    回到循環(huán)依賴的問題,有的人可能會問singletonBeanFactory只是一個三級緩存,那么一級緩存和二級緩存有什么用呢?

    其實大家只要理解整個流程就可以切入了,Spring在初始化Singleton的時候大致可以分幾步,初始化——設(shè)值——銷毀,循環(huán)依賴的場景下只有A——B——A這樣的順序,但在并發(fā)的場景下,每一步在執(zhí)行時,都有可能調(diào)用getBean方法,而單例的Bean需要保證只有一個instance,那么Spring就是通過這些個緩存外加對象鎖去解決這類問題,同時也可以省去不必要的重復(fù)操作。Spring的鎖的粒度選取也是很吊的,這里暫時不深入研究了。

    解決此類問題的關(guān)鍵是要對SpringIOC和DI的整個流程做到心中有數(shù),看源碼一般情況下不要求每一行代碼都了解透徹,但是對于整個的流程和每個流程中在做什么事需要了然,這樣實際遇到問題時才可以很快的切入進行分析解決。

    作者:LeZiJieEdu
    鏈接:https://juejin.im/post/5ec9100a6fb9a047f6103b7f
    來源:掘金

    總結(jié)

    以上是生活随笔為你收集整理的互相引用 spring_巧夺天工,这样理解Spring的IOC、DI下来,真的很清晰了的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。