主頁 > 後端開發 > 深入Spring配置項問題,全面決議

深入Spring配置項問題,全面決議

2023-05-19 14:18:25 後端開發

本文就Spring配置項決議問題展開分析,這其中涉及到bean定義注冊表后置處理、bean工廠后置處理、工廠bean等Spring相關的概念,本文將以上述問題作為切入點,進行分析和展開介紹,

問題背景介紹

我們的專案中某次依賴了某個第三方包及其中的XML檔案,相關代碼如下所示:XML檔案中定義了Mybatis相關的bean,以及對自定義資料源myDataSource的參考,在@Configuration配置類中,我們引入了XML檔案,并通過@Bean注解的方式宣告了資料源bean,
<!-- 配置SqlSessionFactoryBean -->
<bean id="thirdPartySqlSessionFactory"
      
      depends-on="myDataSource">
    <!-- datasource為自定義資料源 -->
    <property name="dataSource" ref="myDataSource"/>
    <property name="mapperLocations" value="https://www.cnblogs.com/88223100/archive/2023/05/19/classpath:mybatis/third-party/*.xml"/>
</bean>

<!-- 配置MapperScannerConfigurer -->
<bean id="thirdPartyMapperScannerConfigurer"
      
      depends-on="thirdPartySqlSessionFactory">
    <property name="basePackage" value="https://www.cnblogs.com/88223100/archive/2023/05/19/com.alibaba.thirdparty.dao"/>
    <property name="sqlSessionFactoryBeanName" value="https://www.cnblogs.com/88223100/archive/2023/05/19/thirdPartySqlSessionFactory"/>
</bean>
 
@Configuration
@EnableTransactionManagement(proxyTargetClass = true)
// 引入上述XML檔案
@ImportResource("classpath*:/mybatis-third-party-config.xml")
public class MyDataSourceConfiguration {

    // 宣告自定義資料源
    @Bean(name = "myDataSource")
    public DataSource createMyDataSource(Environment env) {
        // 回傳資料源實體,具體代碼略
    }
    
}

專案啟動后,我們發現一個原有的通過XML定義的HSF(HSF全稱High-speed Service Framework,是阿里內部主要使用的RPC服務框架)客戶端bean中的配置項無法被正常決議,由于這是一個與我們新引入的包無關的bean,大家都對問題產生的原因感到奇怪,也嘗試了各種不同的處理方式,然而都沒有效果,無奈之下,我們通過將整個XML檔案改寫為Java注解宣告的形式,才最終解決了問題,相關代碼如下所示:


<!-- 專案中一個原有的bean宣告 -->
<bean id="myHsfClient"  init-method="init">
    <property name="interfaceName">
        <value>com.taobao.custom.MyHsfClient</value>
    </property>
    <!-- version屬性值為待決議的配置項占位符 -->
    <property name="version">
        <value>${hsf.client.version}</value>
    </property>
</bean>

<!-- 其余bean宣告省略 -->

 


// 改寫后的Java注解宣告方式
@Configuration
public class MyHsfConfig {

    @HSFConsumer(serviceVersion = "${hsf.client.version}")
    private MyHsfClient myHsfClient;

    // 其余代碼省略
}
 

雖然問題得到了解決,但是大家仍舊對這其中的原因不明所以,筆者在事后通過本地除錯的方式,找到了問題的原因,這其中涉及到bean定義注冊表后置處理、bean工廠后置處理、工廠bean等Spring相關的概念,本文將以上述問題作為切入點,進行分析和展開介紹,

XML配置項決議

為了更好地解答上述問題產生的原因,我們先來看下Spring框架對bean使用的配置項的決議程序,我們知道,Spring會負責對我們在XML檔案中宣告的bean的創建,不過,對其中的配置項決議,并不是在這個環節發生,而是在其前置環節 —— bean工廠后置處理的程序中發生的,bean工廠(BeanFactory)是Spring的核心組件,除了負責初始化bean的實體,記錄單例外,它還維護了各個bean的定義(BeanDefinition),bean的定義中主要記錄了bean的型別、作用域(singleton/prototype)、屬性值、建構式引數值等資訊,bean的實體化便是基于bean的定義進行的,而bean工廠的后置處理環節,則可以在bean被創建之前,修改bean的定義,以達到影響最終生成的bean實體的效果,
對XML中配置項的決議作業,Spring是通過PropertySourcesPlaceholderConfigurer這個bean工廠后置處理器(BeanFactoryPostProcessor)完成的,其核心代碼如下所示,總體思路比較簡單,即遍歷bean工廠中的bean定義,對于每個bean的定義,訪問其屬性值、建構式引數值等資訊,決議其中的配置項占位符(placeholder),這個環節完成之后,在bean工廠對bean進行初始化之前,bean定義中的配置項占位符就已經被替換為實際的屬性值了,

