1. 程式人生 > >配置類需要標註@Configuration卻不知原因?那這次就不能給你漲薪嘍

配置類需要標註@Configuration卻不知原因?那這次就不能給你漲薪嘍

> 專注Java領域分享、成長,拒絕淺嘗輒止。關注公眾號【**BAT的烏托邦**】開啟專欄式學習,拒絕淺嘗輒止。本文 [https://www.yourbatman.cn](https://www.yourbatman.cn) 已收錄,裡面一併有Spring技術棧、MyBatis、中介軟體等小而美的專欄供以學習哦。 [TOC] ![](https://img-blog.csdnimg.cn/2020070817144913.png) # 前言 各位小夥伴大家好,我是A哥。這是繼上篇文章:[真懂Spring的@Configuration配置類?你可能自我感覺太良好](https://mp.weixin.qq.com/s/rXy9T3VgWvdl6Kje1mwCZA) 的原理/原始碼解釋篇。按照本公眾號的定位,原理一般跑不了,雖然很枯燥,但還得做,畢竟做難事必有所得,真的掌握了才有底氣談漲薪嘛。 Tips:鑑於經常有些同學無法區分某個功能/某項能力屬於`Spring Framework`的還是`Spring Boot`,你可以參考文章裡的【版本約定】目錄,那裡會說明本文的版本依賴,也就是功能所屬嘍。比如本文內容它就屬於`Spring Framework`,和`Spring Boot`木有關係。 --- ## 版本約定 本文內容若沒做特殊說明,均基於以下版本: - JDK:`1.8` - Spring Framework:`5.2.2.RELEASE` --- # 正文 Spring的IoC就像個“大熔爐”,什麼都當作Bean放在裡面。然而,雖然它們都放在了一起,但是實際在功能上是有區別的,比如我們熟悉的`BeanPostProcessor`就屬於後置處理器功能的Bean,還有本文要討論的`@Configuration`配置Bean也屬於一種特殊的元件。 判斷一個Bean是否是**Bean的後置處理器**很方便,只需看它是否實現了`BeanPostProcessor`介面即可;那麼如何去確定一個Bean是否是@Configuration配置Bean呢?若是,如何區分是Full模式還是Lite模式呢?這便就是本文將要討論的內容。 --- ## 如何判斷一個元件是否是@Configuration配置? 首先需要明確:`@Configuration`配置前提必須是IoC管理的一個元件(也就是常說的Bean)。Spring使用`BeanDefinitionRegistry`註冊中心管理著所有的Bean定義資訊,那麼對於這些Bean資訊哪些屬於`@Configuration`配置呢,這是需要甄選出來的。 判斷一個Bean是否是`@Configuration`配置類這個邏輯統一交由`ConfigurationClassUtils`這個工具類去完成。 --- ## ConfigurationClassUtils工具類 見名之意,它是和配置有關的一個工具類,提供幾個靜態工具方法供以使用。它是`Spring 3.1`新增,對於它的作用,官方給的解釋是:用於標識`@Configuration`類的實用程式(Utilities)。它主要提供了一個方法:`checkConfigurationClassCandidate()`用於檢查給定的Bean定義是否是配置類的候選物件(或者在配置/元件類中宣告的巢狀元件類),**並做相應的標記**。 --- ### checkConfigurationClassCandidate() 它是一個public static工具方法,用於判斷某個Bean定義是否是`@Configuration`配置。 ```java ConfigurationClassUtils: public static boolean checkConfigurationClassCandidate(BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) { ... // 根據Bean定義資訊,拿到器對應的註解元資料 AnnotationMetadata metadata = xxx; ... // 根據註解元資料判斷該Bean定義是否是配置類。若是:那是Full模式還是Lite模式 Map config = metadata.getAnnotationAttributes(Configuration.class.getName()); if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL); } else if (config != null || isConfigurationCandidate(metadata)) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE); } else { return false; } ... // 到這。它肯定是一個完整配置(Full or Lite) 這裡進一步把@Order排序值放上去 Integer order = getOrder(metadata); if (order != null) { beanDef.setAttribute(ORDER_ATTRIBUTE, order); } return true; } ``` 步驟總結: 1. 根據Bean定義資訊解析成為一個註解元資料物件`AnnotationMetadata metadata` 1. 可能是個`AnnotatedBeanDefinition`,也可能是個`StandardAnnotationMetadata` 2. 根據註解元資料metadata判斷是否是個`@Configuration`配置類,有如下三種可能case: 1. 標註有`@Configuration`註解**並且**該註解的`proxyBeanMethods = false`,那麼mark一下它是**Full模式**的配置。否則進入下一步判斷 2. 標註有`@Configuration`註解**或者**符合Lite模式的條件(上文有說一共有5種可能是Lite模式,原始碼處在`isConfigurationCandidate(metadata)`這個方法裡表述),那麼mark一下它是**Lite模式**的配置。否則進入下一步判斷 3. 不是配置類,並且返回結果`return false` 3. 能進行到這一步,說明該Bean肯定是個配置類了(Full模式或者Lite模式),那就取出其`@Order`值(若有的話),然後mark進Bean定義裡面去 **這個mark動作很有意義:後面判斷一個配置類是Full模式還是Lite模式,甚至判斷它是否是個配置類均可通過`beanDef.getAttribute(CONFIGURATION_CLASS_ATTRIBUTE)`這樣完成判斷**。 --- #### 方法使用處 知曉了`checkConfigurationClassCandidate()`能夠判斷一個Bean(定義)是否是一個配置類,那麼它在什麼時候會被使用呢?通過查詢可以發現它被如下兩處使用到: - 使用處:`ConfigurationClassPostProcessor.processConfigBeanDefinitions()`處理配置Bean定義階段。 ```java ConfigurationClassPostProcessor: public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { // 拿出當前所有的Bean定義資訊,一個個的檢查是否是配置類 String[] candidateNames = registry.getBeanDefinitionNames(); for (String beanName : candidateNames) { BeanDefinition beanDef = registry.getBeanDefinition(beanName); if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) { logger.debug("Bean definition has already been processed as a configuration class: " + beanDef); } // 如果該Bean定義不是配置類,那就繼續判斷一次它是否是配置類,若是就加入結果集合裡 else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) { configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); } } ... } ``` `ConfigurationClassPostProcessor`是個`BeanDefinitionRegistryPostProcessor`,會在`BeanFactory` **準備好後**執行生命週期方法。因此自然而然的,`checkConfigurationClassCandidate()`會在此階段呼叫,用於區分出哪些是配置Bean。 **值得注意的是**:`ConfigurationClassPostProcessor`的執行時期是非常早期的(`BeanFactory`準備好後就執行嘛),這個時候容器內的Bean定義**很少**。這個時候只有**主配置類**才被註冊了進來,那些想通過`@ComponentScan`掃進來的配置類都還沒到“時間”,這個時間節點很重要,請注意區分。為了方便你理解,我分別把Spring和Spring Boot在此階段的Bean定義資訊截圖展示如下: ![](https://img-blog.csdnimg.cn/20200516172229283.png) 以上是Spring環境,對應程式碼為: ```java new AnnotationConfigApplicationContext(AppConfig.class); ``` ![](https://img-blog.csdnimg.cn/20200516172449407.png) 以上是Spring Boot環境,對應程式碼為: ```java @SpringBootApplication public class Boot2Demo1Application { public static void main(String[] args) { SpringApplication.run(Boot2Demo1Application.class, args); } } ``` > 相比之下,Spring Boot裡多了`internalCachingMetadataReaderFactory`這個Bean定義。原因是SB定義了一個`CachingMetadataReaderFactoryPostProcessor`把它放進去的,由於此Processor也是個`BeanDefinitionRegistryPostProcessor`並且order值為`Ordered.HIGHEST_PRECEDENCE`,所以它會優先於`ConfigurationClassPostProcessor`執行把它註冊進去~ - 使用處:`ConfigurationClassParser.doProcessConfigurationClass()` **解析** `@Configuration`配置類階段。所處的大階段同上使用處,仍舊是`ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry()`階段 ```java ConfigurationClassParser: @Nullable protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException { ... // 先解析nested內部類(內部類會存在@Bean方法嘛~) ... // 解析@PropertySource資源,加入到environment環境 ... // 解析@ComponentScan註解,把元件掃描進來 scannedBeanDefinitions = ComponentScanAnnotationParser.parse(componentScan, ...); // 把掃描到的Bean定義資訊依舊需要一個個的判斷,是否是配置類 // 若是配置類,就繼續當作一個@Configuration配置類來解析parse() 遞迴嘛 for (BeanDefinitionHolder holder : scannedBeanDefinitions) { ... if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) { parse(bdCand.getBeanClassName(), holder.getBeanName()); } } ... // 解析@Import註解 ... // 解析@ImportResource註解 ... // 解析當前配置裡配置的@Bean方法 ... // 解析介面預設方法(因為配置類可能實現介面,然後介面預設方法可能標註有@Bean ) ... // 處理父類(遞迴,直到父類為java.打頭的為止) } ``` 這個方法是Spring對配置類解析的**最核心步驟**,通過它順帶也能夠解答你的疑惑了吧:為何你僅需在類上標註一個`@Configuration`註解即可讓它成為一個配置類?因為被Scan掃描進去了嘛~ 通過以上**兩個使用處**的分析和對比,對於`@Configuration`配置類的理解,你至少應該掌握瞭如下訊息: 1. `@Configuration`配置類肯定是個元件,存在於IoC容器裡 2. `@Configuration`配置類是**有主次之分**的,主配置類是驅動整個程式的入口,可以是一個,也可以是多個(若存在多個,支援使用@Order排序) 3. 我們平時一般只書寫**次配置類**(而且一般寫多個),它**一般**是藉助主配置類的`@ComponentScan`能力完成載入進而解析的(當然也可能是`@Import`、又或是被其它次配置類驅動的) 4. 配置類可以存在巢狀(如內部類),繼承,實現介面等特性 聊完了最為重要的`checkConfigurationClassCandidate()`方法,當然還有必要看看`ConfigurationClassUtils`的另一個工具方法`isConfigurationCandidate()`。 --- ### isConfigurationCandidate() 它是一個public static工具方法,通過給定的註解元資料資訊來判斷它是否是一個`Configuration`。 ```java ConfigurationClassUtils: static { candidateIndicators.add(Component.class.getName()); candidateIndicators.add(ComponentScan.class.getName()); candidateIndicators.add(Import.class.getName()); candidateIndicators.add(ImportResource.class.getName()); } public static boolean isConfigurationCandidate(AnnotationMetadata metadata) { // 不考慮介面 or 註解 說明:註解的話也是一種“特殊”的介面哦 if (metadata.isInterface()) { return false; } // 只要該類上標註有以上4個註解任意一個,都算配置類 for (String indicator : candidateIndicators) { if (metadata.isAnnotated(indicator)) { return true; } } // 若一個註解都沒標註,那就看有木有@Bean方法 若有那也算配置類 return metadata.hasAnnotatedMethods(Bean.class.getName()); } ``` 步驟總結: 1. 若是介面型別(含註解型別),直接不予考慮,返回false。否則繼續判斷 2. 若此類上標註有`@Component、@ComponentScan、@Import、@ImportResource`任意一個註解,就判斷成功返回true。否則繼續判斷 3. 到此步,就說明**此類上沒有標註任何註解**。若存在@Bean方法,返回true,否則返回false。 **需要特別特別特別注意的是:此方法它的並不考慮`@Configuration`註解,是“輕量級”判斷,這是它和`checkConfigurationClassCandidate()`方法的最主要區別**。當然,後者依賴於前者,依賴它來根據註解元資料判斷是否是Lite模式的配置。 --- ## Spring 5.2.0版本變化說明 因為本文的講解和程式碼均是基於`Spring 5.2.2.RELEASE`的,而並不是所有小夥伴都會用到這麼新的版本。關於此部分的實現,以Spring 5.2.0版本為分界線實現上有些許差異,所以在此處做出說明。 --- ### proxyBeanMethods屬性的作用 `proxyBeanMethods`屬性是Spring 5.2.0版本為`@Configuration`註解新增加的一個屬性: ```java @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Configuration { @AliasFor(annotation = Component.class) String value() default ""; // @since 5.2 boolean proxyBeanMethods() default true; } ``` 它的作用是:是否允許代理@Bean方法。說白了:決定此配置使用Full模式還是Lite模式。為了保持向下相容,`proxyBeanMethods`的預設值是true,使用Full模式配置。 Spring 5.2提出了這個屬性項,是期望你在已經瞭解了它的作用之後,顯示的把它置為false的,因為在雲原生將要到來的今天,啟動速度方面Spring一直在做著努力,也希望你能配合嘛。這不`Spring Boot`就“配合”得很好,它在2.2.0版本(依賴於Spring 5.2.0)起就把它的所有的自動配置類的此屬性改為了false,即`@Configuration(proxyBeanMethods = false)`。 --- ### Full模式/Lite模式實現上的差異 由於Spring 5.2.0新增了`proxyBeanMethods`屬性來控制模式,因此實現上也有些許詫異,請各位注意甄別: Spring 5.2.0+版本判斷實現: ```java ConfigurationClassUtils: Map config = metadata.getAnnotationAttributes(Configuration.class.getName()); if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL); } else if (config != null || isConfigurationCandidate(metadata)) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE); } else { return false; } ``` Spring 5.2.0-版本判斷實現: ```java ConfigurationClassUtils: if (isFullConfigurationCandidate(metadata)) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL); } else if (isLiteConfigurationCandidate(metadata)) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE); } else { return false; } ``` --- ## 思考題? 1. 既然`isConfigurationCandidate()`判斷方法是為`checkConfigurationClassCandidate()`服務,那Spring為何也把它設計為public static呢? 2. `ConfigurationClassUtils`裡還存在對`@Order`順序的解析方法,不是說Spring的Bean是無序的嗎?這又如何理解呢? --- # 總結 本文作為[上篇文章](https://mp.weixin.qq.com/s/rXy9T3VgWvdl6Kje1mwCZA)的續篇,解釋了@Configuration配置的Full模式和Lite模式的判斷原理,同時順帶的也介紹了什麼叫**主配置配和次配置類**,這個概念(雖然官方並不這麼叫)對你理解Spring Framework是非常有幫助的。如果你使用是基於Spring 5.2.0+的版本,在瞭解了這兩篇文章內容的基礎上,建議你的配置類均採用Lite模式去做,即顯示設定`proxyBeanMethods = false`。 另外關於此部分內容,有些更為感興趣的小夥伴問到:為什麼Full模式下通過方法呼叫指向的仍舊是原來的Bean,保證了只會執行一次呢?開啟的是Full模式這只是表象原因,想要回答此問題需要**涉及到CGLIB增強實現的深水區**內容,為了滿足這些好奇(好學)的娃子,計劃會在下篇文章繼續再拿一篇專程講解(預計篇幅不短,萬字以上),你可訂閱我的公眾號持續保持關注。