Autowired沒法正常注入的疑難雜症

前言

最近項目在整合shiro權限認證模塊時,給本身挖了一個深坑,也是分析了很久才定位到問題的所在,根本緣由仍是對spring相關的技術點掌握的不夠嫺熟。本文基於springboot 2.1.5進行分析。下面會用簡單的Demo去還原問題的場景。java

示例

簡單將遇到的問題還原一下,這段代碼中ShiroProperties 始終注入不到TestController中去。也就是shiroProperties始終是null。spring

1@ConfigurationProperties(prefix = "test")
2@Data
3public class ShiroProperties {
4
5    private String name;
6    private List<String> chain;
7}
複製代碼

1@Configuration
2@EnableConfigurationProperties(ShiroProperties.class)
3public class ShiroConfiguration {
4}
複製代碼

 1@Controller
2public class TestController {
3
4    @Autowired
5    private ShiroProperties shiroProperties;
6
7    @Bean
8    public Test test(){
9        System.out.println(shiroProperties);
10        return new Test();
11    }
12
13    @Bean
14    private TestProcessor testProcessor(){
15        return new TestProcessor();
16    }
17}
複製代碼

1public class Test {
2}
複製代碼

 1public class TestProcessor implements
2        BeanFactoryPostProcessorPriorityOrdered 
{
3
4    @Override
5    public void postProcessBeanFactory
6            (ConfigurableListableBeanFactory beanFactory) throws BeansException 
{
7
8    }
9
10    @Override
11    public int getOrder() {
12        return 0;
13    }
14}
複製代碼

配置文件application.properties:緩存

test.name=mars
test.chain[0]=aa
test.chain[1]=bbspringboot

初始定位

最開始問題定位爲ShiroProperties 沒有註冊到容器中,因此注入時經過getBean沒法取到相應的bean。可是Debug也會發現ShiroProperties 的BeanDefinition已經在容器中持有,因此這個方向基本被排除了。
併發

重定位

從新定位問題,暫時定位在populateBean屬性注入時初始化ShiroProperties 失敗。後面debug查看了一下populateBean時的TestController的BeanDefinition,發現ShiroProperties 這個屬性根本沒有在依賴注入的範圍內。因此壓根就沒初始化。
app


因此這裏我們就須要分析一下Autowired的流程了,通常狀況下,在getBean時會經過 元數據後置處理器去取出類中須要依賴的其餘類,也就是取出@Autowired註解的屬性,放到BeanDefinition的 propertyValues屬性中。

@Autowired注入流程

例如@Autowired註解的屬性會經過類AutowiredAnnotationBeanPostProcessor去處理。我們簡單看一下AutowiredAnnotationBeanPostProcessor的構造函數。less

實例化的時候就會將Autowired和Value給緩存在一個autowiredAnnotationTypes集合中。在後面進行BeanDefinition合併的時候,會遍歷autowiredAnnotationTypes集合,取出註解對應的字段(例如Autowired和Value對應的字段),最後存放到BeanDefinition的propertyValues屬性中,供後面的populateBean調用時進行屬性的注入。具體調用方法以下圖:ide

上面的方法主要乾了兩個活:函數

  1. 取出要注入的元數據,即相似@Autowired註解的屬性,配合上面的例子就是shiroProperties屬性。
  2. 將這些屬性注入到beanDefinition的propertyValues屬性中。

因此這裏我們須要把思路往前推一下,須要判斷出了什麼問題纔會致使TestController的propertyValues屬性爲空。beanDefinition的合併發生在doCreateBean方法中。以下圖:源碼分析

經過這個方法進去,我們將全部的BeanPostProcessor打印出來,結果以下圖所示,並無AutowiredAnnotationBeanPostProcessor這個後置處理器。這也就能解釋我們的ShiroProperties爲啥注入不進去了,雖然找到了緣由,可是這不是一個正常的結果,正常狀況下依賴注入都是沒問題的,畢竟依賴注入是Spring的核心三大板塊之一。

這裏我們深刻分析一下AutowiredAnnotationBeanPostProcessor這個類是啥時候註冊到Spring容器的。

衆所周知,在容器初始化的過程當中,有一個關鍵性的方法refresh,在refresh方法調用過程當中,會調用invokeBeanFactoryPostProcessors方法,全部的後置處理器都在這裏進行初始化。我們看看一共有多少個BeanPostProcessor:

從上圖能夠觀察到正常狀況下全部的postProcessorNames都是會被註冊到Spring容器中的。可是這裏有個例外,從上面的圖片能夠看出還有個testProcessor也在其中,這個是我們自定義的BeanPostProcessor。Spring在註冊這些BeanPostProcessor時會按一種規則去註冊:

  1. 先註冊實現了PriorityOrdered接口的BeanPostProcessor。
  2. 再註冊實現了Ordered接口的BeanPostProcessor。
  3. 最後註冊無順序的BeanPostProcessor。

因此TestProcessor會在AutowiredAnnotationBeanPostProcessor以前進行註冊,而TestProcessor是在TestController中的,因此說TestController做爲TestProcessor的factoryBean,固然要先進行初始化,這樣最後致使TestController在初始化時沒法正常使用AutowiredAnnotationBeanPostProcessor的功能,而後使得ShiroProperties沒法正常注入。

到這裏一切疑惑迎刃而解~,針對上述問題,我們能夠另起一個無需初始化的PostProcessorConfig類去專門處理相似的BeanPostProcessor便可。

科普PriorityOrdered接口