// 處理屬性值
protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
        StringValueResolver valueResolver) {

    BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(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) {
                throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex);
            }
        }
    }

    // 將配置項決議器注冊添加至bean工廠,供基于注解的配置項決議處理器使用(后文將詳細介紹)
    // New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes.
    beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
    // 其余代碼省略
}
了解了bean工廠后置處理環節后,讓我們再往前探究一步,看下bean定義本身是如何被加載到bean工廠中的(這將有助于我們理解文章開頭所提到的問題的產生原因),bean定義主要是在bean定義注冊表后置處理環節被加載到bean工廠中的,與我們前面提到的bean工廠后置處理環節類似,該環節也存在相應的處理器(BeanDefinitionRegistryPostProcessor)完成相關作業,
其中典型的如ConfigurationClassPostProcessor,以Spring Boot場景為例,簡單來說,該bean定義注冊表后置處理器會從包含了@SpringBootApplication注解的啟動引導類開始,根據其組合注解@ComponentScan,掃描被@Component,或者組合了@Component的注解(如@Configuration、@Service、@Repository等)標注的類,將這些配置類(注1)的bean定義注冊至bean工廠,同時,處理器還會根據組合注解@EnableAutoConfiguration,獲取Spring Boot中的自動配置類,在這之后,ConfigurationClassPostProcessor會嘗試決議各個配置類中包含的@Bean、@ImportResource等注解,將對應的bean定義也注冊到bean工廠中,
最后,對于配置項本身來說,Spring的環境抽象(Environment)會拉取并聚合JVM系統屬性、作業系統環境變數、應用屬性組態檔等多個屬性源的資料(注2),以供bean工廠中的bean定義或者bean實體使用,如前面提到的PropertySourcesPlaceholderConfigurer處理器,便是從Spring環境中獲取bean定義中的配置項占位符所對應的屬性值,并將其替換的,上文通過倒序的方式介紹了配置項決議的相關環節,下面我們用順序表示的流程圖作結,以便讀者更好地理解,
圖片

問題原因分析

