
我們一直在使用
SpringBoot
來開發應用程式,但是為什麼在項目啟動時就會自動注冊使用注解
@Component
、
@Service
、
@RestController
...标注的
Bean
呢?
預設掃描目錄
SpringBoot
把入口類所在的
Package
作為了預設的掃描目錄,這也是一個限制,如果我們把需要被注冊到
IOC
的類建立在掃描目錄下就可以實作自動注冊,否則則不會被注冊。
如果你入口類叫做
ExampleApplication
,它位于
org.minbox.chapter
目錄下,當我們啟動應用程式時就會自動掃描
org.minbox.chapter
同級目錄、子級目錄下全部注解的類,如下所示:
. 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
在同一級目錄下,是以在項目啟動時可以被掃描到。
IndexController.java
則是位于入口類的下級目錄
org.minbox.chapter.index
内,因為支援下級目錄掃描,是以它也可以被掃描到。
TestController.java
位于
com.hengboy
目錄下,預設無法掃描到。
自定義掃描目錄
在上面目錄結構中位于
com.hengboy
目錄下的
TestController.java
類,預設情況下是無法被掃描并注冊到
IOC
容器内的,如果想要掃描該目錄下的類,下面有兩種方法。
方法一:使用@ComponentScan注解
@ComponentScan({"org.minbox.chapter", "com.hengboy"})
複制
方法二:使用scanBasePackages屬性
@SpringBootApplication(scanBasePackages = {"org.minbox.chapter", "com.hengboy"})
複制
注意事項:配置自定義掃描目錄後,會覆寫掉預設的掃描目錄,如果你還需要掃描預設目錄,那麼你要進行配置掃描目錄,在上面自定義配置中,如果僅配置掃描目錄,則
com.hengboy
目錄就不會被掃描。
org.minbox.chapter
追蹤源碼
下面我們來看下
SpringBoot
源碼是怎麼實作自動化掃描目錄下的
Bean
,并将
Bean
注冊到容器内的過程。
由于注冊的流程比較複雜,挑選出具有代表性的流程步驟來進行講解。
擷取BasePackages
在
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下的Bean
擷取到全部的
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
注冊定義對象後并設定一些基本屬性。
注冊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接口服務架構師