看一下Spring對於PriorityOrdered這個接口的註釋

Note: {@code PriorityOrdered} post-processor beans are initialized in a special phase, ahead of other post-processor beans. This subtly affects their autowiring behavior: they will only be autowired against beans which do not require eager initialization for type matching.

其實官方也有給我們提示這一點的,用了這個接口的後置處理器將會影響依賴注入的過程,推薦放在不須要初始化的bean中進行裝配。

PS:固然,這種狀況通常也遇不到,只是最近在整合shiro時,shiro有提供一個LifecycleBeanPostProcessor 處理器去管理相關bean的生命週期,須要註冊到Spring容器。而後就致使上面的狀況,和LifecycleBeanPostProcessor 在的同一個類的@Autowired,@Value聲明的字段均沒法正常注入。


下面是關於這個問題可能涉及到的一些源碼的分析。

源碼分析

通常狀況下,有兩種組合可使用。

  1. @ConfigurationProperties與@EnableConfigurationProperties進行配合使用。
  2. @ConfigurationProperties與@Configuration進行配合使用。

兩則實現的目的一致,都是將@ConfigurationProperties註解的類交由Spring容器進行託管,在容器中註冊BeanDefinition,以供後期bean的裝配和獲取。

下面的內容將以第一種組合展開進行講解。咱們能夠先看看@EnableConfigurationProperties這個註解。

 1@Target(ElementType.TYPE)
2@Retention(RetentionPolicy.RUNTIME)
3@Documented
4@Import(EnableConfigurationPropertiesImportSelector.class)
5public @interface EnableConfigurationProperties {
6
7    /**
8     * Convenient way to quickly register {@link ConfigurationProperties} annotated beans
9     * with Spring. Standard Spring Beans will also be scanned regardless of this value.
10     * @return {@link ConfigurationProperties} annotated beans to register
11     */

12    Class<?>[] value() default {};
13
14}
複製代碼

這個註解上有@Import(EnableConfigurationPropertiesImportSelector.class)這樣一個註解,對於@Import這個註解,感興趣的朋友能夠去看看ConfigurationClassParser中的processImports方法。大概流程就是調用EnableConfigurationPropertiesImportSelector中的selectImports方法,並將解析出來的全部類註冊到容器中。

這裏一共是將兩個類注入到了容器中。他們都實現了ImportBeanDefinitionRegistrar接口,這個接口只有一個方法registerBeanDefinitions,看方法名也就略知一二了吧。

ConfigurationPropertiesBeanRegistrar

ConfigurationPropertiesBeanRegistrar這個類的主要目的是用來收集EnableConfigurationProperties 中value值指定的類,並將其註冊到容器中。代碼量並很少,我截取下關鍵性代碼。


一共分爲兩步:

  • getTypes:經過註解@EnableConfigurationProperties獲取其value中的值。
  • register:將上面獲取的Class[]註冊到Spring容器中。
ConfigurationPropertiesBindingPostProcessorRegistrar

ConfigurationPropertiesBindingPostProcessorRegistrar這個類註冊了兩個後置處理器。

從代碼不難看出,上面的邏輯在一個容器中有且只會執行一次。執行過程當中會註冊一個實現BeanPostProcessor的bean後置處理器,還會註冊一個實現BeanFactoryPostProcessor的beanFactory後置處理器。

  • ConfigurationBeanFactoryMetadata:將beanFactory中全部的beandefinition都保存一份到beansFactoryMetadata集合中。
  • ConfigurationPropertiesBindingPostProcessor:這個類是處理ConfigurationProperties的重點類,它將會幫咱們解析配置文件裏面的配置並賦值到bean中。
ConfigurationPropertiesBindingPostProcessor
ConfigurationPropertiesBindingPostProcessor

綁定具體實體類和配置就在 bind方法中進行。解析過程略顯複雜,這裏不作過多說明。

觸發點

前面介紹了兩個ImportBeanDefinitionRegistrar接口的實現類,可是registerBeanDefinitions方法的觸發點還未揭祕。瞭解過spring源碼的同窗想必對refresh這個方法應該不陌生。我們從這裏開始挖掘一波~

refresh中有一個方法invokeBeanFactoryPostProcessors

這個是觸發點的入口,一步步點進去,直到PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors方法,這裏面會執行一些定義好的BeanFactoryPostProcessor中的postProcessBeanDefinitionRegistry方法,
我們要關注的就是ConfigurationClassPostProcessor這個類。這個類其實幹了不少活,包括前面提到的對於@Import這個註解的處理等等。

概括成爲三步,每一步都內嵌在前一步中:

  1. this.reader.loadBeanDefinitions(configClasses);
  2. loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);控制是否跳過一些bean的處理,例如我們有時候會配置@ConditionalOnBean @ConditionalOnClass等等條件,若不知足,則直接跳過這些bean的處理。
  3. loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
1    private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
2        registrars.forEach((registrar, metadata) ->
3                registrar.registerBeanDefinitions(metadata, this.registry));
4    }
複製代碼

這一塊就是我們的registerBeanDefinitions方法觸發的地方了。也與前面一塊的講解也就串起來了。


總結

解決問題的同時也是對本身成長的一種促進,此次源碼分析補充了前面不少強行看源碼時的一些疑惑點。畢竟spring盤子太大了,不必定全部的使用注意事項都會在官方文檔加以註釋,遇見了搜索引擎解決不了的問題仍是得本身手擼源碼------>O(∩_∩)O哈哈~。

End

相關文章
相關標籤/搜索