現在,我們可以對文章開頭提到的問題作進一步分析了,仔細查看我們所引入的XML檔案可以發現,其中包含一個型別為MapperScannerConfigurer的bean宣告,Spring借助該類完成對標注有@Mapper注解的MyBatis映射介面的掃描,MapperScannerConfigurer實作了BeanDefinitionRegistryPostProcessor介面,是一個bean定義注冊表后置處理器,它對映射介面的掃描及其對應的bean定義的注冊,便是在該環節進行的,
前面我們提到,ConfigurationClassPostProcessor這個bean定義注冊表后置處理器會掃描并加載@Configuration和@ImportResource注解相關的bean定義,我們所引入的XML檔案中的bean的定義,便是通過這個動作被注冊到bean工廠中的(見上文MyDataSourceConfiguration配置類),在ConfigurationClassPostProcessor完成其掃描及加載作業后,由于有新的bean定義被注冊,Spring會再次嘗試從bean工廠中找出并初始化其他的bean定義注冊表后置處理器,以觸發它們的處理動作,MapperScannerConfigurer便是在此時被實體化并觸發的,
觀察問題背景介紹章節中的相關代碼可以發現,MapperScannerConfigurer的bean實體(thirdPartyMapperScannerConfigurer)間接依賴了我們通過@Bean注解在配置類中宣告的資料源bean實體(myDataSource),因此,在本文案例中,Spring在創建MapperScannerConfigurer實體時,會首先對資料源bean進行初始化,而對于通過@Bean注解宣告的bean,Spring是通過反射呼叫注解所在的工廠方法(factory method),完成bean的實體化的,我們的資料源myDataSource的實體化,便是通過反射呼叫其工廠方法createMyDataSource完成的,由于該方法包含了一個型別為Environment入參,Spring需要遍歷bean工廠中的bean定義,找到并創建匹配的bean,作為反射呼叫時的方法傳參,
而問題恰恰就出現在這里的引數匹配環節,Spring在進行方法入參匹配時,會首先呼叫getBeanNamesForType方法,將符合引數型別的bean的名稱找出來,然后依據一定的策略(注3)將bean進行實體化,作為方法入參使用,對于普通的bean來說,Spring只需要依據bean定義中包含的bean型別資訊,與引數型別作匹配即可;而對于另一類較為特殊的工廠bean(FactoryBean)來說,其型別推斷方式就會更加復雜些,下文將會展開介紹工廠bean的概念和案例,對此不太熟悉的讀者,這里只需要了解,工廠bean的作用是負責產生某個我們最終實際需要使用的bean,因此,在進行引數匹配時,Spring關心的是這個最終產生的bean的型別,而不是工廠bean本身的型別,
在判斷工廠bean實際輸出的bean的型別時(注4),Spring首先會嘗試根據工廠bean定義中的某些元資料進行型別推斷;其次會嘗試對工廠bean進行一次簡單創建后,通過其getObjectType方法獲取目標bean的型別,如果前兩種嘗試都失敗了,則會使用兜底邏輯 —— 對工廠bean進行正式創建后,再通過getObjectType獲取型別資訊,這里的「正式創建」,我們可以理解為Spring完成了工廠bean的實體化、屬性欄位的賦值、單例資訊的記錄等;而「簡單創建」僅僅指工廠bean的實體化,不包括后續的欄位初始化等動作,
而我們在上文提到的myHsfClient,便是被宣告為了一個型別為HSFSpringConsumerBean的工廠bean,Spring在對createMyDataSource的方法入參進行型別匹配時,由于前述的前兩種型別推斷方式都沒有成功(其具體原因將在后文工廠bean小節中介紹),導致該工廠bean最終被「提前」正式創建了出來,讀者可能已經發現,此時Spring正處在bean定義注冊表后置處理環節,而我們在XML配置項決議章節中提到的對bean定義中的配置項占位符的決議替換,則是在該環節之后的bean工廠后置處理環節進行的 —— 這就是導致myHsfClient這個工廠bean中的配置項沒有被正常決議的原因,整體方法呼叫關系如下圖所示:
圖片
至此可能讀者會有疑問:難道我們的專案中之前沒有對@Mapper映射器介面的掃描動作嗎?答案是有掃描動作,不過是通過MapperScannerRegistrar這個bean定義注冊器觸發的,而由于其與我們通過XML所引入的MapperScannerConfigurer的一些細微區別,使得專案中原先不存在工廠bean被提前創建的問題,由于篇幅所限,這里不再對MapperScannerRegistrar作展開介紹,
知道了問題背后的原因后,尋找對應的解法也就相對簡單了,對于文中案例,一方面,我們可以看到,由于thirdPartyMapperScannerConfigurer依賴了SqlSessionFactoryBean實體(這就是我們剛剛說的「細微區別」所在),導致其間接依賴了myDataSource,而考察原始碼可以發現,其實MapperScannerConfigurer只需要SqlSessionFactory的bean名稱(sqlSessionFactoryBeanName)作為輸入即可,因此我們可以把XML中相關的depends-on宣告去除,另一方面,由于createMyDataSource方法入參是Spring環境抽象,我們可以改由通過使配置類實作EnvironmentAware介面的方式,獲得應用背景關系中的Environment實體,這兩種方法都能解決我們的工廠bean被提前創建的問題,
在更一般化的場景中,如果在Spring啟動的早期階段,對某個bean的依賴注入無法避免,我們可以使相關的類實作ApplicationContextAware介面,嘗試通過應用背景關系(ApplicationContext)的getBean方法獲取我們想要的物件,不過需要注意的是,getBean方法存在兩類版本:根據bean名稱獲取實體,或是根據指定型別獲取實體;而如果我們選擇根據指定型別獲取實體,則仍舊會觸發上文提到的型別匹配機制,導致某些無法通過正常方式進行型別推斷的工廠bean被提前創建出來,最后,對于前文提到的,在使用注解形式改寫myHsfClient的bean宣告后,問題得到解決的原因,我們將在后文分析介紹,

一些引申擴展

經過上文讓人感覺有些繞的分析,我們可以看到,文章開頭所提到的問題的本質是,某些bean被Spring提前正式創建了出來,導致其bean宣告中的配置項占位符沒有來得及被決議和替換,這其中涉及到不少概念,諸如bean定義注冊表后置處理、bean工廠后置處理、工廠bean等,由于我們在日常開發中一般接觸得不多,讀者對它們的理解可能還比較模糊,下文將嘗試結合實際案例,進行一些引申和擴展介紹,

 

bean定義注冊表后置處理

