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

當大潮退去,才知道誰在裸泳。
作者:A哥(YourBatman)
公衆號:BAT的烏托邦(ID:BAT-utopia)
文末是否有彩蛋:有

各位小夥伴大家好,我是A哥。本文對Spring @Configuration配置類繼續進階,雖然有點燒腦,但目的只有一個:爲拿高薪備好彈藥。如果說上篇文章已經腦力有點“不適”了,那這裏得先給你個下馬威:本篇文章內容將更加的讓你“感覺不適”。

讀本文之前,爲確保連貫性,建議你移步先閱讀上篇文章內容,直達電梯:你自我介紹說很懂Spring配置類,那你怎麼解釋這個現象?

爲什麼有些時候我會建議先閱讀上篇文章,這確實是無奈之舉。技術的內容一般都具有很強相關性,它是需要有Context上下文支撐的,所以花幾分鐘先了解相關內容效果更佳,磨刀不誤砍柴工的道理大家都懂。同時呢,這也是寫深度分析類的技術文章的尷尬之處:吃力反而不討好,需要堅持。
在這裏插入圖片描述


版本約定

本文內容若沒做特殊說明,均基於以下版本:

  • JDK:1.8
  • Spring Framework:5.2.2.RELEASE

正文

上篇文章介紹了代理對象兩個攔截器其中的前者,即BeanFactoryAwareMethodInterceptor,它會攔截setBeanFactory()方法從而完成給代理類指定屬性賦值。通過第一個攔截器的講解,你能夠成功“忽悠”很多面試官了,但仍舊不能夠解釋我們最常使用中的這個疑惑:爲何通過調用@Bean方法最終指向的仍舊是同一個Bean呢?

帶着這個疑問,開始本文的陳訴。請繫好安全帶,準備發車了…
在這裏插入圖片描述


Spring配置類的使用誤區

根據不同的配置方式,展示不同情況。從Lite模式的使用產生誤區,到使用Full模式解決問題,最後引出解釋爲何有此效果的原因分析/源碼解析。


Lite模式:錯誤姿勢

配置類:

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);
    }

}

運行程序:

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);
}

運行結果:

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)已經能教你怎麼做了:
在這裏插入圖片描述
按照“指示”,可以使用依賴注入的方式代替從而避免這種問題,如下:

// @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);
}

再次運行程序,結果爲:

son created...624271064
parent created...持有的Son是:624271064
com.yourbatman.fullliteconfig.config.AppConfig@667a738
容器內的Son實例:624271064
容器內Person持有的Son實例:624271064
true

bingo,完美解決了問題。如果你堅持使用Lite模式,那麼請注意它的優缺點哦(Full模式和Lite模式的優缺點見這篇文章)。

沒有仔細看的同學可能會問:我明明就是按照第一種方式寫的,也正常work沒問題呀。說你是不細心吧還真是,不信你再回去瞅瞅對比對比。如果你用第一種方式並且能夠“正常work”,那請你查查類頭上是不是標註有@Configuration註解?
在這裏插入圖片描述


Full模式:

Full模式是容錯性最強的一種方式,你亂造都行,沒啥顧慮。

當然嘍,方法不能是private/final。但一般情況下誰會在配置裏final掉一個方法呢?你說對吧~

@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);
    }

}

運行程序,結果輸出:

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代理。

複雜歸複雜,但沒啥好怕的,一步一步來唄。同樣的,我會按如下兩步去了解它:執行時機 + 做了何事


執行時機

廢話不多說,直接結合源碼解釋。

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()方法的執行爲例,結合源碼進行跟蹤講解:

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。示例代碼:

@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。

在這裏插入圖片描述
因此它最終交給cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);方法直接執行父類(也就是目標類)的方法體:
在這裏插入圖片描述
值得注意的是:此時所處的對象仍舊是代理對象內,這個方法體只是通過代理類調用了super(xxx)方法進來的而已嘛~

  • parent():標註有@Bean。它內部會還會調用notBeanMethod()和son()兩個方法

