註解自己並無什麼實際的功能(非要說標記也是一個「實際」的功能的話,也能夠算吧),隱藏在背後的註解處理器纔是實現註解機制的核心。本篇將從這兩個層面出發探索 spring boot 自動裝配的祕密,並使用 spring boot 的自動裝配機制來實現自動裝配。html
本次代碼已經放到 github:https://github.com/christmad/code-share/tree/master/spring-boot-config-practicejava
代碼中主要是作了 @Configuration 和 @ConfigurationProperties 使用的練習,以及本篇博客筆記重點:自定義 ImportSelector 實現類實現批量掃描包下類文件。mysql
(1) 首先,從 spring 3.X 開始,AnnotationConfigApplicationContext 替代 ClassPathXmlApplicationContext,迎來了全新的 java bean config 配置方式,使用 java bean 和 註解就能輕鬆添加配置。git
(2) 3.X 開始提供的致力於零配置文件的註解:github
@Configuration——用來替代 xml 文件的。面試
@Bean——標記在方法上,替代 xml 配置中的 <bean></bean> 定義,方法名稱就是 bean id。redis
@Import——將 Bean 導入到容器的 BeanDefinition Map 中,能夠接收 Class[] 數組,一般只用它來導入 1~2 個類,不適合批量導入場景。spring
可是 @Import 適合用來作「啓動」裝配的動做,配置不會無中生有,不可能全部的配置步驟都是自動的,必須有個起點的地方是手動的「硬編碼」,就像咱們剛接觸 window 操做系統時瞭解到有不少系統缺省值同樣它們是寫死的硬編碼。而 @Import 就能起到這個做用。其實不須要這個註解 spring boot 也能實現自動裝配,只不過做爲一個開源框架,使用 @Import 更能突出須要導入的意圖和需求,讓框架變得更好理解。sql
另一個 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 融合了 @SpringBootConfiguration(即 @Configuration)、@EnableAutoConfiguration(依賴 @Import,間接依賴 @Conditional)、@ComponentScan 等幾個註解。
(1)@SpringBootConfiguration 註解就沒什麼好說的了,直接是在 @Configuration 註解上派生的註解,多了一層包裝而已
(2)@EnableAutoConfiguration 註解是個組合註解,裏面對咱們有用的註解有兩個
2.1 @AutoConfigurationPackage
2.2 @Import(AutoConfigurationImportSelector.class)
@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 還作了不少工做,不是本次討論重點。
目前看起來,@AutoConfigurationPackage 註解的做用是把 AutoConfigurationPackages 註冊到 IOC BeanDefinition 中。
這個過程從 debug 來看屬於 AbstractApplicationContext#refresh() 中的 invokeBeanFactoryPostProcessors(beanFactory); 流程。
在這個流程中能夠對 BeanFactory 中的 BeanDefinition 進行修改,至關於修改房屋構造圖。以後的流程會用 BeanDefinition 去建立一個個實例,而後會用到 BeanPostProcessor——屬於在 java 實例的基礎上修改的層面了,屋子原本不通風的如今想換通風的也換不了了,可是裏面的傢俱或者裝修風格還能夠更換,嗯,換完以後可能會住的舒服點。
前面說到「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 來講明這些自動裝配類究竟是怎麼用的。
前面說完 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.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 了。
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。
說到底自動裝配實際上是 spring boot autoconfig 工具包替咱們提早寫好了一些 bean 裝配的動做,讓咱們在編碼時只須要寫一些配置文件就能爲運行時傳入 KEY-VAL 對從而構建相應的 bean 來完成特定的功能。
首先要再次重申的是,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 的時候都會接觸過的 BeanPostProcessor 接口就是其中一種 spring IOC 爲開發者提供的回調接口。開發者寫一個 BeanPostProcessor 實現類並交由 spring IOC 管理後它就能夠在 IOC 全部的 bean 實例化完成後進行一個後置處理過程,這些過程通常處於類實例化到服務器正式使用這個實例對外服務之間,好比預熱數據之類的操做。
(瞎嗶嗶時間,大佬勿噴)
前不久剛寫的 netty 筆記中,分享了一些學習 netty 時寫的代碼,從其中也不難發現「回調」的身影。好比在 NettyServer 中咱們爲服務端定製 childHandler 邏輯時爲該方法傳入的一個匿名 ChannelInitializer 實現類且覆蓋了其 initChannel 方法。在 initChannel 方法中,咱們編排咱們的 channel handler 來處理業務邏輯。對 NettyServer 來講,每當有新鏈接到來時,都會調用一次這個匿名 ChannelInitializer 實現類的 initChannel 方法來爲 channel pipeline 中添加咱們定製的 channel handler 了。這正是在回調咱們的代碼。
其實往深了想,編程了這幾年,用了很多框架,真相居然是一直在面對回調編程?——稍微準確一點來講只要是包裹在某種框架流程裏面的狀況就離不開「面向回調編程」,因此咱們是在用面嚮對象語言進行面向回調編程啊~~~
(繼續正題......)
1 @Target(ElementType.TYPE) 2 @Retention(RetentionPolicy.RUNTIME) 3 @Documented 4 @Import(PackagesImportSelector.class) 5 public @interface PackagesImporter { 6 String[] packages(); 7 }
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 文件這個功能........(其實也不必這樣作,能掃描就行......)
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~