我們在前文中已經介紹了ConfigurationClassPostProcessor和MapperScannerConfigurer這兩個bean定義注冊表后置處理器,這類處理器的主要作用便是掃描并向bean工廠中注冊bean定義,其中,ConfigurationClassPostProcessor負責掃描配置類,處理其包含的注解,并將相關的bean定義注冊至bean工廠中,隨后,對于這些新增的bean定義,如果其中又包含了其他的bean定義注冊表后置處理器,Spring會將它們實體化,并觸發它們的處理動作(注5),繼續注冊可能被發現的新的bean定義……如此回圈往復,直到所有該型別的處理器都被觸發,完成bean定義的注冊為止,如以下代碼所示:
public static void invokeBeanFactoryPostProcessors(
        ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> 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) {
            // 如果當前處理器尚未被觸發過
            if (!processedBeans.contains(ppName)) {
                // 初始化處理器,并加入到本次需要觸發的處理器集合中
                currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
                // 標記處理器為已被處理
                processedBeans.add(ppName);
                // 繼續回圈,因為當前集合中的處理器被觸發后,可能會引入新的bean定義,其中可能包含新的bean定義注冊表后置處理器需要被觸發
                reiterate = true;
            }
        }
        sortPostProcessors(currentRegistryProcessors, beanFactory);
        registryProcessors.addAll(currentRegistryProcessors);
        // 觸發集合中的處理器的bean定義注冊表后置處理動作
        // * 本文案例中,我們在第三方XML檔案中引入的MapperScannerConfigurer,便是在此時被觸發的
        invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
        currentRegistryProcessors.clear();
    }
    
}
回到我們的問題案例,MapperScannerConfigurer便是在上述環節被創建出來并觸發的,這里,細心的讀者可能會有疑問:如果我們在使用XML宣告這個Mybatis的處理器時,對其中的某些屬性也使用了配置項占位符,那么Spring在創建它時,是否也會遇到同樣的決議問題?MapperScannerConfigurer的作者顯然是考慮到了這一點 —— 處理器被觸發后,支持首先嘗試對它的屬性欄位進行配置項的決議和替換,其具體的實作方式,是構造一個新的bean工廠,將自身的bean定義注冊其中,然后借助PropertySourcesPlaceholderConfigurer等處理器,對這個bean工廠執行配置項的后置處理操作;最后,用bean定義中的被決議后的屬性值,替換自身實體中原有的屬性值,這在一定程度上相當于模擬了Spring的bean工廠后置處理環節,其具體代碼如下:
/*
* 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.
*/
// 上面這段英文注釋體現了作者的考慮,即文中描述的情況
private void processPropertyPlaceHolders() {
    // 獲取配置項處理器實體,即PropertySourcesPlaceholderConfigurer處理器
  Map<String, PropertyResourceConfigurer> prcs = applicationContext.getBeansOfType(PropertyResourceConfigurer.class);

    if (!prcs.isEmpty() && applicationContext instanceof ConfigurableApplicationContext) {
        BeanDefinition mapperScannerBean = ((ConfigurableApplicationContext) applicationContext)
          .getBeanFactory().getBeanDefinition(beanName);
        
        // 構造一個新的bean工廠
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        // 將自身的bean定義注冊到這個bean工廠中
        factory.registerBeanDefinition(beanName, mapperScannerBean);
        // * 對這個bean工廠執行配置項后置處理操作
        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應用背景關系(ApplicationContext)初始化的程序中,通過硬編碼的形式被注冊到bean工廠中的(注6),這里同時被注冊的還有諸如AutowiredAnnotationBeanPostProcessor等bean后置處理器,我們將在后文對此作相應介紹,

 

bean工廠后置處理

當bean定義注冊表后置處理環節完成后,基本上(注7)所有的bean定義都已經被注冊至bean工廠中了,隨后,Spring會找出所有的bean工廠后置處理器,按照一定的順序實體化并觸發它們的處理動作(優先執行實作了PriorityOrdered介面的,其次執行實作了Ordered介面的,最后執行沒有實作前兩個介面的),這類處理器一般會遍歷bean工廠中所有的bean定義,執行一些特定的操作,我們在前文提到的PropertySourcesPlaceholderConfigurer這個bean工廠后置處理器,便是在此時被觸發的,而在這個所有bean定義都已經準備就緒的階段,統一進行配置項占位符的決議和替換,其時機總體上也是恰當合理的,
其他的比較典型的Spring內置bean工廠后置處理器還有ConfigurationBeanFactoryMetaData,這個處理器執行的動作比較簡單:它會遍歷bean工廠中的bean定義,記錄其中的工廠方法等元資料資訊,其核心代碼如下所示,而這份記錄的作用,我們將在后文說明,
public class ConfigurationBeanFactoryMetaData implements BeanFactoryPostProcessor {
    
    private Map<String, MetaData> beans = new HashMap<String, MetaData>();
    
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
        // 遍歷bean工廠中的bean定義
        for (String name : beanFactory.getBeanDefinitionNames()) {
            BeanDefinition definition = beanFactory.getBeanDefinition(name);
            String method = definition.getFactoryMethodName();
            String bean = definition.getFactoryBeanName();
            // 如果存在工廠方法元資料(如通過@Bean注解宣告的bean),則將相關資訊記錄下來
            if (method != null && bean != null) {
                this.beans.put(name, new MetaData(bean, method));
            }
        }
    }
}
最后,我們來看另一個和我們的XML配置項決議問題相關的處理器,在問題背景介紹章節中我們提到,當把myHsfClient的bean宣告改寫為由@HSFConsumer注解修飾的形式后,問題得到了解決,而這背后則是HsfConsumerPostProcessor這個bean工廠后置處理器在發揮作用:對于每一個bean定義,如果它的類屬性欄位上存在@HSFConsumer注解,處理器會動態生成并注冊一個型別為HSFSpringConsumerBean的工廠bean定義,雖然由于PropertySourcesPlaceholderConfigurer處理器實作了PriorityOrdered介面,在此之前已經被優先執行過了,但是HsfConsumerPostProcessor考慮到了這一點 —— 在生成工廠bean定義的程序中,會主動嘗試決議相關屬性的配置項占位符,因此規避了我們在使用XML方式進行工廠bean宣告時遇到的問題,

 

工廠bean

前面我們提到,HSFSpringConsumerBean是一個工廠bean,不僅如此,我們詳細討論的Mybatis的MapperScannerConfigurer處理器,對于其基于@Mapper注解掃描到的映射介面,也會將其bean定義改寫為MapperFactoryBean這個工廠bean型別,此外,在Spring中,用于創建Mybatis的SqlSession物件的SqlSessionFactory,也是由一個名為SqlSessionFactoryBean的工廠bean生成的,那么,什么是工廠bean,它的作用又是什么呢?
工廠bean,即FactoryBean,其基于工廠模式,創建我們最終需要的bean實體,根據Spring檔案中的介紹(注8),如果某個bean的初始化邏輯較為復雜,不適合使用XML的方式表達,那么我們可以通過使用工廠bean,以Java語言的方式完成目標bean的初始化,工廠bean的概念早在Spring 0.9版本(注9)就已經被引入,在Spring框架中的使用是比較普遍的,至今為止僅其自帶的實作就有50多個,下面我們就文中的案例展開介紹,

// MyBatis組態檔路徑,組態檔包含資料源、映射器等資訊
String resource = "org/mybatis/example/mybatis-config.xml";
// 創建組態檔輸入流
InputStream inputStream = Resources.getResourceAsStream(resource);
// 創建SqlSessionFactory實體
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 創建SqlSession實體
try (SqlSession session = sqlSessionFactory.openSession()) {
  // 獲取BlogMapper映射器
    BlogMapper mapper = session.getMapper(BlogMapper.class);
    // 執行查詢陳述句
    Blog blog = mapper.selectBlog(101);
}
在介紹Mybatis與Spring整合時使用的兩個工廠bean之前,我們先來看下相關功能單純基于Mybatis本身實作時的代碼,代碼片段摘自Mybatis官網,如上所示,可以看到,其中SqlSessionFactory實體是由SqlSessionFactoryBuilder創建的;而用于執行查詢陳述句的映射器實體,則是由SqlSession實體的getMapper方法創建的,與之相對的,如果閱讀原始碼可以發現,在Mybatis-Spring中,用于創建SqlSessionFactory實體的SqlSessionFactoryBean和映射器實體的MapperFactoryBean這兩個工廠bean,在一定程度上可以看作是對上述代碼封裝和擴展,

<bean id="myInputStream" 
      factory-method="getResourceAsStream">
    <constructor-arg value="https://www.cnblogs.com/88223100/archive/2023/05/19/org/mybatis/example/mybatis-config.xml"/>
</bean>

<bean id="mySqlSessionFactoryBuilder" />

<bean id="mySqlSessionFactory" 
      factory-bean="mySqlSessionFactoryBuilder"
      factory-method="build">
    <constructor-arg ref="myInputStream"/>
</bean>
可以看到,雖然借助如上所示的factory-bean和factory-method標簽屬性,我們也能通過XML完成對SqlSessionFactory的宣告,但這種通過XML刻畫bean初始化程序的方式,與我們在問題背景介紹章節看到的基于工廠bean的宣告方式相比,不免顯得有些繁瑣了,不過,隨著Spring 3.0帶來的基于@Configuration的Java注解配置特性,工廠bean在這方面的優勢也變得不再那么明顯了,

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {

    // 映射器介面型別
    private Class<T> mapperInterface;

    // 通過該方法獲取我們實際需要的映射器實體
    @Override
    public T getObject() throws Exception {
        return getSqlSession().getMapper(this.mapperInterface);
    }

    // 獲取實際的bean的型別,即映射器介面型別
    @Override
    public Class<T> getObjectType() {
      return this.mapperInterface;
    }
    
}
不過,當我們考察如上MapperFactoryBean的原始碼時,會發現它的bean初始化邏輯很簡單,與單純基于MyBatis的代碼實作如出一轍,其中僅有的不同是,這里getMapper方法的映射器型別入參,使用的是工廠bean中的mapperInterface屬性,前面我們提到,MapperScannerConfigurer在掃描被@Mapper注解標注的映射器介面時,會為每個介面生成一個對應的bean定義,并將bean定義的型別屬性改寫為工廠bean型別,而對于bean定義中mapperInterface屬性的設定,也是在此時完成的(屬性的值即為映射器介面的全限定名),隨后,在bean的實體化環節,Spring便可以基于這些bean定義,為每個映射器介面生成一個對應的工廠bean,以此服務于我們開發中常用的映射器實體依賴注入場景,對此,如果通過Java注解配置或是XML宣告的方式實作,則會顯得有些大費周章 —— 對于每一個Mybatis映射器介面,我們都需要作一次對應的宣告;而如果一個專案中包含數十個映射器介面(這個量級在中大型專案中應屬常見),則需要做數十次大同小異的宣告,
對于HSFSpringConsumerBean這個工廠bean來說,其作用也是類似,這類bean注入方式的共性是:基于注解(或介面)掃描以及一些相關的配置資訊,為每個被標注的介面生成一個對應的工廠bean;而當工廠bean通過getObject方法輸出我們最終需要的bean時,往往是基于配置資訊為介面生成一個動態代理,供實際使用,這種做法常見于Spring與其他框架集成的場景,就我們文中分析的例子而言,在資料庫持久化領域,除了Mybatis外,Hibernate借助JpaRepositoryFactoryBean這個工廠bean生成其Repository介面的實體;在遠程呼叫領域,除了HSF外,Spring Cloud中的Feign通過FeignClientFactoryBean為標注有@FeignClient注解的客戶端介面生成動態代理,由于篇幅所限,這里僅以MyBatis為例,展示其類結構關系(見下圖),對于其他的案例,我們不再一一展開分析,感興趣的讀者可以閱讀相關原始碼作進一步了解,
圖片
回到我們文章中探討的配置項決議問題,可以看到,雖然工廠bean能為Spring與其他框架整合提供很多便利,但如果使用不慎,則可能導致一些隱蔽的問題,其實,在2015年,MyBatis的MapperFactoryBean也遇到了類似的與型別推斷相關的問題(詳見github - mybatis-spring issue #58及pull request #59),而社區對此的解決方式是:利用Spring對bean進行實體化時,會首先嘗試匹配有參建構式的特性,在MapperFactoryBean中新增一個以映射器型別為入參的建構式;并在處理工廠bean定義的階段,將映射器型別作為建構式引數,放入bean定義中(如下圖所示),如此,在前文提到的「簡單創建」后,Spring便可以通過呼叫getObjectType方法獲取到當前MapperFactoryBean實體所代表的映射器介面型別了,
圖片
最后,回到本次問題的關鍵點之一:HSFSpringConsumerBean,在使用XML宣告的方式時,雖然我們在工廠bean的interfaceName欄位指定了客戶端介面型別,但Spring在嘗試對其進行「簡單創建」以做型別推斷時,并不會為實體中的屬性欄位賦值,這導致我們無法通過呼叫該實體的getObjectType方法得到它所代表的客戶端介面型別,并最終導致該工廠bean被「正式創建」了出來,雖然通過@HSFConsumer注解宣告的形式,我們得以規避了配置項決議問題,但HSF作者可以考慮參考MapperFactoryBean的方式,增加一個以客戶端介面型別為入參的建構式,來更好地兼容基于XML的宣告方式,

基于注解的配置項決議

上文主要圍繞基于XML宣告的配置項決議進行了分析探討,其實,自Spring引入基于Java注解的bean宣告能力以來,我們使用得更多的是基于注解的配置項決議特性,而對此特性的支持主要是通過Spring的bean后置處理器(BeanPostProcessor)完成的,絕大部分bean的后置處理是在bean的創建環節被觸發的:bean工廠首先對bean進行實體化,然后使用bean后置處理器對它們進行相應的處理操作,下面我們進行簡單的介紹,
前面我們提到,Spring會以硬編碼的形式將AutowiredAnnotationBeanPostProcessor這個bean后置處理器注冊到bean工廠中,從字面上看,這個處理器是負責@Autowired注解的,其實,@Value注解也在它的處理范圍之內,處理器會在bean實體化后的屬性賦值步驟(注10)被觸發,對@Value注解中的配置項占位符進行決議,并將屬性值賦給被注解標注的欄位,而其使用的配置項決議器,其中之一就是通過PropertySourcesPlaceholderConfigurer這個bean工廠后置處理器添加的(詳見XML配置項決議章節),
另一個我們常見的配置項相關的注解是@ConfigurationProperties,該注解由ConfigurationPropertiesBindingPostProcessor這個bean后置處理器處理,處理動作在bean實體化后的初始化步驟(注11)被觸發,除了我們熟知的作用于類的使用方式外,@ConfigurationProperties還可以作用于被@Bean注解標注的方法 —— 這主要是針對我們無法直接將注解加在第三方外部類上的情況,而這里對于方法級別的注解決議,處理器便是借助我們之前提到的ConfigurationBeanFactoryMetaData的工廠方法記錄完成的(詳見bean工廠后置處理小節),具體代碼如下所示:

@Override
public Object 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);
    }
    return bean;
}

