開篇說明:
注解本身並沒有什么實際的功能(非要說標記也是一個“實際”的功能的話,也可以算吧),隱藏在背后的注解處理器才是實現注解機制的核心。本篇將從這兩個層面出發探索 spring boot 自動裝配的秘密,並使用 spring boot 的自動裝配機制來實現自動裝配。
本次代碼已經放到 github:https://github.com/christmad/code-share/tree/master/spring-boot-config-practice
代碼中主要是做了 @Configuration 和 @ConfigurationProperties 使用的練習,以及本篇博客筆記重點:自定義 ImportSelector 實現類實現批量掃描包下類文件。
spring boot 自動裝配有幾個核心的“事件”需要理解:
(1) 首先,從 spring 3.X 開始,AnnotationConfigApplicationContext 替代 ClassPathXmlApplicationContext,迎來了全新的 java bean config 配置方式,使用 java bean 和 注解就能輕松添加配置。
(2) 3.X 開始提供的致力於零配置文件的注解:
@Configuration——用來替代 xml 文件的。
@Bean——標記在方法上,替代 xml 配置中的 <bean></bean> 定義,方法名稱就是 bean id。
@Import——將 Bean 導入到容器的 BeanDefinition Map 中,可以接收 Class[] 數組,通常只用它來導入 1~2 個類,不適合批量導入場景。
但是 @Import 適合用來做“啟動”裝配的動作,配置不會無中生有,不可能所有的配置步驟都是自動的,必須有個起點的地方是手動的“硬編碼”,就像我們剛接觸 window 操作系統時了解到有很多系統缺省值一樣它們是寫死的硬編碼。而 @Import 就能起到這個作用。其實不需要這個注解 spring boot 也能實現自動裝配,只不過作為一個開源框架,使用 @Import 更能突出需要導入的意圖和需求,讓框架變得更好理解。
另外一個 ImportSelector 接口的 selectImports() 方法可以批量導入。算是 spring boot 能夠完成自動配置的一個關鍵注解。
@Conditional——spring 4.0 起提供,spring boot 1.X 版本應該是基於 spring 4.0+ 而誕生的,這個注解起到了條件標記的作用,其衍生的注解在 spring boot 自動配置中也起到了一個關鍵的作用,常用的比如 @ConditionalOnClass、@ConditionalOnMissClass、@ConditionalOnBean、@ConditionalOnMissingBean、@ConditionalOnProperty 等。在分析和實戰環節中會用到其中某幾個注解。
(3) 一些新的注解——組合注解的效果,比如 @SpringBootApplication 融合了 @SpringBootConfiguration(即 @Configuration)、@EnableAutoConfiguration(依賴 @Import,間接依賴 @Conditional)、@ComponentScan 等幾個注解。因為組合注解的存在,我們才可以在 @SpringBootApplication 標記的類里面使用 @Bean 等注解,而不用擔心識別不了。@EnableAutoConfiguration 這個注解也是接下來會重點分析到的。
由於 Pivotal 團隊牛人比較多,而且寫 spring boot 框架的人不止一個(spring 3.X 版本開始代碼開始規范和優化了,並一直積累到現在,代碼量非常大),所以很多騷操作的細節在本篇不會深入。
從 @SpringBootApplication 注解開始分析:
前面說了,@SpringBootApplication 融合了 @SpringBootConfiguration(即 @Configuration)、@EnableAutoConfiguration(依賴 @Import,間接依賴 @Conditional)、@ComponentScan 等幾個注解。
(1)@SpringBootConfiguration 注解就沒什么好說的了,直接是在 @Configuration 注解上派生的注解,多了一層包裝而已
(2)@EnableAutoConfiguration 注解是個組合注解,里面對我們有用的注解有兩個
2.1 @AutoConfigurationPackage
2.2 @Import(AutoConfigurationImportSelector.class)
2.1.1 @AutoConfigurationPackage分析:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import(AutoConfigurationPackages.Registrar.class) public @interface AutoConfigurationPackage { }
看了下 AutoConfigurationPackages.Registrar 這個類的代碼,比較少,實現兩個接口 ImportBeanDefinitionRegistrar, DeterminableImports 。直接在其中一個方法上打上一個斷點開始 debug 起來...
為什么沒有在下面那個方法打斷點呢??這其實是一個看源碼的小技巧。
可以看到下面那個 determineImports 方法,只是返回了一個集合,相當於 new 了一個東西,就算斷點跑到它上面去了,后面跟着斷點返回出去也還可能是一些創建對象的代碼,或者其他和你本次想調試的行為無關的代碼,而且不得不說的是,spring 代碼的結構非常深,如果打錯斷點就很可能在某幾個方法里調試幾天都沒調試出來......
好了,接着讓斷點執行一行,你可以在 debug 面板 Variables 欄里面看到 registry 對象的 beanDefinitionNames 屬性變藍了,beanDefinitionNames 被修改了,如下圖:
最后查看 beanDefinitionNames,可以發現 AutoConfigurationPackages.Registrar 只是將 AutoConfigurationPackages 注冊到 IOC BeanDefinition 中。而在這之前,我自己在項目中配置的一些 bean 已提前注冊了。斷點停在這里往上找 debug 的調用棧(emmm,從圖上看是往下找),如下圖,驗證了開篇說的 spring boot 使用 AnnotationConfigApplicationContext 作為 ApplicationContext 實現類:
SpringApplication 這個類定義了一些騷操作,模仿 spring IOC 的一些 prepareContext、refreshContext 流程,如上圖左側那些 refresh 方法分別有不同的類在實現,在調用到 AbstractApplicationContext#refresh() 方法之前,SpringApplication 還做了很多工作,不是本次討論重點。
2.1.2 @AutoConfigurationPackage分析結果:
目前看起來,@AutoConfigurationPackage 注解的作用是把 AutoConfigurationPackages 注冊到 IOC BeanDefinition 中。
這個過程從 debug 來看屬於 AbstractApplicationContext#refresh() 中的 invokeBeanFactoryPostProcessors(beanFactory); 流程。
在這個流程中可以對 BeanFactory 中的 BeanDefinition 進行修改,相當於修改房屋構造圖。之后的流程會用 BeanDefinition 去創建一個個實例,然后會用到 BeanPostProcessor——屬於在 java 實例的基礎上修改的層面了,屋子本來不通風的現在想換通風的也換不了了,但是里面的家具或者裝修風格還可以更換,嗯,換完之后可能會住的舒服點。
2.2.1 @Import(AutoConfigurationImportSelector.class) 分析:
前面說到“ImportSelector 接口的 selectImports() 方法可以批量導入”,下面就來 debug 一下源碼,如果順利的話可以找到 @Import 注解的處理器,最次也能了解 selectImports() 的實現過程,嗯。
先到 AutoConfigurationImportSelector 的 selectImports() 方法里打一個斷點......如下圖:
嗯???結果斷點沒停在這里???糾結了一陣之后,我開始猜想是不是 spring boot autoconfig 包把實現又換了......目前我用的是 2.1.8.RELEASE 版本。既然 debug 時沒有停在預想的地方,但是這個類其實又沒有被替換掉,那應該會運行到其他方法上面去了,所以我們可以換個方法打斷點......經過嘗試,發現斷點進入到了 AutoConfigurationSelectImportor#getAutoConfigurationEntry() 方法中。
順着斷點往上找,找到了 2.0.X 和 2.1.X 版本之間的差異。可以看到方法調用邏輯變了,下面是 2.1.0.RELEASE 版本中 AutoConfigurationSelectImportor$AutoConfigurationGroup#process() 的代碼:
1 @Override 2 public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) { 3 Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector, 4 () -> String.format("Only %s implementations are supported, got %s", 5 AutoConfigurationImportSelector.class.getSimpleName(), 6 deferredImportSelector.getClass().getName())); 7 AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector) 8 .getAutoConfigurationEntry(getAutoConfigurationMetadata(), annotationMetadata); 9 this.autoConfigurationEntries.add(autoConfigurationEntry); 10 for (String importClassName : autoConfigurationEntry.getConfigurations()) { 11 this.entries.putIfAbsent(importClassName, annotationMetadata); 12 } 13 }
需要注意上面代碼的 第7~第8行。同樣的函數在 2.0.9.RELEASE 版本的代碼如下:
1 @Override 2 public void process(AnnotationMetadata annotationMetadata, 3 DeferredImportSelector deferredImportSelector) { 4 String[] imports = deferredImportSelector.selectImports(annotationMetadata); 5 for (String importClassName : imports) { 6 this.entries.put(importClassName, annotationMetadata); 7 } 8 }
現在知道, spring-boot-autoconfig 2.1.0.RELEASE 及以后的版本中 AutoConfigurationSelectImportor#selectImports() 方法已經不再被調用了。在此方法中打斷點,直到項目啟動完也沒有進去過,間接證實了猜想。雖然方法路徑替換了,但是實現是幾乎一模一樣的。將兩個版本的代碼copy如下:
AutoConfigurationSelectImportor#getAutoConfigurationEntry() 方法代碼(PS:spring-boot-autoconfig-2.1.X.RELEASE):
1 protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, 2 AnnotationMetadata annotationMetadata) { 3 if (!isEnabled(annotationMetadata)) { 4 return EMPTY_ENTRY; 5 } 6 AnnotationAttributes attributes = getAttributes(annotationMetadata); 7 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); 8 configurations = removeDuplicates(configurations); 9 Set<String> exclusions = getExclusions(annotationMetadata, attributes); 10 checkExcludedClasses(configurations, exclusions); 11 configurations.removeAll(exclusions); 12 configurations = filter(configurations, autoConfigurationMetadata); 13 fireAutoConfigurationImportEvents(configurations, exclusions); 14 return new AutoConfigurationEntry(configurations, exclusions); 15 }
AutoConfigurationSelectImportor#selectImports() 方法代碼(PS:spring-boot-autoconfig-2.0.X.RELEASE 及以下):
1 @Override 2 public String[] selectImports(AnnotationMetadata annotationMetadata) { 3 if (!isEnabled(annotationMetadata)) { 4 return NO_IMPORTS; 5 } 6 AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader 7 .loadMetadata(this.beanClassLoader); 8 AnnotationAttributes attributes = getAttributes(annotationMetadata); 9 List<String> configurations = getCandidateConfigurations(annotationMetadata, 10 attributes); 11 configurations = removeDuplicates(configurations); 12 Set<String> exclusions = getExclusions(annotationMetadata, attributes); 13 checkExcludedClasses(configurations, exclusions); 14 configurations.removeAll(exclusions); 15 configurations = filter(configurations, autoConfigurationMetadata); 16 fireAutoConfigurationImportEvents(configurations, exclusions); 17 return StringUtils.toStringArray(configurations); 18 }
那么現在來看看 getAutoConfigurationEntry(舊版本 selectImports())方法中做了什么事情:
在這個方法中,有幾行代碼需要關注:
第一行:List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); 這行代碼聲明的 configurations 變量,會在方法最后返回值 entry 中用到,是調試需要關注的一個重點之一。
第二行:configurations = filter(configurations, autoConfigurationMetadata); 這行代碼的方法名明顯地告訴我們將會進行一些過濾策略,這個方法就是為什么 autoconfig 不會幫你配置不需要的 bean 的原因所在,里面用到了 @Conditional 條件來過濾,一些上下文條件不符合的 bean 不會幫你注冊到 IOC 中。
先看下 getCandidateConfigurations 代碼:
1 protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { 2 List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), 3 getBeanClassLoader()); 4 Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you " 5 + "are using a custom packaging, make sure that file is correct."); 6 return configurations; 7 }
看到上面方法中的第一行有個 SpringFactoriesLoader ,從結果逆向來看這個名字起得很好,它的名字起得和要加載的文件名一模一樣,SpringFactoriesLoader#loadFactoryNames() 做的事情就是去 ./META-INF/spring.factories 文件中加載一些配置,下面一行代碼的 Assert 工具也能說明這點。那么在 spring-boot-autoconfig jar 包下 META-INF/spring.factories 文件里的配置長什么樣?我們點開來看一下:
文件中的配置是以一個KEY多個VAL形式的映射存在。經過 getCandidateConfigurations 方法之后,spring.factories 文件中為 EnableAutoConfiguration 配置的自動裝配類的全類名都被加載出來了,全類名是為后面實例化這些自動裝配類做准備。對 spring.factories 文件進行加載的時候,spring 團隊做了一些騷操作,做了個緩存,防止該文件被讀取多次消耗性能。反正我在 debug 的時候代碼從 cache.get() 那個地方進去了,說明前面某個地方進行了掃描 spring.factories 這個動作。
在下面分析中我會挑一個最近在用的 RabbitAutoConfiguration 來說明這些自動裝配類到底是怎么用的。
自動裝配魔法的 filter 方法:
前面說完 getCandidateConfigurations 方法,現在結合 RabbitAutoConfiguration 這個自動裝配類來分析下在 filter(configurations, autoConfigurationMetadata); 這個過濾方法中做了什么。
先看一眼 filter 方法長什么樣:
1 private List<String> filter(List<String> configurations, AutoConfigurationMetadata autoConfigurationMetadata) { 2 long startTime = System.nanoTime(); 3 String[] candidates = StringUtils.toStringArray(configurations); 4 boolean[] skip = new boolean[candidates.length]; 5 boolean skipped = false; 6 for (AutoConfigurationImportFilter filter : getAutoConfigurationImportFilters()) { 7 invokeAwareMethods(filter); 8 boolean[] match = filter.match(candidates, autoConfigurationMetadata); 9 for (int i = 0; i < match.length; i++) { 10 if (!match[i]) { 11 skip[i] = true; 12 candidates[i] = null; 13 skipped = true; 14 } 15 } 16 } 17 if (!skipped) { 18 return configurations; 19 } 20 List<String> result = new ArrayList<>(candidates.length); 21 for (int i = 0; i < candidates.length; i++) { 22 if (!skip[i]) { 23 result.add(candidates[i]); 24 } 25 } 26 if (logger.isTraceEnabled()) { 27 int numberFiltered = configurations.size() - result.size(); 28 logger.trace("Filtered " + numberFiltered + " auto configuration class in " 29 + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) + " ms"); 30 } 31 return new ArrayList<>(result); 32 }
getAutoConfigurationImportFilters() 方法長這樣:
1 protected List<AutoConfigurationImportFilter> getAutoConfigurationImportFilters() { 2 return SpringFactoriesLoader.loadFactories(AutoConfigurationImportFilter.class, this.beanClassLoader); 3 }
很熟悉吧?這個操作是從 META-INF/spring.factories 文件中加載一些類出來,前面只是加載類名。往上翻一點點最近的那張有關 spring.factories 內容的圖中也可以看到 AutoConfigurationImportFilter 配置的信息,直接貼代碼如下:
1 # Auto Configuration Import Filters 2 org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\ 3 org.springframework.boot.autoconfigure.condition.OnBeanCondition,\ 4 org.springframework.boot.autoconfigure.condition.OnClassCondition,\ 5 org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
spring-boot-autoconfig 定義了三種類型的 Conditional ImportFilter,在 filter 方法中依此使用它們對候選配置進行過濾(candidate 是候選人的意思),如上貼出的 filter 方法 6~16 行的意思是經過三種 import filter 過濾后對應 boolean match[] 數組位置上為 true 的配置會被真正啟用。在 java 中 boolean 數組初始化時所有元素都是 false,因此經過三個 import filter 的 match 方法相當於把三個結果進行了或操作,只要有一個中就行。
另一點要注意到的是,因為三個 OnXXXCondition 都在同一個 boolean match[] 數組上操作,所以同一個位置上的判斷結果肯定是出自於對同一個自動裝配類的判斷,而本文中 RabbitAutoConfiguration 排名比較靠前,排第 3 位(數組下標為 2)。
接着進到上面 filter 方法的第 6 行主要看 filter#match 邏輯:
1 @Override 2 public boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) { 3 ConditionEvaluationReport report = ConditionEvaluationReport.find(this.beanFactory); 4 ConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses, autoConfigurationMetadata); 5 boolean[] match = new boolean[outcomes.length]; 6 for (int i = 0; i < outcomes.length; i++) { 7 match[i] = (outcomes[i] == null || outcomes[i].isMatch()); 8 if (!match[i] && outcomes[i] != null) { 9 logOutcome(autoConfigurationClasses[i], outcomes[i]); 10 if (report != null) { 11 report.recordConditionEvaluation(autoConfigurationClasses[i], this, outcomes[i]); 12 } 13 } 14 } 15 return match; 16 }
ConditionOutcome[] 中已經是處理過的結果了,我們要進到更底層的方法去看 condition 是怎么被處理的。RabbitAutoConfiguration 類上的 @ConditionOnClass({ RabbitTemplate.class, Channel.class}) 里面有兩個類,我們要看一下 OnClassCondition 類是怎么處理這種多個 class 條件的。關鍵代碼如下:
1 private ConditionOutcome getOutcome(String candidates) { 2 try { 3 if (!candidates.contains(",")) { 4 return getOutcome(candidates, this.beanClassLoader); 5 } 6 for (String candidate : StringUtils.commaDelimitedListToStringArray(candidates)) { 7 ConditionOutcome outcome = getOutcome(candidate, this.beanClassLoader); 8 if (outcome != null) { 9 return outcome; 10 } 11 } 12 } 13 catch (Exception ex) { 14 // We'll get another chance later 15 } 16 return null; 17 }
多個 class 其實是逐個判斷,getOutcome 遞進代碼如下:
1 private ConditionOutcome getOutcome(String className, ClassLoader classLoader) { 2 if (ClassNameFilter.MISSING.matches(className, classLoader)) { 3 return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class) 4 .didNotFind("required class").items(Style.QUOTE, className)); 5 } 6 return null; 7 }
ClassNameFilter.MISSING.matches(className, classLoader) 里面的邏輯簡單,就是使用了 Class.forName(className); API 進行全類名文件查找。
如果是匹配的話,返回 null。對應了前面貼出來的 filter#match 方法的第 7 行的短路或條件,如果 outcomes[i] == null 則 match[i] = true。
沒有匹配會返回一些信息封裝到 ConditionOutCome 的 ConditionMessage 里面。如果@ConditionOnClass 里面有多個 class,只要有任意一個 class 不存在,就不會匹配成功。不過就 RabbitAutoConfiguration 配置來說,只要 maven dependency 引入了 spring-boot-starter-amqp,那么 com.rabbitmq.client.Channel 和 org.springframework.amqp.rabbit.core.RabbitTemplate 會一起引入。
2.2.2 @Import(AutoConfigurationImportSelector.class) 分析結果:
最后,經過 2.0.9.RELEASE 版本 和 2.1.0.RELEASE 版本的對比,我們知道:
(a) 在 2.0.9.RELEASE 及之前的版本,可以在 AutoConfigurationImportSelector 類的 String[] selectImports() 方法上打斷點進去
(b) 2.1.0.RELEASE 版本及之后的版本,調試斷點變為 AutoConfigurationImportSelector#getAutoConfigurationEntry() 方法
這些方法的最終目的都是從 spring-boot-autoconfig.jar 包的 META-INF 目錄內加載 spring.factories 文件中配置,其中就包含有自動配置類的配置 org.springframework.boot.autoconfigure.EnableAutoConfiguration= xxx,yyy,zzz,....... 這些自動配置類在一定條件下(@Conditional注解派上用場)被啟用,並且在配置bean時會使用到我們在項目classpath下的配置文件(如 yml)中的屬性。
如此一來,只要我們在 pom 引入了相應的 jar 達成 @Conditional 條件,然后通常需要再配置一些 connection 屬性(不管是連 redis,mysql,rabbitmq 都有 connection 這個概念)來供 spring autoconfig 的自動配置類在創建 connection 對象等相關對象時使用,那么 spring autoconfig 就能將這些 bean 創建后加入到 spring IOC 容器中,我們在代碼里就可以通過 spring IOC 獲取這些 bean 了。
RabbitAutoConfiguration 自動裝配類是如何運作的:

