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