1. 程式人生 > >521我發誓讀完本文,再也不會擔心Spring配置類問題了

521我發誓讀完本文,再也不會擔心Spring配置類問題了

> 當大潮退去,才知道誰在裸泳。關注公眾號【**BAT的烏托邦**】開啟專欄式學習,拒絕淺嘗輒止。本文 [https://www.yourbatman.cn](https://www.yourbatman.cn) 已收錄,裡面一併有Spring技術棧、MyBatis、中介軟體等小而美的專欄供以學習哦。 [TOC] ![](https://img-blog.csdnimg.cn/20200710172335199.png) # 前言 各位小夥伴大家好,我是A哥。本文對Spring `@Configuration`配置類繼續進階,雖然有點燒腦,但目的只有一個:為拿高薪備好彈藥。如果說上篇文章已經腦力有點“不適”了,那這裡得先給你個下馬威:本篇文章內容將更加的讓你“感覺不適”。 > 讀本文之前,為確保連貫性,建議你移步先閱讀上篇文章內容,直達電梯:[你自我介紹說很懂Spring配置類,那你怎麼解釋這個現象?](https://mp.weixin.qq.com/s/qKenyoydYm4q2yPnGSdMZw) 為什麼有些時候我會建議先閱讀上篇文章,這確實是無奈之舉。技術的內容一般都具有很強相關性,它是需要有Context上下文支撐的,**所以花幾分鐘先了解相關內容效果更佳**,磨刀不誤砍柴工的道理大家都懂。同時呢,這也是寫深度分析類的技術文章的尷尬之處:吃力反而不討好,需要堅持。 ![](https://img-blog.csdnimg.cn/202005200813015.png) --- ## 版本約定 本文內容若沒做特殊說明,均基於以下版本: - JDK:`1.8` - Spring Framework:`5.2.2.RELEASE` --- # 正文 [上篇文章](https://mp.weixin.qq.com/s/qKenyoydYm4q2yPnGSdMZw)介紹了代理物件兩個攔截器其中的前者,即`BeanFactoryAwareMethodInterceptor`,它會攔截`setBeanFactory()`方法從而完成給代理類指定屬性賦值。通過第一個攔截器的講解,你能夠成功“忽悠”很多面試官了,但仍舊不能夠解釋我們最常使用中的這個疑惑:**為何通過呼叫@Bean方法最終指向的仍舊是同一個Bean呢?** 帶著這個疑問,開始本文的陳訴。請繫好安全帶,準備發車了... ![](https://img-blog.csdnimg.cn/20200520082130796.png) --- ## Spring配置類的使用誤區 根據不同的配置方式,展示不同情況。從Lite模式的使用**產生誤區**,到使用Full模式解決問題,最後引出解釋為何有此效果的原因分析/原始碼解析。 --- ### Lite模式:錯誤姿勢 配置類: ```java public class AppConfig { @Bean public Son son() { Son son = new Son(); System.out.println("son created..." + son.hashCode()); return son; } @Bean public Parent parent() { Son son = son(); System.out.println("parent created...持有的Son是:" + son.hashCode()); return new Parent(son); } } ``` 執行程式: ```java public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); AppConfig appConfig = context.getBean(AppConfig.class); System.out.println(appConfig); // bean情況 Son son = context.getBean(Son.class); Parent parent = context.getBean(Parent.class); System.out.println("容器內的Son例項:" + son.hashCode()); System.out.println("容器內Person持有的Son例項:" + parent.getSon().hashCode()); System.out.println(parent.getSon() == son); } ``` 執行結果: ```java son created...624271064 son created...564742142 parent created...持有的Son是:564742142 com.yourbatman.fullliteconfig.config.AppConfig@1a38c59b 容器內的Son例項:624271064 容器內Person持有的Son例項:564742142 false ``` 結果分析: - Son例項被**建立了2次**。很明顯這兩個**不是同一個**例項 - 第一次是由Spring建立並放進容器裡(`624271064`這個) - 第二次是由構造parent時建立,只放進了parent裡,並沒放進容器裡(`564742142`這個) 這樣的話,**就出問題了**。問題表現在這兩個方面: 1. Son物件被建立了兩次,**單例模式被打破** 2. 對Parent例項而言,它依賴的Son不再是IoC容器內的那個Bean,而是一個非常普通的POJO物件而已。所以這個Son物件將不會享有Spring帶來的任何“好處”,這在實際場景中一般都是會有問題的 這種情況在生產上是**一定需要避免**,那怎麼破呢?下面給出Lite模式下使用的正確姿勢。 --- ### Lite模式:正確姿勢 其實這個問題,現在這麼智慧的IDE(如IDEA)已經能教你怎麼做了: ![](https://img-blog.csdnimg.cn/20200517162902739.png) 按照“指示”,可以使用**依賴注入**的方式代替從而避免這種問題,如下: ```java // @Bean // public Parent parent() { // Son son = son(); // System.out.println("parent created...持有的Son是:" + son.hashCode()); // return new Parent(son); // } @Bean public Parent parent(Son son){ System.out.println("parent created...持有的Son是:" + son.hashCode()); return new Parent(son); } ``` 再次執行程式,結果為: ```java son created...624271064 parent created...持有的Son是:624271064 com.yourbatman.fullliteconfig.config.AppConfig@667a738 容器內的Son例項:624271064 容器內Person持有的Son例項:624271064 true ``` bingo,**完美解決了問題**。如果你堅持使用Lite模式,那麼請注意它的優缺點哦(Full模式和Lite模式的優缺點見[這篇文章](https://mp.weixin.qq.com/s/rXy9T3VgWvdl6Kje1mwCZA))。 沒有仔細看的同學可能會問:我明明就是按照第一種方式寫的,也正常work沒問題呀。說你是不細心吧還真是,不信你再回去瞅瞅對比對比。如果你用第一種方式並且能夠“正常work”,那請你查查類頭上是不是標註有`@Configuration`註解? ![](https://img-blog.csdnimg.cn/20200520104347207.png) --- ### Full模式: Full模式是容錯性最強的一種方式,你亂造都行,沒啥顧慮。 > 當然嘍,方法不能是private/final。但一般情況下誰會在配置裡final掉一個方法呢?你說對吧~ ```java @Configuration public class AppConfig { @Bean public Son son() { Son son = new Son(); System.out.println("son created..." + son.hashCode()); return son; } @Bean public Parent parent() { Son son = son(); System.out.println("parent created...持有的Son是:" + son.hashCode()); return new Parent(son); } } ``` 執行程式,結果輸出: ```java son created...1797712197 parent created...持有的Son是:1797712197 com.yourbatman.fullliteconfig.config.AppConfig$$EnhancerBySpringCGLIB$$8ef51461@be64738 容器內的Son例項:1797712197 容器內Person持有的Son例項:1797712197 true ``` **結果是完美的**。它能夠保證你通過呼叫標註有@Bean的方法得到的是IoC容器裡面的例項物件,而非重新建立一個。相比較於Lite模式,它還有另外一個區別:它會為配置類生成一個`CGLIB`的代理子類物件放進容器,而Lite模式放進容器的是原生物件。 **凡事皆有代價,一切皆在取捨**。原生的才是效率最高的,是對Cloud Native最為友好的方式。但在實際“推薦使用”上,**業務端開發一般只會使用Full模式**,畢竟業務開發的同學水平是殘參差不齊的,容錯性就顯得至關重要了。 > 如果你是容器開發者、中介軟體開發者...推薦使用Lite模式配置,為容器化、Cloud Native做好準備嘛~ Full模式既然是面向使用側為常用的方式,那麼接下來就趴一趴Spring到底是施了什麼“魔法”,讓呼叫@Bean方法竟然可以不進入方法體內而指向同一個例項。 --- --- ## BeanMethodInterceptor攔截器 終於到了今天的主菜。關於前面的流程分析本文就一步跳過,單刀直入分析`BeanMethodInterceptor`這個攔截器,也也就是所謂的兩個攔截器的**後者**。 > 溫馨提示:親務必確保已經瞭解過了上篇文章的流程分析哈,不然下面內容很容易造成你`腦力不適`的 相較於上個攔截器,這個攔截器不可為不復雜。官方解釋它的作用為:攔截任何標註有`@Bean`註解的方法的**呼叫**,以確保正確處理Bean語義,例如**作用域**(請別忽略它)和AOP代理。 複雜歸複雜,但沒啥好怕的,一步一步來唄。同樣的,我會按如下兩步去了解它:**執行時機** + **做了何事**。 --- ### 執行時機 廢話不多說,直接結合原始碼解釋。 ```java BeanMethodInterceptor: @Override public boolean isMatch(Method candidateMethod) { return (candidateMethod.getDeclaringClass() != Object.class && !BeanFactoryAwareMethodInterceptor.isSetBeanFactory(candidateMethod) && BeanAnnotationHelper.isBeanAnnotated(candidateMethod)); } ``` 三行程式碼,三個條件: 1. 該方法不能是Object的方法(即使你Object的方法標註了@Bean,我也不認) 2. 不能是`setBeanFactory()`方法。這很容易理解,它交給上個攔截器搞定即可 3. **方法必須標註標註有@Bean註解** 簡而言之,**標註有@Bean註解方法`執行時`會被攔截**。 所以下面例子中的son()和parent()這兩個,**以及parent()裡面呼叫的son()方法的執行**它都會攔截(一共攔截3次)~ > 小細節:方法只要是個Method即可,**無論是static方法**還是普通方法,都會“參與”此判斷邏輯哦 --- ### 做了何事 這裡是具體攔截邏輯,會比第一個攔截器複雜很多。原始碼不算非常的多,但牽扯到的東西還真不少,比如AOP、比如Scope、比如Bean的建立等等,**理解起來還蠻費勁的**。 本處以攔截到`parent()`方法的執行為例,結合原始碼進行跟蹤講解: ```java BeanMethodInterceptor: // enhancedConfigInstance:被攔截的物件例項,也是代理物件 // beanMethod:parent()方法 // beanMethodArgs:空 // cglibMethodProxy:代理。用於呼叫其invoke/invokeSuper()來執行對應的方法 @Override @Nullable public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs, MethodProxy cglibMethodProxy) throws Throwable { // 通過反射,獲取到Bean工廠。也就是$$beanFactory這個屬性的值~ ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance); // 拿到Bean的名稱 String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod); // 判斷這個方法是否是Scoped代理物件 很明顯本利裡是沒有標註的 暫先略過 // 簡答的說:parent()方法頭上是否標註有@Scoped註解~~~ if (BeanAnnotationHelper.isScopedProxy(beanMethod)) { String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName); if (beanFactory.isCurrentlyInCreation(scopedBeanName)) { beanName = scopedBeanName; } } // ========下面要處理bean間方法引用的情況了======== // 首先:檢查所請求的Bean是否是FactoryBean。也就是bean名稱為`&parent`的Bean是否存在 // 如果是的話,就建立一個代理子類,攔截它的getObject()方法以返回容器裡的例項 // 這樣做保證了方法返回一個FactoryBean和@Bean的語義是效果一樣的,確保了不會重複建立多個Bean if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX + beanName) && factoryContainsBean(beanFactory, beanName)) { // 先得到這個工廠Bean Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName); if (factoryBean instanceof ScopedProxyFactoryBean) { // Scoped proxy factory beans are a special case and should not be further proxied // 如果工廠Bean已經是一個Scope代理Bean,則不需要再增強 // 因為它已經能夠滿足FactoryBean延遲初始化Bean了~ } // 繼續增強 else { return enhanceFactoryBean(factoryBean, beanMethod.getReturnType(), beanFactory, beanName); } } // 檢查給定的方法是否與當前呼叫的容器相對應工廠方法。 // 比較方法名稱和引數列表來確定是否是同一個方法 // 怎麼理解這句話,參照下面詳解吧 if (isCurrentlyInvokedFactoryMethod(beanMethod)) { // 這是個小細節:若你@Bean返回的是BeanFactoryPostProcessor型別 // 請你使用static靜態方法,否則會列印這句日誌的~~~~ // 因為如果是非靜態方法,部分後置處理失效處理不到你,可能對你程式有影像 // 當然也可能沒影響,所以官方也只是建議而已~~~ if (logger.isInfoEnabled() && BeanFactoryPostProcessor.class.isAssignableFrom(beanMethod.getReturnType())) { ... // 輸出info日誌 } // 這表示:當前parent()方法,就是這個被攔截的方法,那就沒啥好說的 // 相當於在代理代理類裡執行了super(xxx); // 但是,但是,但是,此時的this依舊是代理類 return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs); } // parent()方法裡呼叫的son()方法會交給這裡來執行 return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName); } ``` 步驟總結: 1. 拿到當前BeanFactory工廠物件。該工廠物件通過第一個攔截器`BeanFactoryAwareMethodInterceptor`已經完成了設值 2. 確定Bean名稱。**預設是方法名**,若通過@Bean指定了以指定的為準,若指定了多個值以第一個值為準,後面的值當作Bean的alias別名 3. 判斷當前方法(以parent()方法為例)是否是個Scope域代理。也就是方法上是否標註有`@Scope`註解 1. 若是域代理類,那舊以它的方式來處理嘍。beanName的變化變化為`scopedTarget.parent` 2. 判斷`scopedTarget.parent`這個Bean是否正在建立中...若是的,那就把當前beanName替換為`scopedTarget.parent`,以後就關注這個名稱的Bean了~ 3. 試想一下,如果不來這個判斷的話,那最終可能的結果是:容器內一個名為parent的Bean,一個名字為`scopedTarget.parent`的Bean,那豈不又出問題了麼~ 4. 判斷請求的Bean是否是個FactoryBean工廠Bean。 1. 若是工廠Bean,那麼就需要enhance增強這個Bean,以攔截它的getObject()方法 2. 攔截`getObject()`的做法是:當執行`getObject()`方法時轉為 -> `getBean()`方法 3. 為什麼需要這麼做:是為了確保FactoryBean產生的例項是通過getBean()容器去獲取的,而非又自己建立一個出來了 4. **這種case先打個❓,下面會結合程式碼示例加以說明** 5. 判斷這個beanMethod是否是**當前正在被呼叫的工廠方法**。 1. 若是正在建立的方法,那就好說了,直接`super(xxx)`執行父類方法體完事~ 2. 若不是正在建立的方法,那就需要代理嘍,以確保實際呼叫的仍舊是實際呼叫`getBean`方法而保證是同一個Bean 3. **這種case先打個❓,下面會結合程式碼示例加以說明**。因為這個case是最常見的主線case,所以先把它搞定 這是該攔截器的執行步驟,留下兩個打❓下面我來一一解釋(按照倒序)。 --- #### 多次呼叫@Bean方法為何不會產生新例項? **這是最為常見的case**。示例程式碼: ```java @Configuration public class AppConfig { @Bean public Son son() { Son son = new Son(); System.out.println("son created..." + son.hashCode()); return son; } @Bean public Parent parent() { notBeanMethod(); Son son = son(); System.out.println("parent created...持有的Son是:" + son.hashCode()); return new Parent(son); } public void notBeanMethod(){ System.out.println("notBeanMethod invoked by 【" + this + "】"); } } ``` 本配置類一共有**三個**方法: - **son()**:標註有@Bean。 ![](https://img-blog.csdnimg.cn/20200520173703329.png) 因此它最終交給`cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);`方法直接執行父類(也就是目標類)的方法體: ![](https://img-blog.csdnimg.cn/20200520174102191.png) 值得注意的是:**此時所處的物件仍舊是代理物件內**,這個方法體只是通過代理類呼叫了`super(xxx)`方法進來的而已嘛~ - **parent()**:標註有@Bean。它內部會還會呼叫notBeanMethod()和son()兩個方法 同上,會走到目標類的方法體裡,開始呼叫 **notBeanMethod()和son()** 這兩個方法,這個時候處理的方式就不一樣了: 1. 呼叫`notBeanMethod()`方法,因為它沒有標註@Bean註解,所以不會被攔截 -> 直接執行方法體 2. 呼叫`son()`方法,因為它標註有@Bean註解,所以會繼續進入到攔截器裡。但請注意和上面 **直接呼叫** son()方法不一樣的是:**此時當前正在被invoked的方法是parent()方法,而並非son()方法**,所以他會被交給`resolveBeanReference()`方法來處理: ```java BeanMethodInterceptor: private Object resolveBeanReference(Method beanMethod, Object[] beanMethodArgs, ConfigurableBeanFactory beanFactory, String beanName) { // 當前bean(son這個Bean)是否正在建立中... 本處為false嘛 // 這個判斷主要是為了防止後面getBean報錯~~~ boolean alreadyInCreation = beanFactory.isCurrentlyInCreation(beanName); try { // 如果該Bean確實正在建立中,先把它標記下,放置後面getBean報錯~ if (alreadyInCreation) { beanFactory.setCurrentlyInCreation(beanName, false); } // 更具該方法的入參,決定後面使用getBean(beanName)還是getBean(beanName,args) // 基本原則是:但凡只要有一個入參為null,就呼叫getBean(beanName) boolean useArgs = !ObjectUtils.isEmpty(beanMethodArgs); if (useArgs && beanFactory.isSingleton(beanName)) { for (Object arg : beanMethodArgs) { if (arg == null) { useArgs = false; break; } } } // 通過getBean從容器中拿到這個例項 本處拿出的就是Son例項嘍 Object beanInstance = (useArgs ? beanFactory.getBean(beanName, beanMethodArgs) : beanFactory.getBean(beanName)); // 方法返回型別和Bean實際型別做個比較,因為有可能型別不一樣 // 什麼時候會出現型別不一樣呢?當BeanDefinition定義資訊型別被覆蓋的時候,就可能出現此現象 if (!ClassUtils.isAssignableValue(beanMethod.getReturnType(), beanInstance)) { if (beanInstance.equals(null)) { beanInstance = null; } else { ... throw new IllegalStateException(msg); } } // 當前被呼叫的方法,是parent()方法 Method currentlyInvoked = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod(); if (currentlyInvoked != null) { String outerBeanName = BeanAnnotationHelper.determineBeanNameFor(currentlyInvoked); // 這一步是註冊依賴關係,告訴容器: // parent例項的初始化依賴於son例項 beanFactory.registerDependentBean(beanName, outerBeanName); } // 返回例項 return beanInstance; } // 歸還標記:筆記實際確實還在建立中嘛~~~~ finally { if (alreadyInCreation) { beanFactory.setCurrentlyInCreation(beanName, true); } } } ``` 這麼一來,執行完parent()方法體裡的son()方法後,**實際得到的是容器內的例項**,從而保證了我們這麼寫是不會有問題的。 - **notBeanMethod()**:因為沒有標註@Bean,所以它並不會被容器呼叫,而只能是被上面的`parent()`方法呼叫到,並且也不會被攔截(值得注意的是:因為此方法不需要被代理,所以此方法可以是`private final`的哦~) 以上程式的執行結果是: ```java son created...347978868 notBeanMethod invoked by 【com.yourbatman.fullliteconfig.config.AppConfig$$EnhancerBySpringCGLIB$$ec611337@12591ac8】 parent created...持有的Son是:347978868 com.yourbatman.fullliteconfig.config.AppConfig$$EnhancerBySpringCGLIB$$ec611337@12591ac8 容器內的Son例項:347978868 容器內Person持有的Son例項:347978868 true ``` 可以看到,**Son自始至終都只存在一個例項**,這是符合我們的預期的。 --- ##### Lite模式下表現如何? 同樣的程式碼,在Lite模式下(去掉@Configuration註解即可),不存在“如此複雜”的代理邏輯,所以上例的執行結果是: ```java son created...624271064 notBeanMethod invoked by 【com.yourbatman.fullliteconfig.config.AppConfig@21a947fe】 son created...90205195 parent created...持有的Son是:90205195 com.yourbatman.fullliteconfig.config.AppConfig@21a947fe 容器內的Son例項:624271064 容器內Person持有的Son例項:90205195 false ``` 這個結果很好理解,這裡我就不再囉嗦了。總之就不能這麼用就對了~ --- #### FactoryBean模式剖析 `FactoryBean`也是向容器提供Bean的一種方式,如最常見的`SqlSessionFactoryBean`就是這麼一個大代表,因為它比較常用,並且這裡也作為此攔截器一個**單獨的執行分支**,所以很有必要研究一番。 執行此分支邏輯的條件是:容器內已經存在`&beanName`和`beanName`兩個Bean。執行的方式是:使用`enhanceFactoryBean()`方法對`FactoryBean`進行增強。 ```java ConfigurationClassEnhancer: // 建立一個子類代理,攔截對getObject()的呼叫,委託給當前的BeanFactory // 而不是建立一個新的例項。這些代理僅在呼叫FactoryBean時建立 // factoryBean:從容器內拿出來的那個已經存在的工廠Bean例項(是工廠Bean例項) // exposedType:@Bean標註的方法的返回值型別 private Object enhanceFactoryBean(Object factoryBean, Class exposedType, ConfigurableBeanFactory beanFactory, String beanName) { try { // 看看Spring容器內已經存在的這個工廠Bean的情況,看看是否有final Class clazz = factoryBean.getClass(); boolean finalClass = Modifier.isFinal(clazz.getModifiers()); boolean finalMethod = Modifier.isFinal(clazz.getMethod("getObject").getModifiers()); // 類和方法其中有一個是final,那就只能看看能不能走介面代理嘍 if (finalClass || finalMethod) { // @Bean標註的方法返回值若是介面型別 嘗試走基於介面的JDK動態代理 if (exposedType.isInterface()) { // 基於JDK的動態代理 return createInterfaceProxyForFactoryBean(factoryBean, exposedType, beanFactory, beanName); } else { // 類或方法存在final情況,但是呢返回型別又不是 return factoryBean; } } } catch (NoSuchMethodException ex) { // 沒有getObject()方法 很明顯,一般不會走到這裡 } // 到這,說明以上條件不滿足:存在final且還不是介面型別 // 類和方法都不是final,生成一個CGLIB的動態代理 return createCglibProxyForFactoryBean(factoryBean, beanFactory, beanName); } ``` 步驟總結: 1. 拿到容器內已經存在的這個工廠Bean的型別,看看類上、getObject()方法是否用final修飾了 2. 但凡只需**有一個**被final修飾了,那註定不能使用CGLIB代理了嘍,那麼就嘗試使用基於介面的JDK動態代理: 1. 若你標註的@Bean返回的是介面型別(也就是`FactoryBean`型別),那就ok,使用JDK建立個代理物件返回 2. 若不是介面(有final又還不是介面),那老衲無能為力了:原樣return返回 3. 若以上條件不滿足,表示一個final都木有,那就統一使用CGLIB去生成一個代理子類。大多數情況下,都會走到這個分支上,代理是通過CGLIB生成的 > 說明:無論是JDK動態代理還是CGLIB的代理實現均非常簡單,就是把getObject()方法代理為使用`beanFactory.getBean(beanName)`去獲取例項(要不代理掉的話,每次不就執行你getObject()裡面的邏輯了麼,就又會建立新例項啦~) **需要明確**,此攔截器對FactoryBean邏輯處理分支的目的是:確保你**通過方法呼叫**拿到`FactoryBean`後,再呼叫其`getObject()`方法(哪怕呼叫多次)得到的都是同一個示例(容器內的單例)。因此需要對`getObject()`方法做攔截嘛,讓該方法指向到`getBean()`,永遠從容器裡面拿即可。 > 這個攔截處理邏輯只有在@Bean方法呼叫時才有意義,比如parent()裡呼叫了son()這樣子才會起到作用,否則你就忽略它吧~ 針對於此,下面給出不同case下的程式碼示例,加強理解。 --- ##### 程式碼示例(重要) 準備一個`SonFactoryBean`用於產生Son例項: ```java public class SonFactoryBean implements FactoryBean { @Override public Son getObject() throws Exception { return new Son(); } @Override public Class getObjectType() { return Son.class; } } ``` 並且在配置類裡把它放好: ```java @Configuration public class AppConfig { @Bean public FactoryBean son() { SonFactoryBean sonFactoryBean = new SonFactoryBean(); System.out.println("我使用@Bean定義sonFactoryBean:" + sonFactoryBean.hashCode()); System.out.println("我使用@Bean定義sonFactoryBean identityHashCode:" + System.identityHashCode(sonFactoryBean)); return sonFactoryBean; } @Bean public Parent parent(Son son) throws Exception { // 根據前面所學,sonFactoryBean肯定是去容器拿 FactoryBean sonFactoryBean = son(); System.out.println("parent流程使用的sonFactoryBean:" + sonFactoryBean.hashCode()); System.out.println("parent流程使用的sonFactoryBean identityHashCode:" + System.identityHashCode(sonFactoryBean)); System.out.println("parent流程使用的sonFactoryBean:" + sonFactoryBean.getClass()); // 雖然sonFactoryBean是從容器拿的,但是getObject()你可不能保證每次都返回單例哦~ Son sonFromFactory1 = sonFactoryBean.getObject(); Son sonFromFactory2 = sonFactoryBean.getObject(); System.out.println("parent流程使用的sonFromFactory1:" + sonFromFactory1.hashCode()); System.out.println("parent流程使用的sonFromFactory1:" + sonFromFactory2.hashCode()); System.out.println("parent流程使用的son和容器內的son是否相等:" + (son == sonFromFactory1)); return new Parent(sonFromFactory1); } } ``` 執行程式: ```java @Bean public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); SonFactoryBean sonFactoryBean = context.getBean("&son", SonFactoryBean.class); System.out.println("Spring容器內的SonFactoryBean:" + sonFactoryBean.hashCode()); System.out.println("Spring容器內的SonFactoryBean:" + System.identityHashCode(sonFactoryBean)); System.out.println("Spring容器內的SonFactoryBean:" + sonFactoryBean.getClass()); System.out.println("Spring容器內的Son:" + context.getBean("son").hashCode()); } ``` 輸出結果: ```java 我使用@Bean定義sonFactoryBean:313540687 我使用@Bean定義sonFactoryBean identityHashCode:313540687 parent流程使用的sonFactoryBean:313540687 parent流程使用的sonFactoryBean identityHashCode:70807318 parent流程使用的sonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean$$EnhancerBySpringCGLIB$$1ccec41d parent流程使用的sonFromFactory1:910091170 parent流程使用的sonFromFactory1:910091170 parent流程使用的son和容器內的son是否相等:true Spring容器內的SonFactoryBean:313540687 Spring容器內的SonFactoryBean:313540687 Spring容器內的SonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean Spring容器內的Son:910091170 ``` 結果分析: ![](https://img-blog.csdnimg.cn/20200521075138785.png) 達到了預期的效果:parent在呼叫son()方法時,得到的是在容器內已經存在的`SonFactoryBean`基礎上CGLIB位元組碼`提升過的例項`,**攔截成功**,從而getObject()也就實際是去容器裡拿物件的。 通過本例有如下小細節需要指出: 1. 原始物件和代理/增強後(不管是CGLIB還是JDK動態代理)的例項的`.hashCode()`以及`.equals()`方法是一毛一樣的,但是`identityHashCode()`值(實際記憶體值)不一樣哦,因為是不同型別、不同例項,這點請務必注意 2. 最終存在於容器內的仍舊是原生工廠Bean物件,而非代理後的工廠Bean例項。畢竟攔截器只是攔截了@Bean方法的呼叫來了個“偷天換日”而已~ 3. 若`SonFactoryBean`上加個final關鍵字修飾,根據上面講述的邏輯,那代理物件會使用JDK動態代理生成嘍,形如這樣(本處僅作為示例,實際使用中請別這麼幹): ```java public final class SonFactoryBean implements FactoryBean { ... } ``` 再次執行程式,結果輸出為:執行的結果一樣,只是代理方式不一樣而已。從這個小細節你也能看出來Spring對代理實現上的偏向:**優先選擇CGLIB代理方式,JDK動態代理方式用於兜底**。 ```java ... // 使用了JDK的動態代理 parent流程使用的sonFactoryBean:class com.sun.proxy.$Proxy11 ... ``` > 提示:若你標註了final關鍵字了,那麼請保證@Bean方法返回的是`FactoryBean`介面,而不能是`SonFactoryBean`實現類,否則最終無法代理了,原樣輸出。因為JDK動態代理和CGLIB都搞不定了嘛~ --- 在以上例子的基礎上,我給它“加點料”,再看看效果呢: 使用`BeanDefinitionRegistryPostProcessor`提前就放進去一個名為son的例項: ```java // 這兩種方式向容器扔bd or singleton bean都行 我就選擇第二種嘍 // 注意:此處放進去的是BeanFactory工廠,名稱是son哦~~~ 不要寫成了&son @Component public class SonBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { // registry.registerBeanDefinition("son", BeanDefinitionBuilder.rootBeanDefinition(SonFactoryBean.class).getBeanDefinition()); } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { SonFactoryBean sonFactoryBean = new SonFactoryBean(); System.out.println("初始化時,註冊進容器的sonFactoryBean:" + sonFactoryBean); beanFactory.registerSingleton("son", sonFactoryBean); } } ``` 再次執行程式,輸出結果: ```java 初始化時最早進容器的sonFactoryBean:2027775614 初始化時最早進容器的sonFactoryBean identityHashCode:2027775614 parent流程使用的sonFactoryBean:2027775614 parent流程使用的sonFactoryBean identityHashCode:1183888521 parent流程使用的sonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean$$EnhancerBySpringCGLIB$$1ccec41d parent流程使用的sonFromFactory1:2041605291 parent流程使用的sonFromFactory1:2041605291 parent流程使用的son和容器內的son是否相等:true Spring容器內的SonFactoryBean:2027775614 Spring容器內的SonFactoryBean:2027775614 Spring容器內的SonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean Spring容器內的Son:2041605291 ``` **效果上並不差異**,從日誌上可以看到:你配置類上使用@Bean標註的son()**方法體**並沒執行了,而是使用的最開始註冊進去的例項,差異僅此而已。 > 為何是這樣的現象?這就不屬於本文的內容了,是Spring容器對Bean的例項化、初始化邏輯,本公眾號後面依舊會採用專欄式講解,讓你徹底弄懂它。當前有興趣的可以先自行參考`DefaultListableBeanFactory#preInstantiateSingletons`的內容~ --- ##### Lite模式下表現如何? Lite模式下可沒這些“加強特性”,所以在Lite模式下(拿掉`@Configuration`這個註解便可)執行以上程式,結果輸出為: ```java 我使用@Bean定義sonFactoryBean:477289012 我使用@Bean定義sonFactoryBean identityHashCode:477289012 我使用@Bean定義sonFactoryBean:2008966511 我使用@Bean定義sonFactoryBean identityHashCode:2008966511 parent流程使用的sonFactoryBean:2008966511 parent流程使用的sonFactoryBean identityHashCode:2008966511 parent流程使用的sonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean parent流程使用的sonFromFactory1:433874882 parent流程使用的sonFromFactory1:572191680 parent流程使用的son和容器內的son是否相等:false Spring容器內的SonFactoryBean:477289012 Spring容器內的SonFactoryBean:477289012 Spring容器內的SonFactoryBean:class com.yourbatman.fullliteconfig.config.SonFactoryBean Spring容器內的Son:211968962 ``` 結果解釋我就不再囉嗦,有了前面的基礎就太容易理解了。 --- ##### 為何是@Scope域代理就不用處理? 要解釋好這個原因,和`@Scope`代理方式的原理知識強相關。限於篇幅,本文就先賣個關子~ 關於`@Scope`我個人覺得足夠用5篇以上文章專題講解,雖然在`Spring Framework`裡使用得比較少,但是在理解`Spirng Cloud`的自定義擴充套件實現上顯得非常非常有必要,所以你可關注我公眾號,會近期推出相關專欄的。 --- # 總結 關於Spring配置類這個專欄內容,講解到這就完成99%了,毫不客氣的說關於此部分知識真正可以實現“橫掃千軍”,據我瞭解沒有解決不了的問題了。 當然還剩下1%,那自然是缺少一篇總結篇嘍:在下一篇總結篇裡,我會用**圖文並茂**的方式對Spring配置類相關內容的執行流程進行總結,目的是讓你**快速掌握**,應付面試嘛。 **本文將近2萬字,手真的很累**,如果對你有幫助,幫點個在看哈。最主要的是:關注我的公眾號,後期推出的專欄都會很精彩......