spring boot是一款非常優秀的微服務開發框架,具有開箱即用、簡化配置、內置tomcat等等一系列優點。這一切都離不開spring boot的starter機制,starter機制也可以理解爲spring boot所實現的一種SPI機制。本文主要講解spring boot starter機制的使用方式,以及實現原理。
1. 使用示例
1.1 概述
對於starter的使用,首先我們需要注意的是,它一般有四部分:
-
在
application.properties
中聲明的屬性配置; -
用於存儲
application.properties
中的屬性的property model,這個property model可以用@ConfigurationProperties
進行標註,從而自動讀取application.properties
配置文件中的屬性; -
使用
@Configuration
註解標註的XXXAutoConfiguration
類,然後注入第二部分中記錄屬性的model,從而自動創建某些bean; -
在
META-INF/
文件夾下創建spring.factories
文件,其中添加一條配置如下:org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.xxx.XXXAutoConfiguration
這裏的key是
@EnableAutoConfiguration
註解類的全路徑名,值則是我們需要自動裝配的類。
1.2 裝載流程
上面概述的四個部分,對於spring boot而言,其裝載流程如下:
這裏需要說明的一個點在於,spring boot既然已經使用@Configuration
註解標註了XXXAutoConfiguration
類,爲什麼還需要在spring.factories
文件中對其進行聲明。這主要是因爲,spring boot默認會掃描主類所在的目錄以及子目錄下的類,而XXXAutoConfiguration
類一般是第三方jar包,默認是不會被掃描到的。通過spring.factories
進行聲明,spring boot就會對其進行加載,然後通過過濾條件對不需要的bean進行過濾,從而實現自動裝配的目的。這裏所說的過濾,一般都是通過查看配置文件中是否配置了相關屬性,或者classpath中是否存在某個類來進行的,這樣我們在使用的時候,就只需要在配置文件中進行配置,即可直接使用相關的bean,而無需顯示聲明。
1.3 示例
這裏我們以汽車爲例,自動裝載一個Car
對象,而裝配這個Car
對象需要很多的屬性,我們使用CarProperties
對象來存儲這些屬性,而這些屬性則是通過application.properties
文件聲明的,因而需要使用@ConfigurationProperties
對CarProperties
進行標註,從而實現自動讀取配置屬性,如下是CarProperties
的聲明:
@ConfigurationProperties(prefix = "car")
public class CarProperties {
private String name;
private String price;
// getters and setters
}
如此,CarProperties
就可以自動讀取application.properties
文件中的car.name
和car.price
屬性,並且設置到CarProperties
對象中。讀取了配置文件的屬性之後,我們使用CarAutoConfiguration
來構建對這些屬性的使用方式,這裏我們就是通過這些屬性創建一個Car
對象。如下是CarAutoConfiguration
的實現:
@Configuration
@EnableConfigurationProperties(CarProperties.class)
public class CarAutoConfiguration {
@Bean
@ConditionalOnProperty(value = "car.enabled", havingValue = "true")
public Car car(CarProperties properties) {
Car car = new Car();
car.setName(properties.getName());
car.setPrice(properties.getPrice());
return car;
}
}
關於CarAutoConfiguration
有三點需要說明:
CarAutoConfiguration
需要使用@Configuration
註解進行標註,否則其不會生效;- 需要使用
@EnableConfigurationProperties
引入CarProperties
屬性model,否則其不會自動讀取application.properties
中的屬性; - 在
Car
這個目標對象進行裝載的方法上,一般都需要加一定的限制條件,比如這裏是指明application.properties
配置中必須顯示聲明car.enabled
屬性值爲true
,否則就不會調用car()
方法裝載Car
對象,這麼做的目的主要目的是隻有在用戶需要該對象的時候,才進行裝載。
最後就是需要在META-INF/
目錄下創建spring.factories
文件,並且指定需要自動裝載CarAutoConfiguration
,如下是spring.factories
的聲明:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.car.starter.CarAutoConfiguration
上面這些配置一般是在一個單獨的工程中進行的,當某個使用方需要自動裝配Car
對象時,只需要引入該工程的pom
聲明,然後在自身的application.properties
文件中聲明裝配Car
對象所需要的屬性,即可自動創建Car
對象,如下是一個配置示例:
car.enabled=true
car.name=bentian
car.price=200000
配置完成之後,即可自動注入Car
對象了,而不需要做額外的聲明,如下是一個使用示例:
@RestController
@RequestMapping("/home")
public class IndexController {
@Autowired
private Car car;
@GetMapping("/index")
public String index() {
return "hello, spring boot autoconfigure: " + car.getName() + ", " + car.getPrice();
}
}
在瀏覽器訪問http://localhost:8080/home/index
,即可獲取一段文本:hello, spring boot autoconfigure: bentian, 200000
,說明我們成功進行了自動裝配。
2. 實現原理
關於Spring boot的SPI實現原理,其本質就是對@Configuraion
註解的解析過程,當spring boot通過spring.factories
文件讀取到要自動裝配的類的時候,就會對其進行解析,由於其使用了@Configuraion
註解進行標註,因而只需要將其標註了@Bean
註解的方法封裝爲一個個BeanDefinition
即可,然後由spring boot實例化。至於對這些bean的過濾,則根據所定義的配置進行過濾即可。這裏spring.factories
文件的讀取,其也是在@Configuraion
註解的解析過程中進行的,具體的則是讀取該類上標註的@Import
註解來進行的。
2.1 SpringBootApplication註解
在spring boot啓動類上,我們一般會使用@SpringBootApplication
註解進行標註,然後就可以完成自動裝配的功能。這裏@SpringBootApplication
註解是由多個註解組合而成的,其聲明如下:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
// methods...
}
@Configuration
public @interface SpringBootConfiguration {
}
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
// methods...
}
@SpringBootApplication
是由@SpringBootConfiguraion
和@EnableAutoConfiguraion
兩個註解組合而成的,而@SpringBootConfiguraion
則是由@Configuraion
註解標註的,@EnableAutoConfiguraion
註解則是由@AutoConfigurationPackage
和@Import
兩個註解組合而成的,關於這幾個註解的作用如下:
@Configuraion
:自動裝配的核心類,其是通過ConfiguraionClassPostProcessor
進行解析的;@AutoConfiguraionPackage
:需要自動掃描的類,默認情況下,其會掃描其所標註的類所在的包及其子包下的所有對象,註冊成spring boot的bean;@Import
:用於讀取spring.factories
的類,這裏是AutoConfigurationImportSelector
,其只會讀取spring.factories
文件中key爲org.springframework.boot.autoconfigure.EnableAutoConfiguration
的所有屬性;
2.2 @Configuraion註解的解析
@Configuraion
註解的解析是通過ConfiguraionClassPostProcessor
來進行的,ConfiguraionClassPostProcessor
實現了BeanDefinitionRegistryPostProcessor
接口,其會在spring boot解析完所有的BeanDefinition
,並且調用BeanFactoryPostProcessor.postProcessBeanFactory()
方法之前進行調用。ConfiguraionClassPostProcessor
的實現代碼如下:
public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,
PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
processConfigBeanDefinitions(registry);
}
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();
// 獲取BeanDefinitionRegistry中所有使用@Configuraion註解進行了標註的bean
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
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));
}
}
// 判斷如果沒有使用@Configuration標註的bean則退出
if (configCandidates.isEmpty()) {
return;
}
// 對使用@Configuraion進行標註的bean進行排序
configCandidates.sort((bd1, bd2) -> {
int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
return Integer.compare(i1, i2);
});
// 設置BeanNameGenerator
SingletonBeanRegistry sbr = null;
if (registry instanceof SingletonBeanRegistry) {
sbr = (SingletonBeanRegistry) registry;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
}
}
}
// 初始化Environment
if (this.environment == null) {
this.environment = new StandardEnvironment();
}
// 這個ConfigurationClassParser是解析@Configuration註解關鍵的一個類,每個使用@Configuraion註解
// 進行標註的類都會初始化一個ConfigurationClassParser進行解析
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 {
// 對使用@Configuration註解標註的類進行解析
parser.parse(candidates);
// 對解析得到的@Configuration類定義進行校驗
parser.validate();
Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
// 讀取@Configuraion標註的類新創建的類定義,比如其使用@Bean進行標註的方法,
// 將其封裝爲一個個BeanDefinition,然後註冊到BeanDefinitionRegistry中
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);
// 略...
}
}
上面的流程其實比較簡單,主要是查找BeanDefinitionRegistry
中註冊的所有使用@Configuration
註解進行標註的類,然後將每一個類都交由一個ConfigurationClassParser
進行解析。這裏需要說明的一點是,在初始狀態,BeanDefinitionRegistry
中只有啓動類使用@Configuration
進行了標註,ConfigurationParser
會對其解析,然後根據其註解配置,會繼續掃描得到的BeanDefinition
,並且掃描其是否包含@Configuration
註解,從而遞歸的進行解析。
2.3 ConfiguraionClassParser解析
ConfiguraionClassParser.parse()
的解析過程最終是交由doProcessConfigurationClass()
方法進行的,其調用過程中有如下代碼:
// Process any @Import annotations
processImports(configClass, sourceClass, getImports(sourceClass), true);
這個方法就是讀取spring.factories
,並且解析其需要自動裝配的類的入口。我們首先來看getImports()
方法,其是用於讀取spring.factories
的:
private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
Set<SourceClass> imports = new LinkedHashSet<>();
Set<SourceClass> visited = new LinkedHashSet<>();
collectImports(sourceClass, imports, visited);
return imports;
}
// 遞歸的解析sourceClass上標註的註解,找到使用@Import註解聲明的類,
// 對於@SpringBootApplication註解就是AutoConfigurationImportSelector
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
throws IOException {
if (visited.add(sourceClass)) {
for (SourceClass annotation : sourceClass.getAnnotations()) {
String annName = annotation.getMetadata().getClassName();
if (!annName.startsWith("java") && !annName.equals(Import.class.getName())) {
collectImports(annotation, imports, visited);
}
}
imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
}
}
上面的getImports()
方法就是獲取當前類上使用@Import
註解聲明的類,然後將其交由processImports()
方法進行處理,如下是該方法的源碼:
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {
for (SourceClass candidate : importCandidates) {
if (candidate.isAssignable(ImportSelector.class)) {
// 判斷當前使用@Import註解聲明的類是否實現了ImportSelector接口,如果實現了,則交由
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
ParserStrategyUtils.invokeAwareMethods(
selector, this.environment, this.resourceLoader, this.registry);
// 由於AutoConfigurationImportSelector不僅實現了ImportSelector接口,也實現了DeferredImportSelector
// 接口,因而最終是交由其進行處理
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
} else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}
}
這裏的讀取過程最終還是交由ImportSelector
進行,在讀取完成之後,由於該類使用了@Configuration
註解進行標註,因而最終還是會遞歸的交由上面的doProcessConfigurationClass()
方法進行處理,最終實現bean的自動裝配。
3. 小結
本文首先以一個示例講解了spring boot自動裝配機制的使用方式,然後講解了其實現原理,主要是通過@Configuration
註解的解析過程來實現的。