前言
Spring中的@Configuration
註解修飾的類被稱為配置類,透過配置類可以向容器註冊bean以及匯入其它配置類,本篇文章將結合例子和原始碼對@Configuration
註解原理進行學習,並引出對Spring框架在處理配置類過程中起重要作用的ConfigurationClassPostProcessor
的討論。
Springboot版本:2.4.1
正文
一. @Configuration註解簡析
基於@Configuration
註解可以實現基於JavaConfig的方式來宣告Spring中的bean,與之作為對比的是基於XML的方式來宣告bean。由@Configuration
註解標註的類中所有由@Bean
註解修飾的方法返回的物件均會被註冊為Spring容器中的bean,使用舉例如下。
@Configuration
public class TestBeanConfig {
@Bean
public TestBean testBean() {
return new TestBean();
}
}
如上所示,Spring容器會將TestBean
註冊為Spring容器中的bean。由@Configuration
註解修飾的類稱為Spring中的配置類,Spring中的配置類在Spring啟動階段會被先載入並解析為ConfigurationClass
,然後會基於每個配置類對應的ConfigurationClass
物件為容器註冊BeanDefinition
,以及基於每個配置類中由@Bean
註解修飾的方法為容器註冊BeanDefinition
,後續Spring也會基於這些BeanDefinition
向容器註冊bean。關於BeanDefinition
的概念,可以參見Spring-BeanDefinition簡析。
在詳細分析由@Configuration
註解修飾的配置類是如何被解析為ConfigurationClass
以及最終如何被註冊為BeanDefinition
前,得先探究一下Springboot的啟動類,因為後續的分析會以Springboot的啟動為基礎,所以有必要先了解一下Springboot中的啟動類。
Springboot的啟動類由@SpringBootApplication
註解修飾,@SpringBootApplication
註解的功能主要由@SpringBootConfiguration
,@EnableAutoConfiguration
和@ComponentScan
實現,後兩者與Springboot中的自動裝配有關,關於Springboot實現自動裝配,會在後續文章中學習,在這裡主要關心@SpringBootConfiguration
註解。實際上,@SpringBootConfiguration
註解其實就是@Configuration
註解,@SpringBootConfiguration
註解的簽名如下所示。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
......
}
既然@SpringBootConfiguration
註解等同於@Configuration
註解,那麼相應的Springboot的啟動類就是一個配置類,Springboot的啟動類對應的BeanDefinition
會在準備Springboot容器階段就註冊到容器中,將斷點打到SpringApplication#run()
方法中呼叫refreshContext()
方法這一行程式碼,而已知refreshContext()
這一行程式碼用於初始化容器,執行到refreshContext()
方法時容器已經完成了準備,此時看一下容器的資料,如下所示。
此時Springboot容器持有的DefaultListableBeanFactory
中的beanDefinitionMap中已經存在了Springboot啟動類對應的BeanDefinition
,在初始化Springboot容器階段,Springboot啟動類對應的BeanDefinition
會首先被處理,透過處理Springboot啟動類對應的BeanDefinition
才會引入對其它配置類的處理。關於Springboot啟動類,暫時瞭解到這裡,下面再給出一張處理配置類的呼叫鏈,以供後續閱讀參考。
本篇文章後續將從ConfigurationClassPostProcessor
的postProcessBeanDefinitionRegistry()
方法開始,對由@Configuration
註解修飾的配置類的處理進行說明。
二. ConfigurationClassPostProcessor處理配置類
透過第一節中的呼叫鏈可知,在Springboot啟動時,初始化容器階段會呼叫到ConfigurationClassPostProcessor
來處理配置類,即由@Configuration
註解修飾的類。ConfigurationClassPostProcessor
是由Spring框架提供的bean工廠後置處理器,類圖如下所示。
可知ConfigurationClassPostProcessor
實現了BeanDefinitionRegistryPostProcessor
介面,同時BeanDefinitionRegistryPostProcessor
介面又繼承於BeanFactoryPostProcessor
,所以ConfigurationClassPostProcessor
本質上就是一個bean工廠後置處理器。ConfigurationClassPostProcessor
實現了BeanDefinitionRegistryPostProcessor
介面定義的postProcessBeanDefinitionRegistry()
方法,在ConfigurationClassPostProcessor
中對該方法的註釋如下。
Derive further bean definitions from the configuration classes in the registry.
直譯過來就是:從登錄檔中的配置類派生進一步的bean定義。那麼這裡的登錄檔指的就是容器持有的DefaultListableBeanFactory
,而Springboot框架在容器準備階段就將Springboot的啟動類對應的BeanDefinition
註冊到了DefaultListableBeanFactory
的beanDefinitionMap中,所以登錄檔中的配置類指的就是Springboot的啟動類(前文已知Springboot的啟動類就是一個配置類),而派生進一步的bean定義,就是將Springboot啟動類上@EnableAutoConfiguration
和@ComponentScan
等註解載入的配置類解析為BeanDefinition
並註冊到DefaultListableBeanFactory
的beanDefinitionMap中。暫時不清楚在Springboot啟動流程中,ConfigurationClassPostProcessor
的postProcessBeanDefinitionRegistry()
方法註釋中提到的配置類是否會有除了Springboot啟動類之外的配置類,歡迎留言討論。
即現在知道,ConfigurationClassPostProcessor
的postProcessBeanDefinitionRegistry()
方法主要處理目標就是Springboot的啟動類,透過處理Springboot啟動類引出對其它配置類的處理,下面跟隨原始碼,進行學習。postProcessBeanDefinitionRegistry()
方法如下所示。
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
int registryId = System.identityHashCode(registry);
if (this.registriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
}
if (this.factoriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanFactory already called on this post-processor against " + registry);
}
//記錄已經處理過的登錄檔id
this.registriesPostProcessed.add(registryId);
processConfigBeanDefinitions(registry);
}
postProcessBeanDefinitionRegistry()
方法會記錄已經處理過的登錄檔id,防止同一登錄檔被重複處理。實際的處理邏輯在processConfigBeanDefinitions()
中,由於processConfigBeanDefinitions()
方法比較長,所以這裡先把processConfigBeanDefinitions()
方法的處理流程進行一個梳理,如下所示。
- 先把Springboot啟動類的
BeanDefinition
從登錄檔(這裡指DefaultListableBeanFactory
,後續如果無特殊說明,登錄檔預設指DefaultListableBeanFactory
)的beanDefinitionMap中獲取出來; - 建立
ConfigurationClassParser
,解析Springboot啟動類的BeanDefinition
,即解析@PropertySource
,@ComponentScan
,@Import
,@ImportResource
,@Bean
等註解並生成ConfigurationClass
,最後快取在ConfigurationClassParser
的configurationClasses中; - 建立
ConfigurationClassBeanDefinitionReader
,解析所有ConfigurationClass
,基於ConfigurationClass
建立BeanDefinition
並快取到登錄檔的beanDefinitionMap中。
processConfigBeanDefinitions()
方法原始碼如下。
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();
//從登錄檔中把Springboot啟動類對應的BeanDefinition獲取出來
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}
//如果未獲取到Springboot啟動類對應的BeanDefinition,則直接返回
if (configCandidates.isEmpty()) {
return;
}
configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});
SingletonBeanRegistry sbr = null;
if (registry instanceof SingletonBeanRegistry) {
sbr = (SingletonBeanRegistry) registry;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}
if (this.environment == null) {
this.environment = new StandardEnvironment();
}
//建立ConfigurationClassParser以解析Springboot啟動類及其引出的其它配置類
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
do {
StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse");
//ConfigurationClassParser開始執行解析
parser.parse(candidates);
parser.validate();
//將ConfigurationClassParser解析得到的ConfigurationClass拿出來
Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);
//建立ConfigurationClassBeanDefinitionReader,以基於ConfigurationClass建立BeanDefinition
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
//開始建立BeanDefinition並註冊到登錄檔中
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);
processConfig.tag("classCount", () -> String.valueOf(configClasses.size())).end();
candidates.clear();
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
Set<String> alreadyParsedClasses = new HashSet<>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());
if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}
if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
}
}
在processConfigBeanDefinitions()
方法中,ConfigurationClassPostProcessor
將解析Sprngboot啟動類以得到ConfigurationClass
的任務委託給了ConfigurationClassParser
,將基於ConfigurationClass
建立BeanDefinition
並註冊到登錄檔的任務委託給了ConfigurationClassBeanDefinitionReader
,所以下面會對這兩個步驟進行分析。首先是ConfigurationClassParser
解析Springboot啟動類,其parse()
方法如下所示。
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
//Springboot啟動類對應的BeanDefinition為AnnotatedGenericBeanDefinition
//AnnotatedGenericBeanDefinition實現了AnnotatedBeanDefinition介面
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}
//延遲處理DeferredImportSelector
this.deferredImportSelectorHandler.process();
}
由於Springboot啟動類對應的BeanDefinition
為AnnotatedGenericBeanDefinition
,而AnnotatedGenericBeanDefinition
實現了AnnotatedBeanDefinition
介面,所以繼續看parse(AnnotationMetadata metadata, String beanName)
方法,如下所示。
protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER);
}
繼續看processConfigurationClass()
方法,如下所示。
protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException {
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}
ConfigurationClass existingClass = this.configurationClasses.get(configClass);
if (existingClass != null) {
if (configClass.isImported()) {
if (existingClass.isImported()) {
existingClass.mergeImportedBy(configClass);
}
return;
}
else {
this.configurationClasses.remove(configClass);
this.knownSuperclasses.values().removeIf(configClass::equals);
}
}
SourceClass sourceClass = asSourceClass(configClass, filter);
do {
//實際開始處理配置類
sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
}
while (sourceClass != null);
this.configurationClasses.put(configClass, configClass);
}
在processConfigurationClass()
方法中會呼叫doProcessConfigurationClass()
方法來實際的處理配置類的@ComponentScan
,@Import
,@Bean
等註解。在本節的論述中,其實一直是將Springboot啟動類與其它配置類分開的,因為筆者認為Springboot啟動類是一個特殊的配置類,其它配置類的掃描和載入均依賴Springboot啟動類上的一系列註解(@ComponentScan
,@Import
等)。上述processConfigurationClass()
方法是一個會被遞迴呼叫的方法,第一次該方法被呼叫時,處理的配置類是Springboot的啟動類,處理Springboot啟動類時就會載入進來許多其它的配置類,那麼這些配置類也會呼叫processConfigurationClass()
方法來處理,因為其它配置類上可能也會有一些@Import
,@Bean
等註解。這裡只討論第一次呼叫,即處理Springboot啟動類的情況。doProcessConfigurationClass()
方法原始碼如下所示。
@Nullable
protected final SourceClass doProcessConfigurationClass(
ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
throws IOException {
if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
processMemberClasses(configClass, sourceClass, filter);
}
//處理@PropertySource註解
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}
//處理@ComponentScan註解
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
//處理@Import註解
processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
//處理@ImportResource註解
AnnotationAttributes importResource =
AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
if (importResource != null) {
String[] resources = importResource.getStringArray("locations");
Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
for (String resource : resources) {
String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
configClass.addImportedResource(resolvedResource, readerClass);
}
}
//處理由@Bean註解修飾的方法
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}
processInterfaces(configClass, sourceClass);
if (sourceClass.getMetadata().hasSuperClass()) {
String superclass = sourceClass.getMetadata().getSuperClassName();
if (superclass != null && !superclass.startsWith("java") &&
!this.knownSuperclasses.containsKey(superclass)) {
this.knownSuperclasses.put(superclass, configClass);
return sourceClass.getSuperClass();
}
}
return null;
}
doProcessConfigurationClass()
方法中對於每種註解的處理會在後續文章中介紹,本文暫時不討論。在processConfigurationClass()
方法中處理完Springboot啟動類之後,實際上此時只會將自定義bean(由@Component
,@Controller
,@Service
等註解修飾的類)對應的ConfigurationClass
,自定義配置類(由@Configuration
註解修飾的類)對應的ConfigurationClass
新增到ConfigurationClassParser
的configurationClasses
中,那麼最為關鍵的各種starter中的配置類對應的ConfigurationClass
是在哪裡新增的呢,回到ConfigurationClassParser
的parse()
方法,下面再給出其原始碼,如下所示。
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
//這裡處理完,ConfigurationClassParser的configurationClasses中只會有自定義bean和自定義配置類對應的ConfigurationClass
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}
//這裡處理完,starter中的配置類對應的ConfigurationClass才會新增到ConfigurationClassParser的configurationClasses中
this.deferredImportSelectorHandler.process();
}
因為Springboot掃描starter並處理其配置類是依賴啟動類上的@EnableAutoConfiguration
註解,@EnableAutoConfiguration
註解的功能由@Import(AutoConfigurationImportSelector.class)
實現,其中AutoConfigurationImportSelector
實現了DeferredImportSelector
介面,而DeferredImportSelector
表明需要被延遲處理,所以Springboot需要延遲處理AutoConfigurationImportSelector
,延遲處理的地方就在上述parse()
方法的最後一行程式碼,關於@Import
註解,後續文章中會對其進行分析,這裡暫時不討論。現在定義一個TestBeanConfig
配置類,在其中向容器註冊TestBean
,同時再定義一個由@Component
註解修飾的TestComponent
,程式碼如下所示。
@Configuration
public class TestBeanConfig {
@Bean
public TestBean testBean() {
return new TestBean();
}
}
public class TestBean {
public TestBean() {
System.out.println("Initialize TestBean.");
}
}
@Component
public class TestComponent {
public TestComponent() {
System.out.println("Initialize TestComponent.");
}
}
現在在ConfigurationClassParser
的parse()
方法的this.deferredImportSelectorHandler.process();
這一行程式碼打斷點,程式執行到這裡時,ConfigurationClassParser
的configurationClasses如下所示。
可見此時configurationClasses中沒有starter中的配置類對應的ConfigurationClass
,往下執行一行,此時ConfigurationClassParser
的configurationClasses如下所示。
可見此時starter中的配置類對應的ConfigurationClass
已經被載入,至此ConfigurationClassParser
解析Springboot啟動類分析完畢。
現在分析ConfigurationClassBeanDefinitionReader
解析所有ConfigurationClass
,並基於ConfigurationClass
建立BeanDefinition
並快取到登錄檔的beanDefinitionMap中。首先是ConfigurationClassBeanDefinitionReader
的loadBeanDefinitions()
方法,如下所示。
public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
for (ConfigurationClass configClass : configurationModel) {
loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
}
}
在loadBeanDefinitions()
方法中遍歷每一個ConfigurationClass
並呼叫了loadBeanDefinitionsForConfigurationClass()
方法,繼續看loadBeanDefinitionsForConfigurationClass()
方法,如下所示。
private void loadBeanDefinitionsForConfigurationClass(
ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
if (trackedConditionEvaluator.shouldSkip(configClass)) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
}
this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
return;
}
if (configClass.isImported()) {
//基於ConfigurationClass自身建立BeanDefinition並快取到登錄檔中
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
//基於ConfigurationClass中由@Bean註解修飾的方法建立BeanDefinition並快取到登錄檔中
loadBeanDefinitionsForBeanMethod(beanMethod);
}
loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
上述loadBeanDefinitionsForConfigurationClass()
方法中,除了將自身建立為BeanDefinition
外,還會將所有由@Bean
註解修飾的方法(如果有的話)建立為BeanDefinition
,所有建立的BeanDefinition
最後都會註冊到登錄檔中,即快取到DefaultListableBeanFactory
的beanDefinitionMap中。至此,ConfigurationClassBeanDefinitionReader
解析所有ConfigurationClass
的大致流程也分析完畢。
總結
由@Configuration
註解修飾的配置類結合@Bean
註解可以實現向容器註冊bean的功能,同時也可以藉助@ComponentScan
,@Import
等註解將其它配置類掃描到容器中。Springboot的啟動類就是一個配置類,透過ConfigurationClassPostProcessor
處理Springboot啟動類,可以實現將自定義的bean,自定義的配置類和各種starter中的配置類掃描到容器中,以達到自動裝配的效果。