思考與總結

我們可以看到,隨著Spring從基于XML的bean宣告到基于Java注解的bean宣告能力的演化,對配置項的決議方式也在發生著變化,其中牽涉到bean工廠后置處理、bean后置處理等環節,而它們彼此之間又存在一定的關聯,同時,如果某些bean(如工廠bean)由于某些原因,在Spring啟動的早期階段(如bean定義注冊表后置處理環節)被提前創建了出來,則可能導致其中的配置項決議失敗,對此,我們一方面可以嘗試尋找規避手段,另一方面也可以從該bean本身的設計探究原因,
Spring后置處理器
處理器型別
說明
ConfigurationClassPostProcessor
bean定義注冊表后置處理器
在Spring Boot中,從包含了@SpringBootApplication注解的引導類開始,掃描并注冊bean定義至bean工廠,在本文案例中,MapperScannerConfigurer的bean定義便是在此時被注冊的,
MapperScannerConfigurer
bean定義注冊表后置處理器
掃描@Mapper注解標注的映射器介面,生成并注冊對應的MapperFactoryBean工廠bean定義,支持使用PropertySourcesPlaceholderConfigurer等處理器對自身的屬性欄位進行配置項決議,
PropertySourcesPlaceholderConfigurer
bean工廠后置處理器
遍歷bean定義,決議其中的配置項占位符,
ConfigurationBeanFactoryMetaData
bean工廠后置處理器
遍歷bean定義,記錄工廠方法等資訊,
HsfConsumerPostProcessor
bean工廠后置處理器
遍歷bean定義,對于被@HsfConsumer注解標注的屬性欄位,生成并注冊對應的HSFSpringConsumerBean工廠bean定義,
AutowiredAnnotationBeanPostProcessor
bean后置處理器
決議@Value注解中的配置項占位符,決議器之一由PropertySourcesPlaceholderConfigurer提供,
ConfigurationPropertiesBindingPostProcessor
bean后置處理器
決議@ConfigurationProperties注解中的配置項,對@Bean方法級別的注解決議借助ConfigurationBeanFactoryMetaData中的bean工廠方法記錄完成,
為了方便讀者理解,以上表格整理了文中提到的各類Spring后置處理器,以及它們彼此的關聯,可以看到,Spring框架在給我們提供了很多開發便利的同時,其整體的設計還是較為復雜的,在日常開發中,我們可能時不時會遇到一些「疑難雜癥」,而此時對框架的深入理解能幫助我們高效地解決問題,此外,善用對Spring代碼的除錯,也能幫助我們在紛繁的思路或線索中定位到問題原因,最后,由于寫作時間倉促,且Spring不同版本間可能存在一定的行為差異,文中如有錯漏之處還請讀者包涵指正,