同上,會走到目標類的方法體裏,開始調用 notBeanMethod()和son() 這兩個方法,這個時候處理的方式就不一樣了:

  1. 調用notBeanMethod()方法,因爲它沒有標註@Bean註解,所以不會被攔截 -> 直接執行方法體
  2. 調用son()方法,因爲它標註有@Bean註解,所以會繼續進入到攔截器裏。但請注意和上面 直接調用 son()方法不一樣的是:此時當前正在被invoked的方法是parent()方法,而並非son()方法,所以他會被交給resolveBeanReference()方法來處理:
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的哦~)

以上程序的運行結果是:

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註解即可),不存在“如此複雜”的代理邏輯,所以上例的運行結果是:

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就是這麼一個大代表,因爲它比較常用,並且這裏也作爲此攔截器一個單獨的執行分支,所以很有必要研究一番。

執行此分支邏輯的條件是:容器內已經存在&beanNamebeanName兩個Bean。執行的方式是:使用enhanceFactoryBean()方法對FactoryBean進行增強。

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實例:

public class SonFactoryBean implements FactoryBean<Son> {
    @Override
    public Son getObject() throws Exception {
        return new Son();
    }

    @Override
    public Class<?> getObjectType() {
        return Son.class;
    }
}

並且在配置類裏把它放好:

@Configuration
public class AppConfig {

    @Bean
    public FactoryBean<Son> 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<Son> 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);
    }

}

運行程序:

@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());
}

輸出結果:

我使用@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

結果分析:
在這裏插入圖片描述
達到了預期的效果:parent在調用son()方法時,得到的是在容器內已經存在的SonFactoryBean基礎上CGLIB字節碼提升過的實例攔截成功,從而getObject()也就實際是去容器裏拿對象的。

通過本例有如下小細節需要指出:

  1. 原始對象和代理/增強後(不管是CGLIB還是JDK動態代理)的實例的.hashCode()以及.equals()方法是一毛一樣的,但是identityHashCode()值(實際內存值)不一樣哦,因爲是不同類型、不同實例,這點請務必注意
  2. 最終存在於容器內的仍舊是原生工廠Bean對象,而非代理後的工廠Bean實例。畢竟攔截器只是攔截了@Bean方法的調用來了個“偷天換日”而已~
  3. SonFactoryBean上加個final關鍵字修飾,根據上面講述的邏輯,那代理對象會使用JDK動態代理生成嘍,形如這樣(本處僅作爲示例,實際使用中請別這麼幹):
public final class SonFactoryBean implements FactoryBean<Son> { ... }

再次運行程序,結果輸出爲:執行的結果一樣,只是代理方式不一樣而已。從這個小細節你也能看出來Spring對代理實現上的偏向:優先選擇CGLIB代理方式,JDK動態代理方式用於兜底

...
// 使用了JDK的動態代理
parent流程使用的sonFactoryBean:class com.sun.proxy.$Proxy11
...

提示:若你標註了final關鍵字了,那麼請保證@Bean方法返回的是FactoryBean接口,而不能是SonFactoryBean實現類,否則最終無法代理了,原樣輸出。因爲JDK動態代理和CGLIB都搞不定了嘛~


在以上例子的基礎上,我給它“加點料”,再看看效果呢:

使用BeanDefinitionRegistryPostProcessor提前就放進去一個名爲son的實例:

// 這兩種方式向容器扔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);
    }
}

再次運行程序,輸出結果:

初始化時最早進容器的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這個註解便可)運行以上程序,結果輸出爲:

我使用@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萬字,手真的很累,如果對你有幫助,幫點個在看哈。最主要的是:關注我的公衆號,後期推出的專欄都會很精彩…


關於A哥

  • 專欄式學習咱們小衆聊,拒絕淺嘗輒止。知識星球誠邀您掃碼入駐(提示:請務必先關注公衆號,回覆 知識星球 領取優惠券後再輕裝入駐)
  • 私人微信,掃碼加A哥好友(備註:“公衆號”),邀你進入 Java高工、架構師 系列純技術羣(或關注公衆號,回覆 加羣 亦可直接加入)
  • 文章在公衆號首發,其它平臺慢1-2天。也可關注A哥的個人博客:https://www.yourbatman.cn

    碼字非常不易,不可以白嫖,點個在看就表示你支持A哥的原創嘍~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章