1 @Configuration 2 @ConditionalOnClass({ RabbitTemplate.class, Channel.class }) 3 @EnableConfigurationProperties(RabbitProperties.class) 4 @Import(RabbitAnnotationDrivenConfiguration.class) 5 public class RabbitAutoConfiguration { 6 7 @Configuration 8 @ConditionalOnMissingBean(ConnectionFactory.class) 9 protected static class RabbitConnectionFactoryCreator { 10 11 @Bean 12 public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties properties, 13 ObjectProvider<ConnectionNameStrategy> connectionNameStrategy) throws Exception { 14 PropertyMapper map = PropertyMapper.get(); 15 CachingConnectionFactory factory = new CachingConnectionFactory( 16 getRabbitConnectionFactoryBean(properties).getObject()); 17 map.from(properties::determineAddresses).to(factory::setAddresses); 18 map.from(properties::isPublisherConfirms).to(factory::setPublisherConfirms); 19 map.from(properties::isPublisherReturns).to(factory::setPublisherReturns); 20 RabbitProperties.Cache.Channel channel = properties.getCache().getChannel(); 21 map.from(channel::getSize).whenNonNull().to(factory::setChannelCacheSize); 22 map.from(channel::getCheckoutTimeout).whenNonNull().as(Duration::toMillis) 23 .to(factory::setChannelCheckoutTimeout); 24 RabbitProperties.Cache.Connection connection = properties.getCache().getConnection(); 25 map.from(connection::getMode).whenNonNull().to(factory::setCacheMode); 26 map.from(connection::getSize).whenNonNull().to(factory::setConnectionCacheSize); 27 map.from(connectionNameStrategy::getIfUnique).whenNonNull().to(factory::setConnectionNameStrategy); 28 return factory; 29 } 30 31 private RabbitConnectionFactoryBean getRabbitConnectionFactoryBean(RabbitProperties properties) 32 throws Exception { 33 PropertyMapper map = PropertyMapper.get(); 34 RabbitConnectionFactoryBean factory = new RabbitConnectionFactoryBean(); 35 map.from(properties::determineHost).whenNonNull().to(factory::setHost); 36 map.from(properties::determinePort).to(factory::setPort); 37 map.from(properties::determineUsername).whenNonNull().to(factory::setUsername); 38 map.from(properties::determinePassword).whenNonNull().to(factory::setPassword); 39 map.from(properties::determineVirtualHost).whenNonNull().to(factory::setVirtualHost); 40 map.from(properties::getRequestedHeartbeat).whenNonNull().asInt(Duration::getSeconds) 41 .to(factory::setRequestedHeartbeat); 42 RabbitProperties.Ssl ssl = properties.getSsl(); 43 if (ssl.isEnabled()) { 44 factory.setUseSSL(true); 45 map.from(ssl::getAlgorithm).whenNonNull().to(factory::setSslAlgorithm); 46 map.from(ssl::getKeyStoreType).to(factory::setKeyStoreType); 47 map.from(ssl::getKeyStore).to(factory::setKeyStore); 48 map.from(ssl::getKeyStorePassword).to(factory::setKeyStorePassphrase); 49 map.from(ssl::getTrustStoreType).to(factory::setTrustStoreType); 50 map.from(ssl::getTrustStore).to(factory::setTrustStore); 51 map.from(ssl::getTrustStorePassword).to(factory::setTrustStorePassphrase); 52 map.from(ssl::isValidateServerCertificate) 53 .to((validate) -> factory.setSkipServerCertificateValidation(!validate)); 54 map.from(ssl::getVerifyHostname).to(factory::setEnableHostnameVerification); 55 } 56 map.from(properties::getConnectionTimeout).whenNonNull().asInt(Duration::toMillis) 57 .to(factory::setConnectionTimeout); 58 factory.afterPropertiesSet(); 59 return factory; 60 } 61 62 } 63 64 @Configuration 65 @Import(RabbitConnectionFactoryCreator.class) 66 protected static class RabbitTemplateConfiguration { 67 68 private final RabbitProperties properties; 69 70 private final ObjectProvider<MessageConverter> messageConverter; 71 72 private final ObjectProvider<RabbitRetryTemplateCustomizer> retryTemplateCustomizers; 73 74 public RabbitTemplateConfiguration(RabbitProperties properties, 75 ObjectProvider<MessageConverter> messageConverter, 76 ObjectProvider<RabbitRetryTemplateCustomizer> retryTemplateCustomizers) { 77 this.properties = properties; 78 this.messageConverter = messageConverter; 79 this.retryTemplateCustomizers = retryTemplateCustomizers; 80 } 81 82 @Bean 83 @ConditionalOnSingleCandidate(ConnectionFactory.class) 84 @ConditionalOnMissingBean 85 public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { 86 PropertyMapper map = PropertyMapper.get(); 87 RabbitTemplate template = new RabbitTemplate(connectionFactory); 88 MessageConverter messageConverter = this.messageConverter.getIfUnique(); 89 if (messageConverter != null) { 90 template.setMessageConverter(messageConverter); 91 } 92 template.setMandatory(determineMandatoryFlag()); 93 RabbitProperties.Template properties = this.properties.getTemplate(); 94 if (properties.getRetry().isEnabled()) { 95 template.setRetryTemplate(new RetryTemplateFactory( 96 this.retryTemplateCustomizers.orderedStream().collect(Collectors.toList())).createRetryTemplate( 97 properties.getRetry(), RabbitRetryTemplateCustomizer.Target.SENDER)); 98 } 99 map.from(properties::getReceiveTimeout).whenNonNull().as(Duration::toMillis) 100 .to(template::setReceiveTimeout); 101 map.from(properties::getReplyTimeout).whenNonNull().as(Duration::toMillis).to(template::setReplyTimeout); 102 map.from(properties::getExchange).to(template::setExchange); 103 map.from(properties::getRoutingKey).to(template::setRoutingKey); 104 map.from(properties::getDefaultReceiveQueue).whenNonNull().to(template::setDefaultReceiveQueue); 105 return template; 106 } 107 108 private boolean determineMandatoryFlag() { 109 Boolean mandatory = this.properties.getTemplate().getMandatory(); 110 return (mandatory != null) ? mandatory : this.properties.isPublisherReturns(); 111 } 112 113 @Bean 114 @ConditionalOnSingleCandidate(ConnectionFactory.class) 115 @ConditionalOnProperty(prefix = "spring.rabbitmq", name = "dynamic", matchIfMissing = true) 116 @ConditionalOnMissingBean 117 public AmqpAdmin amqpAdmin(ConnectionFactory connectionFactory) { 118 return new RabbitAdmin(connectionFactory); 119 } 120 121 } 122 123 @Configuration 124 @ConditionalOnClass(RabbitMessagingTemplate.class) 125 @ConditionalOnMissingBean(RabbitMessagingTemplate.class) 126 @Import(RabbitTemplateConfiguration.class) 127 protected static class MessagingTemplateConfiguration { 128 129 @Bean 130 @ConditionalOnSingleCandidate(RabbitTemplate.class) 131 public RabbitMessagingTemplate rabbitMessagingTemplate(RabbitTemplate rabbitTemplate) { 132 return new RabbitMessagingTemplate(rabbitTemplate); 133 } 134 135 } 136 137 }
(代碼還是比較多的,折疊了)
RabbitAutoConfiguration 類上面有幾個注解,@ConditionOnClass 上面已經分析過了。@EnableConfigurationProperties(RabbitProperties.class) 這個注解的意思就是說我們在 yml 等文件里面配置的一些 KEY-VAL 對會被 RabbitProperties 這個類的屬性利用(注入了),比如在創建 RabbitMQ Connection 時會從 RabbitProperties 類里面獲取屬性而不再是從文件中一個個讀取或者是再用一些基礎的 Properties 類來處理了。
RabbitAutoConfiguration 類里面沒有任何多余的方法,只有三個靜態內部類。它們之間的引用關系如下:
1 @Configuration 2 @ConditionalOnMissingBean(ConnectionFactory.class) 3 protected static class RabbitConnectionFactoryCreator {...} 4 5 @Configuration 6 @Import(RabbitConnectionFactoryCreator.class) 7 protected static class RabbitTemplateConfiguration {...} 8 9 @Configuration 10 @ConditionalOnClass(RabbitMessagingTemplate.class) 11 @ConditionalOnMissingBean(RabbitMessagingTemplate.class) 12 @Import(RabbitTemplateConfiguration.class) 13 protected static class MessagingTemplateConfiguration {...}
后面的類會 @Import 前面的類作為依賴配置。每個內部類里面都有一些被 @Bean 注解 和 派生的 @Conditional 注解標記的方法,在合適的條件下這些方法會產生對應的 bean。
@EnableAutoConfiguration 總結:
說到底自動裝配其實是 spring boot autoconfig 工具包替我們提前寫好了一些 bean 裝配的動作,讓我們在編碼時只需要寫一些配置文件就能為運行時傳入 KEY-VAL 對從而構建相應的 bean 來完成特定的功能。
實戰自定義@Import 注解和注解處理器
首先要再次重申的是,spring framework 中任何注解都只是一個標記的作用,想要讓被注解標記的類最終被 IOC 識別就需要讓該類能被 spring IOC 執行包掃描的時候能掃描到,假設你在某個類上標記了 @Import 注解,但是該類沒有被 spring IOC 掃描路徑掃描到,那么這么做就沒有任何意義;或者說即使在包掃描時該類被“掃描過”,但是由於沒有任何標記(包括 @Component、@Configuration 等),它也不會被 IOC 解析為 bean definition。
NOTE:在 spring boot 中放置 main application class 是有講究的,官方文檔提到:將 @SpringBootApplicaiton 標記的項目 main applicaiton class 放在項目的根目錄下。比如你的項目根目錄結構是 com.DEMO.A,com.DEMO.B,com.DEMO.C 等等,那么你的 main application class 全類名就應該是 com.DEMO.YourMainApplicationName。因為標記 @SpringBootApplication 默認會掃描本包和本包的子包下所有的標記類。沒錯,spring boot 又幫你默認掃描了一些路徑。
spring boot 官網文檔 main class locating 相關說明:https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-locating-the-main-class
那么如果你用了非主流的目錄結構,有些類就是沒有和 main application class 在同一個根目錄下,或者即使在同一個根目錄下但是你就是不想用 @Component 這些注解來標記它們,這時候 @Import 注解就可以派上用場了:@Import 可以為你的 spring 項目引入那些沒有被包掃描過程識別出來的 bean definition。
spring IOC 的回調:
我們的類如果要起到某些特定的作用,只能實現 spring IOC 容器中為我們預定義好的接口類型。比如大家在學習 spring IOC 的時候都會接觸過的 BeanPostProcessor 接口就是其中一種 spring IOC 為開發者提供的回調接口。開發者寫一個 BeanPostProcessor 實現類並交由 spring IOC 管理后它就可以在 IOC 所有的 bean 實例化完成后進行一個后置處理過程,這些過程一般處於類實例化到服務器正式使用這個實例對外服務之間,比如預熱數據之類的操作。
關於開源框架中的回調這件事——面向回調編程:
(瞎嗶嗶時間,大佬勿噴)
前不久剛寫的 netty 筆記中,分享了一些學習 netty 時寫的代碼,從其中也不難發現“回調”的身影。比如在 NettyServer 中我們為服務端定制 childHandler 邏輯時為該方法傳入的一個匿名 ChannelInitializer 實現類且覆蓋了其 initChannel 方法。在 initChannel 方法中,我們編排我們的 channel handler 來處理業務邏輯。對 NettyServer 來說,每當有新連接到來時,都會調用一次這個匿名 ChannelInitializer 實現類的 initChannel 方法來為 channel pipeline 中添加我們定制的 channel handler 了。這正是在回調我們的代碼。
其實往深了想,編程了這幾年,用了不少框架,真相竟然是一直在面對回調編程?——稍微准確一點來說只要是包裹在某種框架流程里面的情況就離不開“面向回調編程”,所以我們是在用面向對象語言進行面向回調編程啊~~~
(繼續正題......)
本次實戰 @Import 和 ImportSelector 接口我們需要實現的功能是:指定一個或多個包名,能夠遞歸地識別指定包名下的所有類文件。
1.首先定義我們的 import 類型注解:
1 @Target(ElementType.TYPE) 2 @Retention(RetentionPolicy.RUNTIME) 3 @Documented 4 @Import(PackagesImportSelector.class) 5 public @interface PackagesImporter { 6 String[] packages(); 7 }
2.然后定義注解處理器:
1 public class PackagesImportSelector implements DeferredImportSelector { 2 3 private List<String> clzList = new ArrayList<>(100); 4 5 @Override 6 public String[] selectImports(AnnotationMetadata importingClassMetadata) { 7 Map<String, Object> attributes = importingClassMetadata.getAnnotationAttributes(PackagesImporter.class.getName(), true); 8 if (attributes == null) { 9 return new String[0]; 10 } 11 String[] packages = (String[]) attributes.get("packages"); 12 if (packages == null) { 13 return new String[0]; 14 } 15 scanPackages(packages); 16 return clzList.isEmpty() ? new String[0] : clzList.toArray(new String[0]); 17 } 18 19 private void scanPackages(String[] packages) { 20 for (String path : packages) { 21 doScanPackages(path); 22 } 23 } 24 25 private LinkedList<File> directories = new LinkedList<>(); 26 private LinkedList<String> pathList = new LinkedList<>(); 27 /** 28 * 遞歸處理子文件夾 29 */ 30 private void doScanPackages(String path) { 31 URL resource = this.getClass().getClassLoader().getResource(path.replaceAll("\\.", "/")); 32 if (resource == null) { 33 return; 34 } 35 File file = new File(resource.getFile()); 36 File[] files = file.listFiles(); 37 if (files == null || files.length == 0) { 38 return; 39 } 40 for (File f : files) { 41 if (f.isDirectory()) { 42 pathList.addLast(path); 43 directories.addLast(f); 44 } else { 45 // 先處理當前目錄下的文件 46 String fileName = f.getName(); 47 if (fileName.endsWith(".class")) { 48 String fullClassName = path + "." + fileName.substring(0, fileName.indexOf(".")); 49 clzList.add(fullClassName); 50 } 51 } 52 } 53 // 保證先處理平級的包。比如我的demo中,會先加載 ClassA、ClassB、ClassC,然后加載 ClassAA 54 while (!directories.isEmpty()) { 55 doScanPackages(pathList.removeFirst() + "." + directories.removeFirst().getName()); 56 } 57 58 } 59 }
PackagesImportSelector 實現了將指定目錄下所有的 .class 文件全部掃描到 IOC 中管理。在 doScanPackages 中實現了先掃描平級的 class 文件再掃描子目錄 class 文件這個功能........(其實也沒必要這樣做,能掃描就行......)
3.最后在某個 @Configuration 標記的類上使用我們的自定義 import 注解:
1 @Configuration 2 @PackagesImporter(packages = {"code.dev.arch", "code.christ.model"}) 3 public class MyConfig { // 被 @Configuration 注解也會生成一個 bean, 默認 bean name 和 @Component 規則一樣——駝峰 4 5 public MyConfig() { 6 System.out.println("MyConfig 被 @Configuration 初始化了"); 7 } 8 9 }
當然,把 @PackagesImporter 注解放到 main application class 類上也是可以的,因為 @SpringBootApplication 注解帶有 @SpringBootConfiguration 注解,它只是對 @Configuration 做了二次包裝。因此這也能解釋為什么 spring 官方有些 demo 直接在 main application class 里面使用 @Bean 注解,就是因為啟動類已經帶了 @Configuration 注解所以該類下面的 @Bean 注解才能被識別。
demo中寫了一個 NoConfigButNoteBean 類來驗證這個用法。實際上在我使用的 intellij idea 上面,沒有使用 @Configuration 或其他能托管到 spring IOC 的注解時,在類名上有灰色的標記,如下:
其他有標記托管的類會上成白色字體。這個是 IDE 特性,可以做一點小參考。
那么在不使用托管注解的情況下,hello world 怎么加入 spring IOC 呢?可以修改一下我們的自定義 import 注解,再添加一個包名,把 NoConfigButNoteBean 的包名放上去,這樣就可以讓 spring IOC 在處理 bean definition 時順帶處理類里面帶 @Bean 的注解,當然這是 spring IOC bean definition 處理器的功能了,我們本次實現的只是掃描包下所有的類文件並上傳所有的類文件全類名給 spring IOC。
有沒有感覺 spring 就像一個老司機?每個公司或許都應該存在這樣一個老司機,或者說每個 java 開發者第一個遇到的老司機就是 spring framework 吧。在某些方面,它強大可靠,可以解決你的燃眉之急。同時它也是一個寶庫,有許多可以學習借鑒的地方。
結尾:
由於 spring boot 自動裝配這一塊面試挺多會問到,所以沒辦法還是抽空寫了一篇筆記,這樣在面試的時候就可以簡單帶過然后把博客鏈接扔給面試官了(嗯~)。后續應該還有一篇筆記來講 @Conditional 實戰拓展的吧,也是另一個喜歡面試的點。應該會盡快抽空寫出來。See ya~