自动配置
Spring Boot的自动配置原理关键在于简化开发者的配置工作,通过预设的默认配置和条件加载来实现项目的快速启动和运行。自动配置在Spring Boot中是一个极为重要的功能,它让开发人员在创建项目时无需手动编写大量的配置文件,而是通过一系列注解和约定来实现配置的自动化。
启用自动配置
Spring Boot 的自动配置是由 @SpringBootApplication
注解启用的,这个注解实际上是三个注解的组合:@SpringBootConfiguration
、@EnableAutoConfiguration
和 @ComponentScan
。
@SpringBootConfiguration
表示这是一个配置类,并且可以包含@Bean
方法。@EnableAutoConfiguration
用来开启自动配置功能,它会根据类路径中的 jar 包来决定哪些自动配置类应该生效。@ComponentScan
用来扫描指定包及其子包下所有带有@Component
及其衍生注解(如@Service
,@Controller
)的类,并将它们注册为 Spring Bean。
自动配置类的位置
自动配置类通常位于 org.springframework.boot.autoconfigure
包中。这些类通过 @ConditionalOnClass
、@ConditionalOnMissingBean
、@ConditionalOnProperty
等条件注解来控制是否生效。
例如,如果类路径中有 Tomcat 服务器,那么 TomcatAutoConfiguration
类就会被激活;如果没有,则不会激活。这样的设计使得 Spring Boot 能够自动选择最适合的组件进行配置。
自动配置的过程
启动应用
当我们运行 Spring Boot 应用的 main
方法时,实际上是启动了一个 Spring 应用的上下文(ApplicationContext
)。
@EnableAutoConfiguration
注解
@EnableAutoConfiguration
注解是自动配置的入口点。这个注解的主要作用是导入 AutoConfigurationImportSelector
类,它负责选择和导入自动配置类。
@AutoConfigurationPackage
注解
@AutoConfigurationPackage
是 @EnableAutoConfiguration
的一个内部注解,它的作用是确保使用 @EnableAutoConfiguration
的配置类所在的包被 Spring 的组件扫描器扫描到。这样,自动配置类才能正确地找到并注册应用中定义的组件。
AutoConfigurationImportSelector
类
AutoConfigurationImportSelector
实现了 ImportSelector
接口,它的 selectImports
方法会被调用,用于选择和返回需要导入的配置类的名称数组。
读取 spring.factories 文件
AutoConfigurationImportSelector
使用 SpringFactoriesLoader
类来加载 META-INF/spring.factories
文件。这个文件位于每个 Spring Boot Starter 项目的 JAR 文件中,并包含了键值对,其中键是 EnableAutoConfiguration
,值是自动配置类的全限定名列表。
条件注解
自动配置类通常会使用一系列的条件注解来确保只在满足特定条件时才应用配置。以下是一些常见的条件注解:
@ConditionalOnClass
:当类路径下存在指定的类时,配置类才会被加载。@ConditionalOnMissingBean
:当容器中没有指定类型的 Bean 时,才会创建新的 Bean。@ConditionalOnProperty
:当指定的属性值满足条件时,配置类才会被加载。@ConditionalOnWebApplication
:当应用是一个 Web 应用时,配置类才会被加载。
实例化和注册 Bean
如果自动配置类的条件注解评估为 true
,则该类中的配置将被应用,并创建相应的 Bean。这些 Bean 被实例化并注册到 Spring 的 ApplicationContext
中。
配置属性覆盖
自动配置类通常会绑定到 application.properties
或 application.yml
文件中的属性。用户可以通过在这些配置文件中设置属性来覆盖自动配置的默认值。
自动配置与外部配置
Spring Boot 还支持从多种来源读取外部配置信息,如 properties 文件、YAML 文件、环境变量等,并将这些配置信息绑定到特定的 Java 对象上。这些对象通常带有 @ConfigurationProperties
注解,它们会被自动注入到自动配置类中。
例如,如果我们有一个 application.properties
文件,其中包含数据库连接的属性,那么 Spring Boot 将会自动创建一个 DataSource
并使用这些属性进行配置。
@Import
注解
在Spring框架中,@Import
是一个类级别的元注解,用于导入一个或多个配置类或者Bean定义。它简化了配置,并允许在一个配置类中引用其他配置类。
使用方式
@Import注解主要用于将类生成的bean快速导入Spring的IoC容器中,它有三种不同的使用方式:直接填写class数组、实现ImportSelector接口的方式、以及实现ImportBeanDefinitionRegistrar接口的方式
导入配置类
创建一个主配置类来导入这两个配置类:
@Configuration
@Import({MyConfig1.class, MyConfig2.class})
public class MainConfig {
}
现在,MainConfig
类包含了 MyConfig1
和 MyConfig2
中定义的所有 Bean。
使用ImportSelector
除了直接导入配置类外,@Import
还可以与ImportSelector
接口一起使用,该接口允许根据条件动态选择要导入的类。
创建选择器
创建一个实现ImportSelector
接口的类:
查看代码
public class ConditionalConfigSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// 根据某些条件选择要导入的类
if (importingClassMetadata.hasAnnotation(SomeCondition.class)) {
return new String[]{"com.example.config.MyConditionalConfig"};
} else {
return new String[0];
}
}
}
使用选择器
在我们的配置类中使用这个选择器:
@Configuration
@Import(ConditionalConfigSelector.class)
public class MainConfig {
}
如果MainConfig
类上有@SomeCondition
注解,则MyConditionalConfig
将被导入。
使用ImportBeanDefinitionRegistrar
ImportBeanDefinitionRegistrar
提供了一种更强大的方式来注册bean定义,因为它提供了对bean定义注册过程的完全控制。
创建注册器
创建一个实现ImportBeanDefinitionRegistrar
接口的类:
查看代码
public class CustomBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 根据条件注册bean定义
if (importingClassMetadata.hasAnnotation(SpecificCondition.class)) {
RootBeanDefinition beanDefinition = new RootBeanDefinition(MySpecialBean.class);
registry.registerBeanDefinition("mySpecialBean", beanDefinition);
}
}
}
使用注册器
在配置类上使用它:
@Configuration
@Import(CustomBeanDefinitionRegistrar.class)
public class MainConfig {
@SpecificCondition
public void someMethod() {}
}
如果MainConfig
类上的someMethod()
方法上有@SpecificCondition
注解,则会注册MySpecialBean
的bean定义。
源码
源码
在ConfigurationClassParser
中
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
boolean checkForCircularImports) {
if (importCandidates.isEmpty()) {
return;
}
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
for (SourceClass candidate : importCandidates) {
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
Predicate<String> selectorFilter = selector.getExclusionFilter();
if (selectorFilter != null) {
exclusionFilter = exclusionFilter.or(selectorFilter);
}
if (selector instanceof DeferredImportSelector deferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, deferredImportSelector);
}
else {
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
}
}
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
else {
// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
// process it as an @Configuration class
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
}
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]: " + ex.getMessage(), ex);
}
finally {
this.importStack.pop();
}
}
}
这个方法processImports
主要用于处理配置类的导入。它接收以下参数:
configClass
:当前正在处理的配置类。currentSourceClass
:当前正在处理的源类。importCandidates
:候选的导入类集合。exclusionFilter
:排除过滤器,用于过滤不需要导入的类。checkForCircularImports
:是否检查循环导入的标志。
方法的主要逻辑如下:
- 如果
importCandidates
为空,则直接返回,因为没有需要处理的导入类。 - 如果
checkForCircularImports
为真且检测到循环导入,则报告错误。 - 否则,将
configClass
压入importStack
栈中,以便跟踪导入链。 - 遍历
importCandidates
中的每个候选类:- 如果候选类是
ImportSelector
的实例,那么委托给ImportSelector
来确定导入哪些类。 - 如果候选类是
ImportBeanDefinitionRegistrar
的实例,那么委托给ImportBeanDefinitionRegistrar
来注册额外的bean定义。 - 否则,将候选类作为@Configuration类进行处理。
registerImport
是为了跟踪和记录导入关系,主要用于检测循环导入。processConfigurationClass
是为了实际解析和注册配置类中的Bean定义和其他配置。
- 如果候选类是
- 如果在处理过程中发生异常,抛出
BeanDefinitionStoreException
。 - 最后,从
importStack
栈中弹出configClass
。
这个方法主要用于解析和处理Spring配置类中的导入关系,包括处理ImportSelector
、ImportBeanDefinitionRegistrar
和其他普通的配置类。
看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);
}
// Otherwise ignore new imported config class; existing non-imported class overrides it.
return;
}
else {
// Explicit bean definition found, probably replacing an import.
// Let's remove the old one and go with the new one.
this.configurationClasses.remove(configClass);
this.knownSuperclasses.values().removeIf(configClass::equals);
}
}
// Recursively process the configuration class and its superclass hierarchy.
SourceClass sourceClass = null;
try {
sourceClass = asSourceClass(configClass, filter);
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
}
while (sourceClass != null);
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"I/O failure while processing configuration class [" + sourceClass + "]", ex);
}
this.configurationClasses.put(configClass, configClass);
}
将配置类转换为SourceClass
,递归地处理配置类及其超类。
下面asSourceClass
具体方法内容。
SourceClass asSourceClass(@Nullable String className, Predicate<String> filter) throws IOException {
if (className == null || filter.test(className)) {
return this.objectSourceClass;
}
if (className.startsWith("java")) {
// Never use ASM for core java types
try {
return new SourceClass(ClassUtils.forName(className, this.resourceLoader.getClassLoader()));
}
catch (ClassNotFoundException ex) {
throw new IOException("Failed to load class [" + className + "]", ex);
}
}
return new SourceClass(this.metadataReaderFactory.getMetadataReader(className));
}
如果类名以"java"开头(即标准Java库中的类),则直接使用反射加载类,并创建SourceClass
实例。
对于其他类,使用MetadataReaderFactory
获取类元数据,并创建SourceClass
实例。
总结步骤为:
- 条件评估:使用
ConditionEvaluator
来确定是否处理配置类。 - 处理已存在的配置类:检查是否存在相同的配置类实例,并决定是否合并或忽略。
- 创建
SourceClass
表示:将配置类转换为SourceClass
表示。 - 递归处理配置类:递归处理配置类及其超类。
- 记录处理过的配置类:将处理过的配置类添加到映射中。
在处理过程中,还会根据需要创建SourceClass
实例,并根据类名和过滤器来决定是否处理该类。这整个过程确保了配置类及其导入的Bean被正确地解析和处理。