知識改變命運,擼碼使我快樂,2020繼續遊走在開源界<br/>
點贊再看,養成習慣<br/>
給我來個Star吧, 點擊瞭解下基於SpringBoot的組件化接口服務落地解決方案
咱們一直在使用SpringBoot
來開發應用程序,可是爲何在項目啓動時就會自動註冊使用註解@Component
、@Service
、@RestController
...標註的Bean
呢?html
SpringBoot
把入口類所在的Package
做爲了默認的掃描目錄,這也是一個約束,若是咱們把須要被註冊到IOC
的類建立在掃描目錄下就能夠實現自動註冊,不然則不會被註冊。java
若是你入口類叫作ExampleApplication
,它位於org.minbox.chapter
目錄下,當咱們啓動應用程序時就會自動掃描org.minbox.chapter
同級目錄、子級目錄下所有註解的類,以下所示:git
. src/main/java ├── org.minbox.chapter │ ├── ExampleApplication.java │ ├── HelloController.java │ ├── HelloExample.java │ └── index │ │ └── IndexController.java ├── com.hengboy │ ├── TestController.java └──
HelloController.java
、HelloExample.java
與入口類ExampleApplication.java
在同一級目錄下,因此在項目啓動時能夠被掃描到。spring
IndexController.java
則是位於入口類的下級目錄org.minbox.chapter.index
內,由於支持下級目錄掃描,因此它也能夠被掃描到。segmentfault
TestController.java
位於com.hengboy
目錄下,默認沒法掃描到。api
在上面目錄結構中位於com.hengboy
目錄下的TestController.java
類,默認狀況下是沒法被掃描並註冊到IOC
容器內的,若是想要掃描該目錄下的類,下面有兩種方法。架構
方法一:使用@ComponentScan註解app
@ComponentScan({"org.minbox.chapter", "com.hengboy"})
方法二:使用scanBasePackages屬性框架
@SpringBootApplication(scanBasePackages = {"org.minbox.chapter", "com.hengboy"})
注意事項:配置自定義掃描目錄後, 會覆蓋掉默認的掃描目錄,若是你還須要掃描默認目錄,那麼你要進行配置掃描目錄,在上面自定義配置中,若是僅配置掃描com.hengboy
目錄,則org.minbox.chapter
目錄就不會被掃描。
下面咱們來看下SpringBoot
源碼是怎麼實現自動化掃描目錄下的Bean
,並將Bean
註冊到容器內的過程。spring-boot
因爲註冊的流程比較複雜,挑選出具備表明性的流程步驟來進行講解。
在org.springframework.context.annotation.ComponentScanAnnotationParser#parse
方法內有着獲取basePackages
的業務邏輯,源碼以下所示:
Set<String> basePackages = new LinkedHashSet<>(); // 獲取@ComponentScan註解配置的basePackages屬性值 String[] basePackagesArray = componentScan.getStringArray("basePackages"); // 將basePackages屬性值加入Set集合內 for (String pkg : basePackagesArray) { String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg), ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); Collections.addAll(basePackages, tokenized); } // 獲取@ComponentScan註解的basePackageClasses屬性值 for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) { // 獲取basePackageClasses所在的package並加入Set集合內 basePackages.add(ClassUtils.getPackageName(clazz)); } // 若是並無配置@ComponentScan的basePackages、basePackageClasses屬性值 if (basePackages.isEmpty()) { // 使用Application入口類的package做爲basePackage basePackages.add(ClassUtils.getPackageName(declaringClass)); }
獲取basePackages
分爲了那麼三個步驟,分別是:
@ComponentScan
註解basePackages
屬性值@ComponentScan
註解basePackageClasses
屬性值Application
入口類所在的package
做爲默認的basePackages
注意事項:根據源碼也就證明了,爲何咱們配置了basePackages
、basePackageClasses
後會把默認值覆蓋掉,這裏其實也不算是覆蓋,是根本不會去獲取Application
入口類的package
。
獲取到所有的Packages
後,經過org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan
方法來掃描每個Package
下使用註冊註解(@Component
、@Service
、@RestController
...)標註的類,源碼以下所示:
protected Set<BeanDefinitionHolder> doScan(String... basePackages) { // 當basePackages爲空時拋出IllegalArgumentException異常 Assert.notEmpty(basePackages, "At least one base package must be specified"); Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>(); // 遍歷每個basePackage,掃描package下的所有Bean for (String basePackage : basePackages) { // 獲取掃描到的所有Bean Set<BeanDefinition> candidates = findCandidateComponents(basePackage); // 遍歷每個Bean進行處理註冊相關事宜 for (BeanDefinition candidate : candidates) { // 獲取做用域的元數據 ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); candidate.setScope(scopeMetadata.getScopeName()); // 獲取Bean的Name String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry); if (candidate instanceof AbstractBeanDefinition) { postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); } // 若是是註解方式註冊的Bean if (candidate instanceof AnnotatedBeanDefinition) { // 處理Bean上的註解屬性,相應的設置到BeanDefinition(AnnotatedBeanDefinition)類內字段 AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); } // 檢查是否知足註冊的條件 if (checkCandidate(beanName, candidate)) { // 聲明Bean具有的基本屬性 BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); // 應用做用域代理模式 definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); // 寫入返回的集合 beanDefinitions.add(definitionHolder); // 註冊Bean registerBeanDefinition(definitionHolder, this.registry); } } } return beanDefinitions; }
在上面源碼中會掃描每個basePackage
下經過註解定義的Bean
,獲取Bean
註冊定義對象後並設置一些基本屬性。
掃描到basePackage
下的Bean
後會直接經過org.springframework.beans.factory.support.BeanDefinitionReaderUtils#registerBeanDefinition
方法進行註冊,源碼以下所示:
public static void registerBeanDefinition( BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException { // 註冊Bean的惟一名稱 String beanName = definitionHolder.getBeanName(); // 經過BeanDefinitionRegistry註冊器進行註冊Bean registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()); // 若是存在別名,進行註冊Bean的別名 String[] aliases = definitionHolder.getAliases(); if (aliases != null) { for (String alias : aliases) { registry.registerAlias(beanName, alias); } } }
經過org.springframework.beans.factory.support.BeanDefinitionRegistry#registerBeanDefinition
註冊器內的方法能夠直接將Bean
註冊到IOC
容器內,而BeanName
則是它生命週期內的惟一名稱。
經過本文的講解我想你應該已經瞭解了SpringBoot
應用程序啓動時爲何會自動掃描package
並將Bean
註冊到IOC
容器內,雖然項目啓動時間很短暫,不過這是一個很是複雜的過程,在學習過程當中你們能夠使用Debug
模式來查看每個步驟的邏輯處理。
做者我的 博客
使用開源框架 ApiBoot 助你成爲Api接口服務架構師