注釋:

  1. 除了被@Configuration注解標注的類外,被@Component等注解標注的類也被Spring視為配置類,不過是輕量級(lite)配置類,參見《Spring Core Technologies》1.12章節 - Java-based Container Configuration,

  2. 參見《Spring實戰》6.1.1小節 - 理解Spring的環境抽象,

  3. 對于匹配到多個bean的情況,會優先取包含@Primary注解或者優先級高的bean,如果無法判斷,則會拋出NoUniqueBeanDefinitionException例外;對于沒有匹配到bean的情況,拋出NoSuchBeanDefinitionException例外,

  4. 具體代碼詳見AbstractAutowireCapableBeanFactory#getTypeForFactoryBean方法,

  5. 即呼叫BeanDefinitionRegistryPostProcessor介面中定義的postProcessBeanDefinitionRegistry方法,

  6. 具體代碼詳見AnnotationConfigUtils#registerAnnotationConfigProcessors方法,

  7. 某些bean工廠后置處理器也會向bean工廠中添加新的bean定義,比如我們后文將討論的HsfConsumerPostProcessor處理器,

  8. 參見《Spring Core Technologies》1.8.3小節 - 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月份被創建的,而根據《History of Spring Framework and Spring Boot》一文,Spring 0.9的發布時間為2003年6月,

  10. 具體代碼詳見AbstractAutowireCapableBeanFactory#populateBean方法,

  11. 具體代碼詳見AbstractAutowireCapableBeanFactory#initializeBean方法,

