網(wǎng)站源代碼沒(méi)有tdk(做網(wǎng)站不給源代碼)
作 者 | 楊天逸(在田)
導(dǎo)語(yǔ):本文就Spring配置項(xiàng)解析問(wèn)題展開(kāi)分析,這其中涉及到bean定義注冊(cè)表后置處理、bean工廠后置處理、工廠bean等Spring相關(guān)的概念。本文將以上述問(wèn)題作為切入點(diǎn),進(jìn)行分析和展開(kāi)介紹。
問(wèn)題背景介紹
我們的項(xiàng)目中某次依賴(lài)了某個(gè)第三方包及其中的XML文件,相關(guān)代碼如下所示:XML文件中定義了Mybatis相關(guān)的bean,以及對(duì)自定義數(shù)據(jù)源myDataSource的引用。在@Configuration配置類(lèi)中,我們引入了XML文件,并通過(guò)@Bean注解的方式聲明了數(shù)據(jù)源bean。
beanid= "thirdPartySqlSessionFactory"
class= "org.mybatis.spring.SqlSessionFactoryBean"
depends-on= "myDataSource"
propertyname= "dataSource"ref= "myDataSource"/
propertyname= "mapperLocations"value= "classpath:mybatis/third-party/*.xml"/
/ bean
beanid= "thirdPartyMapperScannerConfigurer"
class= "org.mybatis.spring.mapper.MapperScannerConfigurer"
depends-on= "thirdPartySqlSessionFactory"
展開(kāi)全文
propertyname= "basePackage"value= "com.alibaba.thirdparty.dao"/
propertyname= "sqlSessionFactoryBeanName"value= "thirdPartySqlSessionFactory"/
/ bean
@Configuration
@EnableTransactionManagement(proxyTargetClass = true)
// 引入上述XML文件
@ImportResource( "classpath*:/mybatis-third-party-config.xml")
publicclassMyDataSourceConfiguration{
// 聲明自定義數(shù)據(jù)源
@Bean(name = "myDataSource")
publicDataSource createMyDataSource(Environment env) {
// 返回?cái)?shù)據(jù)源實(shí)例,具體代碼略
}
}
項(xiàng)目啟動(dòng)后,我們發(fā)現(xiàn)一個(gè)原有的通過(guò)XML定義的HSF(HSF全稱(chēng)High-speed Service Framework,是阿里內(nèi)部主要使用的RPC服務(wù)框架)客戶(hù)端bean中的配置項(xiàng)無(wú)法被正常解析。由于這是一個(gè)與我們新引入的包無(wú)關(guān)的bean,大家都對(duì)問(wèn)題產(chǎn)生的原因感到奇怪,也嘗試了各種不同的處理方式,然而都沒(méi)有效果。無(wú)奈之下,我們通過(guò)將整個(gè)XML文件改寫(xiě)為Java注解聲明的形式,才最終解決了問(wèn)題。相關(guān)代碼如下所示:
beanid= "myHsfClient"class= "com.taobao.hsf.app.spring.util.HSFSpringConsumerBean"init-method= "init"
propertyname= "interfaceName"
value com.taobao.custom.MyHsfClient / value
/ property
propertyname= "version"
value ${hsf.client.version} / value
/ property
/ bean
// 改寫(xiě)后的Java注解聲明方式
@Configuration
publicclassMyHsfConfig{
@HSFConsumer(serviceVersion = " ${hsf.client.version}" )
privateMyHsfClient myHsfClient;
// 其余代碼省略
}
雖然問(wèn)題得到了解決,但是大家仍舊對(duì)這其中的原因不明所以。筆者在事后通過(guò)本地調(diào)試的方式,找到了問(wèn)題的原因。這其中涉及到bean定義注冊(cè)表后置處理、bean工廠后置處理、工廠bean等Spring相關(guān)的概念。本文將以上述問(wèn)題作為切入點(diǎn),進(jìn)行分析和展開(kāi)介紹。
XML配置項(xiàng)解析
為了更好地解答上述問(wèn)題產(chǎn)生的原因,我們先來(lái)看下Spring框架對(duì)bean使用的配置項(xiàng)的解析過(guò)程。我們知道,Spring會(huì)負(fù)責(zé)對(duì)我們?cè)赬ML文件中聲明的bean的創(chuàng)建。不過(guò),對(duì)其中的配置項(xiàng)解析,并不是在這個(gè)環(huán)節(jié)發(fā)生,而是在其前置環(huán)節(jié) —— bean工廠后置處理的過(guò)程中發(fā)生的。bean工廠(BeanFactory)是Spring的核心組件,除了負(fù)責(zé)初始化bean的實(shí)例,記錄單例外,它還維護(hù)了各個(gè) bean的定義(BeanDefinition)。bean的定義中主要記錄了bean的類(lèi)型、作用域(singleton/prototype)、屬性值、構(gòu)造函數(shù)參數(shù)值等信息。bean的實(shí)例化便是基于bean的定義進(jìn)行的。而bean工廠的后置處理環(huán)節(jié),則可以在bean被創(chuàng)建之前,修改bean的定義,以達(dá)到影響最終生成的bean實(shí)例的效果。
對(duì)XML中配置項(xiàng)的解析工作,Spring是通過(guò) PropertySourcesPlaceholderConfigurer這個(gè)bean工廠后置處理器(BeanFactoryPostProcessor)完成的。其核心代碼如下所示??傮w思路比較簡(jiǎn)單,即遍歷bean工廠中的bean定義,對(duì)于每個(gè)bean的定義,訪問(wèn)其屬性值、構(gòu)造函數(shù)參數(shù)值等信息,解析其中的配置項(xiàng)占位符(placeholder)。這個(gè)環(huán)節(jié)完成之后,在bean工廠對(duì)bean進(jìn)行初始化之前,bean定義中的配置項(xiàng)占位符就已經(jīng)被替換為實(shí)際的屬性值了。
// 處理屬性值
protectedvoiddoProcessProperties( ConfigurableListableBeanFactory beanFactoryToProcess,
StringValueResolver valueResolver ) {
BeanDefinitionVisitor visitor = newBeanDefinitionVisitor(valueResolver);
String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames;
// 遍歷bean工廠中的bean名稱(chēng)集合
for(String curName : beanNames) {
// 跳過(guò)對(duì)自身的處理
if(!(curName. equals( this.beanName) beanFactoryToProcess. equals( this.beanFactory))) {
// 通過(guò)bean的名稱(chēng)獲取bean的定義
BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName);
try{
// 訪問(wèn)bean的定義,解析并替換其中的配置項(xiàng)占位符
visitor.visitBeanDefinition(bd);
}
catch(Exception ex) {
thrownewBeanDefinitionStoreException(bd.getResourceDeion, curName, ex.getMessage, ex);
}
}
}
// 將配置項(xiàng)解析器注冊(cè)添加至bean工廠,供基于注解的配置項(xiàng)解析處理器使用(后文將詳細(xì)介紹)
// New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes.
beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
// 其余代碼省略
}
了解了bean工廠后置處理環(huán)節(jié)后,讓我們?cè)偻疤骄恳徊?,看下bean定義本身是如何被加載到bean工廠中的(這將有助于我們理解文章開(kāi)頭所提到的問(wèn)題的產(chǎn)生原因)。bean定義主要是在 bean定義注冊(cè)表后置處理環(huán)節(jié)被加載到bean工廠中的。與我們前面提到的bean工廠后置處理環(huán)節(jié)類(lèi)似,該環(huán)節(jié)也存在相應(yīng)的處理器(BeanDefinitionRegistryPostProcessor)完成相關(guān)工作。
其中典型的如 ConfigurationClassPostProcessor。以Spring Boot場(chǎng)景為例,簡(jiǎn)單來(lái)說(shuō),該bean定義注冊(cè)表后置處理器會(huì)從包含了@SpringBootApplication注解的啟動(dòng)引導(dǎo)類(lèi)開(kāi)始,根據(jù)其組合注解@ComponentScan,掃描被@Component,或者組合了@Component的注解(如@Configuration、@Service、@Repository等)標(biāo)注的類(lèi),將這些配置類(lèi)(注1)的bean定義注冊(cè)至bean工廠。同時(shí),處理器還會(huì)根據(jù)組合注解@EnableAutoConfiguration,獲取Spring Boot中的自動(dòng)配置類(lèi)。在這之后,ConfigurationClassPostProcessor會(huì)嘗試解析各個(gè)配置類(lèi)中包含的@Bean、@ImportResource等注解,將對(duì)應(yīng)的bean定義也注冊(cè)到bean工廠中。
最后,對(duì)于配置項(xiàng)本身來(lái)說(shuō),Spring的環(huán)境抽象(Environment)會(huì)拉取并聚合JVM系統(tǒng)屬性、操作系統(tǒng)環(huán)境變量、應(yīng)用屬性配置文件等多個(gè)屬性源的數(shù)據(jù)(注2),以供bean工廠中的bean定義或者bean實(shí)例使用。如前面提到的PropertySourcesPlaceholderConfigurer處理器,便是從Spring環(huán)境中獲取bean定義中的配置項(xiàng)占位符所對(duì)應(yīng)的屬性值,并將其替換的。上文通過(guò)倒序的方式介紹了配置項(xiàng)解析的相關(guān)環(huán)節(jié),下面我們用順序表示的流程圖作結(jié),以便讀者更好地理解。
問(wèn)題原因分析
現(xiàn)在,我們可以對(duì)文章開(kāi)頭提到的問(wèn)題作進(jìn)一步分析了。仔細(xì)查看我們所引入的XML文件可以發(fā)現(xiàn),其中包含一個(gè)類(lèi)型為 MapperScannerConfigurer的bean聲明。Spring借助該類(lèi)完成對(duì)標(biāo)注有@Mapper注解的MyBatis映射接口的掃描。MapperScannerConfigurer實(shí)現(xiàn)了BeanDefinitionRegistryPostProcessor接口,是一個(gè)bean定義注冊(cè)表后置處理器。它對(duì)映射接口的掃描及其對(duì)應(yīng)的bean定義的注冊(cè),便是在該環(huán)節(jié)進(jìn)行的。
前面我們提到, ConfigurationClassPostProcessor這個(gè)bean定義注冊(cè)表后置處理器會(huì)掃描并加載@Configuration和@ImportResource注解相關(guān)的bean定義。我們所引入的XML文件中的bean的定義,便是通過(guò)這個(gè)動(dòng)作被注冊(cè)到bean工廠中的(見(jiàn)上文MyDataSourceConfiguration配置類(lèi))。在ConfigurationClassPostProcessor完成其掃描及加載工作后,由于有新的bean定義被注冊(cè),Spring會(huì)再次嘗試從bean工廠中找出并初始化其他的bean定義注冊(cè)表后置處理器,以觸發(fā)它們的處理動(dòng)作。MapperScannerConfigurer便是在此時(shí)被實(shí)例化并觸發(fā)的。
觀察問(wèn)題背景介紹章節(jié)中的相關(guān)代碼可以發(fā)現(xiàn),MapperScannerConfigurer的bean實(shí)例(thirdPartyMapperScannerConfigurer) 間接依賴(lài)了我們通過(guò)@Bean注解在配置類(lèi)中聲明的數(shù)據(jù)源bean實(shí)例(myDataSource)。因此,在本文案例中,Spring在創(chuàng)建MapperScannerConfigurer實(shí)例時(shí),會(huì)首先對(duì)數(shù)據(jù)源bean進(jìn)行初始化。而對(duì)于通過(guò)@Bean注解聲明的bean,Spring是通過(guò)反射調(diào)用注解所在的工廠方法(factory method),完成bean的實(shí)例化的。我們的數(shù)據(jù)源myDataSource的實(shí)例化,便是通過(guò)反射調(diào)用其工廠方法createMyDataSource完成的。由于該方法包含了一個(gè)類(lèi)型為Environment入?yún)?,Spring需要遍歷bean工廠中的bean定義,找到并創(chuàng)建匹配的bean,作為反射調(diào)用時(shí)的方法傳參。
而問(wèn)題恰恰就出現(xiàn)在這里的 參數(shù)匹配環(huán)節(jié)。Spring在進(jìn)行方法入?yún)⑵ヅ鋾r(shí),會(huì)首先調(diào)用getBeanNamesForType方法,將符合參數(shù)類(lèi)型的bean的名稱(chēng)找出來(lái),然后依據(jù)一定的策略(注3)將bean進(jìn)行實(shí)例化,作為方法入?yún)⑹褂谩?duì)于普通的bean來(lái)說(shuō),Spring只需要依據(jù)bean定義中包含的bean類(lèi)型信息,與參數(shù)類(lèi)型作匹配即可;而對(duì)于另一類(lèi)較為特殊的工廠bean(FactoryBean)來(lái)說(shuō),其類(lèi)型推斷方式就會(huì)更加復(fù)雜些。下文將會(huì)展開(kāi)介紹工廠bean的概念和案例,對(duì)此不太熟悉的讀者,這里只需要了解,工廠bean的作用是負(fù)責(zé)產(chǎn)生某個(gè)我們最終實(shí)際需要使用的bean。因此,在進(jìn)行參數(shù)匹配時(shí),Spring關(guān)心的是這個(gè)最終產(chǎn)生的bean的類(lèi)型,而不是工廠bean本身的類(lèi)型。
在判斷工廠bean實(shí)際輸出的bean的類(lèi)型時(shí)(注4),Spring首先會(huì)嘗試根據(jù)工廠bean定義中的某些元數(shù)據(jù)進(jìn)行類(lèi)型推斷;其次會(huì)嘗試對(duì)工廠bean進(jìn)行一次簡(jiǎn)單創(chuàng)建后,通過(guò)其getObjectType方法獲取目標(biāo)bean的類(lèi)型。如果前兩種嘗試都失敗了,則會(huì)使用 兜底邏輯 —— 對(duì)工廠bean進(jìn)行正式創(chuàng)建后,再通過(guò)getObjectType獲取類(lèi)型信息。這里的「正式創(chuàng)建」,我們可以理解為Spring完成了工廠bean的實(shí)例化、屬性字段的賦值、單例信息的記錄等;而「簡(jiǎn)單創(chuàng)建」僅僅指工廠bean的實(shí)例化,不包括后續(xù)的字段初始化等動(dòng)作。
而我們?cè)谏衔奶岬降膍yHsfClient,便是被聲明為了一個(gè)類(lèi)型為HSFSpringConsumerBean的工廠bean。Spring在對(duì)createMyDataSource的方法入?yún)⑦M(jìn)行類(lèi)型匹配時(shí),由于前述的前兩種類(lèi)型推斷方式都沒(méi)有成功(其具體原因?qū)⒃诤笪墓Sbean小節(jié)中介紹),導(dǎo)致該工廠bean最終被「提前」正式創(chuàng)建了出來(lái)。讀者可能已經(jīng)發(fā)現(xiàn),此時(shí)Spring正處在 bean定義注冊(cè)表后置處理環(huán)節(jié)。而我們?cè)赬ML配置項(xiàng)解析章節(jié)中提到的對(duì)bean定義中的配置項(xiàng)占位符的解析替換,則是在該環(huán)節(jié)之后的 bean工廠后置處理環(huán)節(jié)進(jìn)行的 —— 這就是導(dǎo)致myHsfClient這個(gè)工廠bean中的配置項(xiàng)沒(méi)有被正常解析的原因。整體方法調(diào)用關(guān)系如下圖所示:
至此可能讀者會(huì)有疑問(wèn):難道我們的項(xiàng)目中之前沒(méi)有對(duì)@Mapper映射器接口的掃描動(dòng)作嗎?答案是有掃描動(dòng)作,不過(guò)是通過(guò)MapperScannerRegistrar這個(gè)bean定義注冊(cè)器觸發(fā)的。而由于其與我們通過(guò)XML所引入的MapperScannerConfigurer的一些細(xì)微區(qū)別,使得項(xiàng)目中原先不存在工廠bean被提前創(chuàng)建的問(wèn)題。由于篇幅所限,這里不再對(duì)MapperScannerRegistrar作展開(kāi)介紹。
知道了問(wèn)題背后的原因后,尋找對(duì)應(yīng)的解法也就相對(duì)簡(jiǎn)單了。對(duì)于文中案例,一方面,我們可以看到,由于thirdPartyMapperScannerConfigurer依賴(lài)了SqlSessionFactoryBean實(shí)例(這就是我們剛剛說(shuō)的「細(xì)微區(qū)別」所在),導(dǎo)致其間接依賴(lài)了myDataSource。而考察源碼可以發(fā)現(xiàn),其實(shí)MapperScannerConfigurer只需要SqlSessionFactory的bean名稱(chēng)(sqlSessionFactoryBeanName)作為輸入即可,因此我們可以把XML中相關(guān)的depends-on聲明去除。另一方面,由于createMyDataSource方法入?yún)⑹荢pring環(huán)境抽象,我們可以改由通過(guò)使配置類(lèi)實(shí)現(xiàn)EnvironmentAware接口的方式,獲得應(yīng)用上下文中的Environment實(shí)例。這兩種方法都能解決我們的工廠bean被提前創(chuàng)建的問(wèn)題。
在更一般化的場(chǎng)景中,如果在Spring啟動(dòng)的早期階段,對(duì)某個(gè)bean的依賴(lài)注入無(wú)法避免,我們可以使相關(guān)的類(lèi)實(shí)現(xiàn) ApplicationContextAware接口,嘗試通過(guò)應(yīng)用上下文(ApplicationContext)的getBean方法獲取我們想要的對(duì)象。不過(guò)需要注意的是,getBean方法存在兩類(lèi)版本:根據(jù)bean名稱(chēng)獲取實(shí)例,或是根據(jù)指定類(lèi)型獲取實(shí)例;而如果我們選擇根據(jù)指定類(lèi)型獲取實(shí)例,則仍舊會(huì)觸發(fā)上文提到的類(lèi)型匹配機(jī)制,導(dǎo)致某些無(wú)法通過(guò)正常方式進(jìn)行類(lèi)型推斷的工廠bean被提前創(chuàng)建出來(lái)。最后,對(duì)于前文提到的,在使用注解形式改寫(xiě)myHsfClient的bean聲明后,問(wèn)題得到解決的原因,我們將在后文分析介紹。
一些引申擴(kuò)展
經(jīng)過(guò)上文讓人感覺(jué)有些繞的分析,我們可以看到,文章開(kāi)頭所提到的問(wèn)題的本質(zhì)是,某些bean被Spring提前正式創(chuàng)建了出來(lái),導(dǎo)致其bean聲明中的配置項(xiàng)占位符沒(méi)有來(lái)得及被解析和替換。這其中涉及到不少概念,諸如bean定義注冊(cè)表后置處理、bean工廠后置處理、工廠bean等。由于我們?cè)谌粘i_(kāi)發(fā)中一般接觸得不多,讀者對(duì)它們的理解可能還比較模糊,下文將嘗試結(jié)合實(shí)際案例,進(jìn)行一些引申和擴(kuò)展介紹。
bean定義注冊(cè)表后置處理
我們?cè)谇拔闹幸呀?jīng)介紹了ConfigurationClassPostProcessor和MapperScannerConfigurer這兩個(gè)bean定義注冊(cè)表后置處理器。這類(lèi)處理器的主要作用便是掃描并向bean工廠中注冊(cè)bean定義。其中, ConfigurationClassPostProcessor負(fù)責(zé)掃描配置類(lèi),處理其包含的注解,并將相關(guān)的bean定義注冊(cè)至bean工廠中。隨后,對(duì)于這些新增的bean定義,如果其中又包含了其他的bean定義注冊(cè)表后置處理器,Spring會(huì)將它們實(shí)例化,并觸發(fā)它們的處理動(dòng)作(注5),繼續(xù)注冊(cè)可能被發(fā)現(xiàn)的新的bean定義……如此循環(huán)往復(fù),直到所有該類(lèi)型的處理器都被觸發(fā),完成bean定義的注冊(cè)為止。如以下代碼所示:
publicstaticvoidinvokeBeanFactoryPostProcessors(
ConfigurableListableBeanFactory beanFactory, ListBeanFactoryPostProcessor beanFactoryPostProcessors ) {
// 部分代碼省略
// Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear.
boolean reiterate = true;
while(reiterate) {
reiterate = false;
// 從bean工廠中找出bean定義注冊(cè)表后置處理器
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for(String ppName : postProcessorNames) {
// 如果當(dāng)前處理器尚未被觸發(fā)過(guò)
if(!processedBeans.contains(ppName)) {
// 初始化處理器,并加入到本次需要觸發(fā)的處理器集合中
currentRegistryProcessors. add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
// 標(biāo)記處理器為已被處理
processedBeans. add(ppName);
// 繼續(xù)循環(huán),因?yàn)楫?dāng)前集合中的處理器被觸發(fā)后,可能會(huì)引入新的bean定義,其中可能包含新的bean定義注冊(cè)表后置處理器需要被觸發(fā)
reiterate = true;
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
// 觸發(fā)集合中的處理器的bean定義注冊(cè)表后置處理動(dòng)作
// * 本文案例中,我們?cè)诘谌絏ML文件中引入的MapperScannerConfigurer,便是在此時(shí)被觸發(fā)的
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear;
}
}
回到我們的問(wèn)題案例, MapperScannerConfigurer便是在上述環(huán)節(jié)被創(chuàng)建出來(lái)并觸發(fā)的。這里,細(xì)心的讀者可能會(huì)有疑問(wèn):如果我們?cè)谑褂肵ML聲明這個(gè)Mybatis的處理器時(shí),對(duì)其中的某些屬性也使用了配置項(xiàng)占位符,那么Spring在創(chuàng)建它時(shí),是否也會(huì)遇到同樣的解析問(wèn)題?MapperScannerConfigurer的作者顯然是考慮到了這一點(diǎn) —— 處理器被觸發(fā)后,支持首先嘗試對(duì)它的屬性字段進(jìn)行配置項(xiàng)的解析和替換。其具體的實(shí)現(xiàn)方式,是構(gòu)造一個(gè)新的bean工廠,將自身的bean定義注冊(cè)其中,然后借助PropertySourcesPlaceholderConfigurer等處理器,對(duì)這個(gè)bean工廠執(zhí)行配置項(xiàng)的后置處理操作;最后,用bean定義中的被解析后的屬性值,替換自身實(shí)例中原有的屬性值。這在一定程度上相當(dāng)于模擬了Spring的bean工廠后置處理環(huán)節(jié)。其具體代碼如下:
/*
* BeanDefinitionRegistries are called early in application startup, before
* BeanFactoryPostProcessors. This means that PropertyResourceConfigurers will not have been
* loaded and any property substitution of this class' properties will fail. To avoid this, find
* any PropertyResourceConfigurers defined in the context and run them on this class' bean
* definition. Then update the values.
*/
// 上面這段英文注釋體現(xiàn)了作者的考慮,即文中描述的情況
private voidprocessPropertyPlaceHolders {
// 獲取配置項(xiàng)處理器實(shí)例,即PropertySourcesPlaceholderConfigurer處理器
Map String, PropertyResourceConfigurer prcs = applicationContext.getBeansOfType(PropertyResourceConfigurer. class);
if(!prcs.isEmpty applicationContext instanceof ConfigurableApplicationContext) {
BeanDefinition mapperScannerBean = ((ConfigurableApplicationContext) applicationContext)
.getBeanFactory.getBeanDefinition(beanName);
// 構(gòu)造一個(gè)新的bean工廠
DefaultListableBeanFactory factory= newDefaultListableBeanFactory;
// 將自身的bean定義注冊(cè)到這個(gè)bean工廠中
factory.registerBeanDefinition(beanName, mapperScannerBean);
// * 對(duì)這個(gè)bean工廠執(zhí)行配置項(xiàng)后置處理操作
for(PropertyResourceConfigurer prc : prcs.values) {
prc.postProcessBeanFactory( factory);
}
PropertyValues values = mapperScannerBean.getPropertyValues;
// 使用被解析處理過(guò)的值更新原有的值
this.basePackage = updatePropertyValue( "basePackage", values);
this.sqlSessionFactoryBeanName = updatePropertyValue( "sqlSessionFactoryBeanName", values);
this.sqlSessionTemplateBeanName = updatePropertyValue( "sqlSessionTemplateBeanName", values);
}
}
最后,值得一提的是,對(duì)于ConfigurationClassPostProcessor的bean定義本身,則是在Spring應(yīng)用上下文(ApplicationContext)初始化的過(guò)程中,通過(guò)硬編碼的形式被注冊(cè)到bean工廠中的(注6)。這里同時(shí)被注冊(cè)的還有諸如AutowiredAnnotationBeanPostProcessor等 bean后置處理器,我們將在后文對(duì)此作相應(yīng)介紹。
bean 工廠后置處理
當(dāng)bean定義注冊(cè)表后置處理環(huán)節(jié)完成后,基本上(注7)所有的bean定義都已經(jīng)被注冊(cè)至bean工廠中了。隨后,Spring會(huì)找出所有的bean工廠后置處理器,按照一定的順序?qū)嵗⒂|發(fā)它們的處理動(dòng)作(優(yōu)先執(zhí)行實(shí)現(xiàn)了PriorityOrdered接口的,其次執(zhí)行實(shí)現(xiàn)了Ordered接口的,最后執(zhí)行沒(méi)有實(shí)現(xiàn)前兩個(gè)接口的)。這類(lèi)處理器一般會(huì)遍歷bean工廠中所有的bean定義,執(zhí)行一些特定的操作。我們?cè)谇拔奶岬降腜ropertySourcesPlaceholderConfigurer這個(gè)bean工廠后置處理器,便是在此時(shí)被觸發(fā)的。而在這個(gè)所有bean定義都已經(jīng)準(zhǔn)備就緒的階段,統(tǒng)一進(jìn)行配置項(xiàng)占位符的解析和替換,其時(shí)機(jī)總體上也是恰當(dāng)合理的。
其他的比較典型的Spring內(nèi)置bean工廠后置處理器還有 ConfigurationBeanFactoryMetaData。這個(gè)處理器執(zhí)行的動(dòng)作比較簡(jiǎn)單:它會(huì)遍歷bean工廠中的bean定義,記錄其中的工廠方法等元數(shù)據(jù)信息。其核心代碼如下所示。而這份記錄的作用,我們將在后文說(shuō)明。
public classConfigurationBeanFactoryMetaDataimplementsBeanFactoryPostProcessor{
private Map String, MetaData beans = newHashMap String, MetaData;
public voidpostProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
// 遍歷bean工廠中的bean定義
for( Stringname : beanFactory.getBeanDefinitionNames) {
BeanDefinition definition = beanFactory.getBeanDefinition(name);
Stringmethod = definition.getFactoryMethodName;
Stringbean = definition.getFactoryBeanName;
// 如果存在工廠方法元數(shù)據(jù)(如通過(guò)@Bean注解聲明的bean),則將相關(guān)信息記錄下來(lái)
if(method != null bean != null) {
this.beans.put(name, newMetaData(bean, method));
}
}
}
}
最后,我們來(lái)看另一個(gè)和我們的XML配置項(xiàng)解析問(wèn)題相關(guān)的處理器。在問(wèn)題背景介紹章節(jié)中我們提到,當(dāng)把myHsfClient的bean聲明改寫(xiě)為由@HSFConsumer注解修飾的形式后,問(wèn)題得到了解決。而這背后則是 HsfConsumerPostProcessor這個(gè)bean工廠后置處理器在發(fā)揮作用:對(duì)于每一個(gè)bean定義,如果它的類(lèi)屬性字段上存在@HSFConsumer注解,處理器會(huì)動(dòng)態(tài)生成并注冊(cè)一個(gè)類(lèi)型為HSFSpringConsumerBean的工廠bean定義。雖然由于PropertySourcesPlaceholderConfigurer處理器實(shí)現(xiàn)了PriorityOrdered接口,在此之前已經(jīng)被優(yōu)先執(zhí)行過(guò)了,但是HsfConsumerPostProcessor考慮到了這一點(diǎn) —— 在生成工廠bean定義的過(guò)程中,會(huì)主動(dòng)嘗試解析相關(guān)屬性的配置項(xiàng)占位符,因此規(guī)避了我們?cè)谑褂肵ML方式進(jìn)行工廠bean聲明時(shí)遇到的問(wèn)題。
工廠bean
前面我們提到,HSFSpringConsumerBean是一個(gè)工廠bean。不僅如此,我們?cè)敿?xì)討論的Mybatis的MapperScannerConfigurer處理器,對(duì)于其基于@Mapper注解掃描到的映射接口,也會(huì)將其bean定義改寫(xiě)為 MapperFactoryBean這個(gè)工廠bean類(lèi)型。此外,在Spring中,用于創(chuàng)建Mybatis的SqlSession對(duì)象的SqlSessionFactory,也是由一個(gè)名為 SqlSessionFactoryBean的工廠bean生成的。那么,什么是工廠bean,它的作用又是什么呢?
工廠bean,即 FactoryBean,其基于工廠模式,創(chuàng)建我們最終需要的bean實(shí)例。根據(jù)Spring文檔中的介紹(注8),如果某個(gè)bean的初始化邏輯較為復(fù)雜,不適合使用XML的方式表達(dá),那么我們可以通過(guò)使用工廠bean,以Java語(yǔ)言的方式完成目標(biāo)bean的初始化。工廠bean的概念早在Spring 0.9版本(注9)就已經(jīng)被引入,在Spring框架中的使用是比較普遍的,至今為止僅其自帶的實(shí)現(xiàn)就有50多個(gè)。下面我們就文中的案例展開(kāi)介紹。
// MyBatis配置文件路徑。配置文件包含數(shù)據(jù)源、映射器等信息
Stringresource = "org/mybatis/example/mybatis-config.xml";
// 創(chuàng)建配置文件輸入流
InputStream inputStream = Resources.getResourceAsStream(resource);
// 創(chuàng)建SqlSessionFactory實(shí)例
SqlSessionFactory sqlSessionFactory = newSqlSessionFactoryBuilder.build(inputStream);
// 創(chuàng)建SqlSession實(shí)例
try(SqlSession session = sqlSessionFactory.openSession) {
// 獲取BlogMapper映射器
BlogMapper mapper = session.getMapper(BlogMapper. class);
// 執(zhí)行查詢(xún)語(yǔ)句
Blog blog = mapper.selectBlog( 101);
}
在介紹Mybatis與Spring整合時(shí)使用的兩個(gè)工廠bean之前,我們先來(lái)看下相關(guān)功能單純基于Mybatis本身實(shí)現(xiàn)時(shí)的代碼。代碼片段摘自Mybatis官網(wǎng),如上所示。可以看到,其中SqlSessionFactory實(shí)例是由SqlSessionFactoryBuilder創(chuàng)建的;而用于執(zhí)行查詢(xún)語(yǔ)句的映射器實(shí)例,則是由SqlSession實(shí)例的getMapper方法創(chuàng)建的。與之相對(duì)的,如果閱讀源碼可以發(fā)現(xiàn),在Mybatis-Spring中,用于創(chuàng)建SqlSessionFactory實(shí)例的SqlSessionFactoryBean和映射器實(shí)例的MapperFactoryBean這兩個(gè)工廠bean,在一定程度上可以看作是對(duì)上述代碼封裝和擴(kuò)展。
bean id= "myInputStream"class= "org.apache.ibatis.io.Resources"
factory-method= "getResourceAsStream"
constructor-argvalue= "org/mybatis/example/mybatis-config.xml"/
/ bean
bean id= "mySqlSessionFactoryBuilder"class= "org.apache.ibatis.session.SqlSessionFactoryBuilder"/
beanid= "mySqlSessionFactory"class= "org.apache.ibatis.session.SqlSessionFactory"
factory-bean= "mySqlSessionFactoryBuilder"
factory-method= "build"
constructor-argref= "myInputStream"/
/ bean
可以看到,雖然借助如上所示的factory-bean和factory-method標(biāo)簽屬性,我們也能通過(guò)XML完成對(duì)SqlSessionFactory的聲明,但這種通過(guò)XML刻畫(huà)bean初始化過(guò)程的方式,與我們?cè)趩?wèn)題背景介紹章節(jié)看到的基于工廠bean的聲明方式相比,不免顯得有些繁瑣了。不過(guò),隨著Spring 3.0帶來(lái)的基于@Configuration的Java注解配置特性,工廠bean在這方面的優(yōu)勢(shì)也變得不再那么明顯了。
publicclassMapperFactoryBean T extendsSqlSessionDaoSupportimplementsFactoryBean T {
// 映射器接口類(lèi)型
privateClassT mapperInterface;
// 通過(guò)該方法獲取我們實(shí)際需要的映射器實(shí)例
@Override
publicT getObjectthrowsException {
returngetSqlSession.getMapper( this.mapperInterface);
}
// 獲取實(shí)際的bean的類(lèi)型,即映射器接口類(lèi)型
@Override
publicClassT getObjectType{
returnthis.mapperInterface;
}
}
不過(guò),當(dāng)我們考察如上MapperFactoryBean的源碼時(shí),會(huì)發(fā)現(xiàn)它的bean初始化邏輯很簡(jiǎn)單,與單純基于MyBatis的代碼實(shí)現(xiàn)如出一轍。其中僅有的不同是,這里getMapper方法的映射器類(lèi)型入?yún)?,使用的是工廠bean中的mapperInterface屬性。前面我們提到,MapperScannerConfigurer在掃描被@Mapper注解標(biāo)注的映射器接口時(shí),會(huì)為每個(gè)接口生成一個(gè)對(duì)應(yīng)的bean定義,并將bean定義的類(lèi)型屬性改寫(xiě)為工廠bean類(lèi)型。而對(duì)于bean定義中mapperInterface屬性的設(shè)置,也是在此時(shí)完成的(屬性的值即為映射器接口的全限定名)。隨后,在bean的實(shí)例化環(huán)節(jié),Spring便可以基于這些bean定義,為每個(gè)映射器接口生成一個(gè)對(duì)應(yīng)的工廠bean,以此服務(wù)于我們開(kāi)發(fā)中常用的映射器實(shí)例依賴(lài)注入場(chǎng)景。對(duì)此,如果通過(guò)Java注解配置或是XML聲明的方式實(shí)現(xiàn),則會(huì)顯得有些大費(fèi)周章 —— 對(duì)于每一個(gè)Mybatis映射器接口,我們都需要作一次對(duì)應(yīng)的聲明;而如果一個(gè)項(xiàng)目中包含數(shù)十個(gè)映射器接口(這個(gè)量級(jí)在中大型項(xiàng)目中應(yīng)屬常見(jiàn)),則需要做數(shù)十次大同小異的聲明。
對(duì)于HSFSpringConsumerBean這個(gè)工廠bean來(lái)說(shuō),其作用也是類(lèi)似。這類(lèi)bean注入方式的共性是:基于注解(或接口)掃描以及一些相關(guān)的配置信息,為每個(gè)被標(biāo)注的接口生成一個(gè)對(duì)應(yīng)的工廠bean;而當(dāng)工廠bean通過(guò)getObject方法輸出我們最終需要的bean時(shí),往往是基于配置信息為接口生成一個(gè)動(dòng)態(tài)代理,供實(shí)際使用。這種做法常見(jiàn)于Spring與其他框架集成的場(chǎng)景。就我們文中分析的例子而言,在數(shù)據(jù)庫(kù)持久化領(lǐng)域,除了Mybatis外,Hibernate借助JpaRepositoryFactoryBean這個(gè)工廠bean生成其Repository接口的實(shí)例;在遠(yuǎn)程調(diào)用領(lǐng)域,除了HSF外,Spring Cloud中的Feign通過(guò)FeignClientFactoryBean為標(biāo)注有@FeignClient注解的客戶(hù)端接口生成動(dòng)態(tài)代理。由于篇幅所限,這里僅以MyBatis為例,展示其類(lèi)結(jié)構(gòu)關(guān)系(見(jiàn)下圖)。對(duì)于其他的案例,我們不再一一展開(kāi)分析,感興趣的讀者可以閱讀相關(guān)源碼作進(jìn)一步了解。
回到我們文章中探討的配置項(xiàng)解析問(wèn)題,可以看到,雖然工廠bean能為Spring與其他框架整合提供很多便利,但如果使用不慎,則可能導(dǎo)致一些隱蔽的問(wèn)題。其實(shí),在2015年,MyBatis的MapperFactoryBean也遇到了類(lèi)似的與類(lèi)型推斷相關(guān)的問(wèn)題(詳見(jiàn)github - mybatis-spring issue #58及pull request #59),而社區(qū)對(duì)此的解決方式是:利用Spring對(duì)bean進(jìn)行實(shí)例化時(shí),會(huì)首先嘗試匹配有參構(gòu)造函數(shù)的特性,在MapperFactoryBean中新增一個(gè)以映射器類(lèi)型為入?yún)⒌臉?gòu)造函數(shù);并在處理工廠bean定義的階段,將映射器類(lèi)型作為構(gòu)造函數(shù)參數(shù),放入bean定義中(如下圖所示)。如此,在前文提到的「簡(jiǎn)單創(chuàng)建」后,Spring便可以通過(guò)調(diào)用getObjectType方法獲取到當(dāng)前MapperFactoryBean實(shí)例所代表的映射器接口類(lèi)型了。
最后,回到本次問(wèn)題的關(guān)鍵點(diǎn)之一:HSFSpringConsumerBean。在使用XML聲明的方式時(shí),雖然我們?cè)诠Sbean的interfaceName字段指定了客戶(hù)端接口類(lèi)型,但Spring在嘗試對(duì)其進(jìn)行「簡(jiǎn)單創(chuàng)建」以做類(lèi)型推斷時(shí),并不會(huì)為實(shí)例中的屬性字段賦值。這導(dǎo)致我們無(wú)法通過(guò)調(diào)用該實(shí)例的getObjectType方法得到它所代表的客戶(hù)端接口類(lèi)型,并最終導(dǎo)致該工廠bean被「正式創(chuàng)建」了出來(lái)。雖然通過(guò)@HSFConsumer注解聲明的形式,我們得以規(guī)避了配置項(xiàng)解析問(wèn)題,但HSF作者可以考慮參考MapperFactoryBean的方式,增加一個(gè)以客戶(hù)端接口類(lèi)型為入?yún)⒌臉?gòu)造函數(shù),來(lái)更好地兼容基于XML的聲明方式。
基于注解的配置項(xiàng)解析
上文主要圍繞基于XML聲明的配置項(xiàng)解析進(jìn)行了分析探討,其實(shí),自Spring引入基于Java注解的bean聲明能力以來(lái),我們使用得更多的是基于注解的配置項(xiàng)解析特性。而對(duì)此特性的支持主要是通過(guò)Spring的bean后置處理器(BeanPostProcessor)完成的。絕大部分bean的后置處理是在bean的創(chuàng)建環(huán)節(jié)被觸發(fā)的:bean工廠首先對(duì)bean進(jìn)行實(shí)例化,然后使用bean后置處理器對(duì)它們進(jìn)行相應(yīng)的處理操作。下面我們進(jìn)行簡(jiǎn)單的介紹。
前面我們提到,Spring會(huì)以硬編碼的形式將AutowiredAnnotationBeanPostProcessor這個(gè)bean后置處理器注冊(cè)到bean工廠中。從字面上看,這個(gè)處理器是負(fù)責(zé)@Autowired注解的,其實(shí),@Value注解也在它的處理范圍之內(nèi)。處理器會(huì)在bean實(shí)例化后的屬性賦值步驟(注10)被觸發(fā),對(duì)@Value注解中的配置項(xiàng)占位符進(jìn)行解析,并將屬性值賦給被注解標(biāo)注的字段。而其使用的配置項(xiàng)解析器,其中之一就是通過(guò)PropertySourcesPlaceholderConfigurer這個(gè)bean工廠后置處理器添加的(詳見(jiàn)XML配置項(xiàng)解析章節(jié))。
另一個(gè)我們常見(jiàn)的配置項(xiàng)相關(guān)的注解是@ConfigurationProperties。該注解由ConfigurationPropertiesBindingPostProcessor這個(gè)bean后置處理器處理,處理動(dòng)作在bean實(shí)例化后的初始化步驟(注11)被觸發(fā)。除了我們熟知的作用于類(lèi)的使用方式外,@ConfigurationProperties還可以作用于被@Bean注解標(biāo)注的方法 —— 這主要是針對(duì)我們無(wú)法直接將注解加在第三方外部類(lèi)上的情況。而這里對(duì)于方法級(jí)別的注解解析,處理器便是借助我們之前提到的ConfigurationBeanFactoryMetaData的工廠方法記錄完成的(詳見(jiàn)bean工廠后置處理小節(jié))。具體代碼如下所示:
@Override
publicObject postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
ConfigurationProperties annotation= AnnotationUtils.findAnnotation(bean.getClass,
ConfigurationProperties. class);
if( annotation!= null) {
postProcessBeforeInitialization(bean, beanName, annotation);
}
// 處理方法級(jí)別的@ConfigurationProperties注解
// 這里的this.beans即為ConfigurationBeanFactoryMetaData實(shí)例
annotation= this.beans.findFactoryAnnotation(beanName, ConfigurationProperties. class);
if( annotation!= null) {
postProcessBeforeInitialization(bean, beanName, annotation);
}
returnbean;
}
思考與總結(jié)
我們可以看到,隨著Spring從基于XML的bean聲明到基于Java注解的bean聲明能力的演化,對(duì)配置項(xiàng)的解析方式也在發(fā)生著變化。其中牽涉到bean工廠后置處理、bean后置處理等環(huán)節(jié),而它們彼此之間又存在一定的關(guān)聯(lián)。同時(shí),如果某些bean(如工廠bean)由于某些原因,在Spring啟動(dòng)的早期階段(如bean定義注冊(cè)表后置處理環(huán)節(jié))被提前創(chuàng)建了出來(lái),則可能導(dǎo)致其中的配置項(xiàng)解析失敗。對(duì)此,我們一方面可以嘗試尋找規(guī)避手段,另一方面也可以從該bean本身的設(shè)計(jì)探究原因。
Spring后置處理器
處理器類(lèi)型
說(shuō)明
ConfigurationClassPostProcessor
bean定義注冊(cè)表后置處理器
在Spring Boot中,從包含了@SpringBootApplication注解的引導(dǎo)類(lèi)開(kāi)始,掃描并注冊(cè)bean定義至bean工廠。在本文案例中,MapperScannerConfigurer的bean定義便是在此時(shí)被注冊(cè)的。
MapperScannerConfigurer
bean定義注冊(cè)表后置處理器
掃描@Mapper注解標(biāo)注的映射器接口,生成并注冊(cè)對(duì)應(yīng)的MapperFactoryBean工廠bean定義。支持使用PropertySourcesPlaceholderConfigurer等處理器對(duì)自身的屬性字段進(jìn)行配置項(xiàng)解析。
PropertySourcesPlaceholderConfigurer
bean工廠后置處理器
遍歷bean定義,解析其中的配置項(xiàng)占位符。
ConfigurationBeanFactoryMetaData
bean工廠后置處理器
遍歷bean定義,記錄工廠方法等信息。
HsfConsumerPostProcessor
bean工廠后置處理器
遍歷bean定義,對(duì)于被@HsfConsumer注解標(biāo)注的屬性字段,生成并注冊(cè)對(duì)應(yīng)的HSFSpringConsumerBean工廠bean定義。
AutowiredAnnotationBeanPostProcessor
bean后置處理器
解析@Value注解中的配置項(xiàng)占位符。解析器之一由PropertySourcesPlaceholderConfigurer提供。
ConfigurationPropertiesBindingPostProcessor
bean后置處理器
解析@ConfigurationProperties注解中的配置項(xiàng)。對(duì)@Bean方法級(jí)別的注解解析借助ConfigurationBeanFactoryMetaData中的bean工廠方法記錄完成。
為了方便讀者理解,以上表格整理了文中提到的各類(lèi)Spring后置處理器,以及它們彼此的關(guān)聯(lián)。可以看到,Spring框架在給我們提供了很多開(kāi)發(fā)便利的同時(shí),其整體的設(shè)計(jì)還是較為復(fù)雜的。在日常開(kāi)發(fā)中,我們可能時(shí)不時(shí)會(huì)遇到一些「疑難雜癥」,而此時(shí)對(duì)框架的深入理解能幫助我們高效地解決問(wèn)題。此外,善用對(duì)Spring代碼的調(diào)試,也能幫助我們?cè)诩姺钡乃悸坊蚓€索中定位到問(wèn)題原因。最后,由于寫(xiě)作時(shí)間倉(cāng)促,且Spring不同版本間可能存在一定的行為差異,文中如有錯(cuò)漏之處還請(qǐng)讀者包涵指正。
注釋?zhuān)?/p>
1.除了被@Configuration注解標(biāo)注的類(lèi)外,被@Component等注解標(biāo)注的類(lèi)也被Spring視為配置類(lèi),不過(guò)是輕量級(jí)(lite)配置類(lèi),參見(jiàn)《Spring Core Technologies》1.12章節(jié) - Java-based Container Configuration。
2.參見(jiàn)《Spring實(shí)戰(zhàn)》6.1.1小節(jié) - 理解Spring的環(huán)境抽象。
3.對(duì)于匹配到多個(gè)bean的情況,會(huì)優(yōu)先取包含@Primary注解或者優(yōu)先級(jí)高的bean,如果無(wú)法判斷,則會(huì)拋出NoUniqueBeanDefinitionException異常;對(duì)于沒(méi)有匹配到bean的情況,拋出NoSuchBeanDefinitionException異常。
4.具體代碼詳見(jiàn)AbstractAutowireCapableBeanFactory#getTypeForFactoryBean方法。
5.即調(diào)用BeanDefinitionRegistryPostProcessor接口中定義的postProcessBeanDefinitionRegistry方法。
6.具體代碼詳見(jiàn)AnnotationConfigUtils#registerAnnotationConfigProcessors方法。
7.某些bean工廠后置處理器也會(huì)向bean工廠中添加新的bean定義,比如我們后文將討論的HsfConsumerPostProcessor處理器。
8.參見(jiàn)《Spring Core Technologies》1.8.3小節(jié) - Customizing Instantiation Logic with a FactoryBean:If you have complex initialization code that is better expressed in Java as opposed to a (potentially) verbose amount of XML, you can create your own FactoryBean, write the complex initialization inside that class, and then plug your custom FactoryBean into the container.
9.在FactoryBean的代碼注釋中,我們可以看到,該類(lèi)是在2003年3月份被創(chuàng)建的。而根據(jù)《History of Spring Framework and Spring Boot》一文,Spring 0.9的發(fā)布時(shí)間為2003年6月。
10.具體代碼詳見(jiàn)AbstractAutowireCapableBeanFactory#populateBean方法。
11.具體代碼詳見(jiàn)AbstractAutowireCapableBeanFactory#initializeBean方法。
參考資料:
1.《Spring Core Technologies》:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html
4.《History of Spring Framework and Spring Boot》:https://www.quickprogrammingtips.com/spring-boot/history-of-spring-framework-and-spring-boot.html
6.mybatis - getting started:https://mybatis.org/mybatis-3/getting-started.html
7.github - mybatis-spring issue #58:https://github.com/mybatis/spring/issues/58
8.github - mybatis-spring pull request #59:https://github.com/mybatis/spring/pull/59
掃描二維碼推送至手機(jī)訪問(wèn)。
版權(quán)聲明:本文由飛速云SEO網(wǎng)絡(luò)優(yōu)化推廣發(fā)布,如需轉(zhuǎn)載請(qǐng)注明出處。