參考資料:

  1. 《Spring Core Technologies》:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html

  2. 《Spring實戰》:https://book.douban.com/subject/36142064/

  3. 《MyBatis 3原始碼深度決議》:https://book.douban.com/subject/34836563/

  4. 《History of Spring Framework and Spring Boot》:https://www.quickprogrammingtips.com/spring-boot/history-of-spring-framework-and-spring-boot.html

  5. 《SpringCloud、Nginx高并發核心編程》:https://book.douban.com/subject/35396578/

  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
  作者|楊天逸(在田)  

本文來自博客園,作者:古道輕風,轉載請注明原文鏈接:https://www.cnblogs.com/88223100/p/In-depth-Spring-configuration-item-issues-and-comprehensive-analysis.html

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/552910.html

標籤:其他

上一篇:例外體系與專案實踐

下一篇:返回列表

標籤雲
其他(159342) Python(38156) JavaScript(25439) Java(18078) C(15229) 區塊鏈(8267) C#(7972) AI(7469) 爪哇(7425) MySQL(7201) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5871) 数组(5741) R(5409) Linux(5340) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4573) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2433) ASP.NET(2403) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1975) 功能(1967) Web開發(1951) HtmlCss(1940) python-3.x(1918) C++(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1878) .NETCore(1861) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • 深入Spring配置項問題,全面決議

    本文就Spring配置項決議問題展開分析,這其中涉及到bean定義注冊表后置處理、bean工廠后置處理、工廠bean等Spring相關的概念。本文將以上述問題作為切入點,進行分析和展開介紹。 ......

    uj5u.com 2023-05-19 14:18:25 more
  • 例外體系與專案實踐

    程式中的例外可以反映系統的缺陷和待優化的點,并且是無法完全避免的,如何處理例外和降低例外出現的頻率,是系統質量的基礎保障;隨著分布式架構的流行,各種復雜的請求鏈路給例外處理帶來了巨大的麻煩; ......

    uj5u.com 2023-05-19 14:05:28 more
  • 從零玩轉Nginx

    01【熟悉】實際開發中的問題? 現在我們一個專案跑在一個tomcat里面 當一個tomcat無法支持高的并發量時。可以使用多個tomcat 那么這多個tomcat如何云分配請求 |-nginx 02【熟悉】服務器概述 1,目前常見的web服務器 1,Apache(http://httpd.apach ......

    uj5u.com 2023-05-19 13:41:51 more
  • Windows10安裝Jmeter(圖文教程)

    Apache JMeter是Apache組織開發的基于Java的壓力測驗工具。用于對軟體做壓力測驗,它最初被設計用于Web應用測驗,但后來擴展到其他測驗領域。 它可以用于測驗靜態和動態資源,例如靜態檔案、Java 小服務程式、CGI 腳本、Java 物件、資料庫、FTP 服務器, 等等。JMeter ......

    uj5u.com 2023-05-19 13:40:47 more
  • 37基于java的職工管理系統設計與實作

    基于java的職工管理系統設計與實作,員工管理系統,企業員工管理系統,公司員工管理系統,企業人事管理系統,基于java職工管理系統,前后端分離,員工考勤管理系統,職工獎懲管理系統,職員合同管理,HR管理系統,人事HR管理系統。 ......

    uj5u.com 2023-05-19 13:40:43 more
  • 實用教程丨如何將實時資料顯示在前端電子表格中(一)

    Author Alex Zhang Category SpreadJS Tags SpreadJS,前端電子表格,實時資料,RealTime Data 前言 資料(包括股票、天氣和體育比分)在不斷更新為新資訊時最為有用。SpreadJS是一個非常通用的 JavaScript 電子表格組件,它還可以輕 ......

    uj5u.com 2023-05-19 13:40:33 more
  • 希望所有計算機專業同學都知道這些老師

    C語言教程——翁凱老師、赫斌 翁愷老師是土生土長的浙大碼農,從本科到博士都畢業于浙大計算機系,后來留校教書,一教就是20多年。 翁愷老師的c語言課程非常好,講解特別有趣,很適合初學者學習。 郝斌老師的思路是以初學者的思路來思考的,非常適合小白,你不理解的問題,基本上他都會詳細說一下。 C++——侯捷 ......

    uj5u.com 2023-05-19 13:39:42 more
  • 36基于java的醫院管理系統設計與實作

    基于java的醫院管理系統設計與實作,門診和住院管理系統,醫院住院管理系統,醫院門診系統,門診管理系統,醫院住院系統,藥房管理系統,藥品,掛號管理系統。 ......

    uj5u.com 2023-05-19 13:38:11 more
  • 深入Spring配置項問題,全面決議

    本文就Spring配置項決議問題展開分析,這其中涉及到bean定義注冊表后置處理、bean工廠后置處理、工廠bean等Spring相關的概念。本文將以上述問題作為切入點,進行分析和展開介紹。 ......

    uj5u.com 2023-05-19 13:37:36 more
  • 例外體系與專案實踐

    程式中的例外可以反映系統的缺陷和待優化的點,并且是無法完全避免的,如何處理例外和降低例外出現的頻率,是系統質量的基礎保障;隨著分布式架構的流行,各種復雜的請求鏈路給例外處理帶來了巨大的麻煩; ......

    uj5u.com 2023-05-19 13:22:34 more