Appearance
Spring Boot 自动配置
本文介绍Spring Boot自动配置原理。
1. 注解@Import
本小节介绍注解@Import的用法。
1.1 引入配置类
@Import最基本的用法是引入另外的类(即使该类没有标注@Configuration注解),使其成为配置类。
java
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(Config1.class);
applicationContext.refresh();
for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
}
}
@Configuration
@Import({Config2.class})
class Config1{
}
class Config2{
@Bean
public Bean1 bean1(){
return new Bean1();
}
}
class Bean1{
}在容器中,可以发现Config,Config,Bean1都是组件。
1.2 引入ImportSelector
在@Import中,除了可以引入普通类,还可以引入实现了ImportSelector接口的类,作用是将该接口selectImports()的返回值所对应的类加入到容器中。
java
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(Config1.class);
applicationContext.refresh();
for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
}
}
@Configuration
@Import({MyImportSelector.class})
class Config1{
}
class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{Bean1.class.getName()};
}
}
class Bean1{
}在容器中,只存在Config和Bean1,ImportSelector的实现类并没有加入到容器中。
1.3 引入DeferredImportSelector
我们先来看下面这个例子:
- 首先通过配置类
Config1引入了Bean1; - 然后通过
@Import+ImportSelector的方式引入另一个配置类Config2,在该配置类中又引入了Bean1; - 最终容器中会是哪个
Bean1呢?
java
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.setAllowBeanDefinitionOverriding(true);
applicationContext.register(Config1.class);
applicationContext.refresh();
for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
Map<String, Bean1> beansOfType = applicationContext.getBeansOfType(Bean1.class);
System.out.println(beansOfType);
}
}
// 可以看成 本项目配置
@Configuration
@Import({MyImportSelector.class})
class Config1 {
@Bean
public Bean1 bean1() {
return new Bean1("本项目");
}
}
class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{Config2.class.getName()};
}
}
// 可以看成 第三方项目配置
class Config2 {
@Bean
public Bean1 bean1() {
return new Bean1("第三方");
}
}
class Bean1 {
private String name;
public Bean1(String name) {
this.name = name;
}
@Override
public String toString() {
return "Bean1{" +
"name='" + name + '\'' +
'}';
}
}结果如下:
txt
11:32:23.469 [main] INFO org.springframework.beans.factory.support.DefaultListableBeanFactory -- Overriding bean definition for bean 'bean1' with a different definition: replacing [Root bean: class=null; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; fallback=false; factoryBeanName=com.lee.t2.Config2; factoryMethodName=bean1; initMethodNames=null; destroyMethodNames=[(inferred)]; defined in class path resource [com/lee/t2/Config2.class]] with [Root bean: class=null; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; fallback=false; factoryBeanName=config1; factoryMethodName=bean1; initMethodNames=null; destroyMethodNames=[(inferred)]; defined in com.lee.t2.Config1]
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
config1
com.lee.t2.Config2
bean1
{bean1=Bean1{name='本项目'}}可以发现,容器中存在的Bean1是配置类Config1引入的,而且第一行提示出现了Bean定义覆盖:Overriding bean definition for bean 'bean1',所以,其实是先引入Config2中定义的Bean1,然后再引入Config1中的Bean1,此时发生了覆盖。
我们可以通过容器设置,禁止覆盖:
java
applicationContext.setAllowBeanDefinitionOverriding(false);设置后,再次运行上面的程序,那么就会报错。
如果我们想先引入自己编写的配置类,再引入ImportSelector中指定的配置的,可以继承DeferredImportSelector:
java
class MyImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{Config2.class.getName()};
}
}这样,容器中的Bean1就是第三方的了。
1.4 引入ImportBeanDefinitionRegistrar
我们也可以在@Import中引入ImportBeanDefinitionRegistrar接口实现类,以编程的方式向容器中加入其他组件。
java
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.setAllowBeanDefinitionOverriding(true);
applicationContext.register(Config1.class);
applicationContext.refresh();
for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
Map<String, Bean1> beansOfType = applicationContext.getBeansOfType(Bean1.class);
System.out.println(beansOfType);
}
}
@Configuration
@Import({MyImportBeanDefinitionRegistrar.class})
class Config1 {
@Bean
public Bean1 bean1() {
return new Bean1("本项目");
}
}
class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
/**
* 以编程的方式向容器中加入BeanDefinition
* @param importingClassMetadata 注解元信息
* @param registry 容器
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(Config2.class)
.setScope("singleton")
.getBeanDefinition();
registry.registerBeanDefinition("config2", beanDefinition);
}
}
class Config2 {
@Bean
public Bean1 bean1() {
return new Bean1("第三方");
}
}
class Bean1 {
private String name;
public Bean1(String name) {
this.name = name;
}
@Override
public String toString() {
return "Bean1{" +
"name='" + name + '\'' +
'}';
}
}结果如下:
txt
12:00:13.294 [main] INFO org.springframework.beans.factory.support.DefaultListableBeanFactory -- Overriding bean definition for bean 'bean1' with a different definition: replacing [Root bean: class=null; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; fallback=false; factoryBeanName=config1; factoryMethodName=bean1; initMethodNames=null; destroyMethodNames=[(inferred)]; defined in com.lee.t2.Config1] with [Root bean: class=null; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; fallback=false; factoryBeanName=config2; factoryMethodName=bean1; initMethodNames=null; destroyMethodNames=[(inferred)]; defined in com.lee.t2.Config2]
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
config1
bean1
config2
{bean1=Bean1{name='第三方'}}可以发现,容器中存在的Bean1是第三方配置中引入的,因此,整个项目是先往容器中加入本项目的Bean1,然后引入第三方的容器Bean1,发生覆盖。ImportBeanDefinitionRegistrar的功能和DeferredImportSelector的作用类似。
2. 条件注解@Conditional
2.1 基本使用
在 Spring 框架中,@Conditional 注解用于实现条件化的 Bean 注册或配置,只有当特定条件满足时,相关的 Bean 或配置才会被加载到 Spring 容器中。
其所需参数是实现了Condition接口的实现类,接口定义如下:
java
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}ConditionContext context:提供了对容器上下文的访问,可以看出是容器对象;AnnotatedTypeMetadata metadata:提供对标注了@Conditional的类或方法的元数据访问,主要用于获取注解信息。
因此,只有matches方法返回true,那么@Conditional注解标注的配置类或方法才生效。
示例如下:
java
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.setAllowBeanDefinitionOverriding(true);
applicationContext.register(Config1.class);
applicationContext.refresh();
for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
}
}
class MyCondition implements Condition{
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return false;
}
}
@Configuration
@Conditional({MyCondition.class})
class Config1{
@Bean
public Bean1 bean1(){
return new Bean1();
}
}
class Bean1{
}可以发现,容器中并没有Bean1和Config1,是由于@Conditional注解生效了,并没有满足条件。
2.2 自定义条件注解
我们可以基于@Conditional注解,自定义条件注解,使得条件注解的使用更加直观。
在这个例子中,我们自定义一个条件注解,只有类路径下存在某个类时,才会使得配置类或组件定义生效。
java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnClassCondition.class)
public @interface MyConditionalOnClass {
// 指定类的全限定名称
String className();
}实现OnClassCondition类:
java
public class OnClassCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> attributes = metadata.getAnnotationAttributes(MyConditionalOnClass.class.getName());
String className = attributes.get("className").toString();
// ClassUtils是Spring提供的工具类,isPresent() 判断某个类是否存在于类路径下
boolean present = ClassUtils.isPresent(className, null);
return present;
}
}最后,使用如下:
java
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.setAllowBeanDefinitionOverriding(true);
applicationContext.register(Config1.class);
applicationContext.refresh();
for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
System.out.println(beanDefinitionName);
}
}
}
@Configuration
@MyConditionalOnClass(className = "com.lee.t3.WebIndicator")
class Config1{
@Bean
public Bean1 bean1(){
return new Bean1();
}
}
class Bean1{
}只要我们的类路径下存在com.lee.t3.WebIndicator这个类,那么配置Config1就会生效,容器中就有相关组件。
2.3 其他条件注解
Spring 提供了多个基于 @Conditional 的内置条件注解,简化了常见场景的使用:
| 注解 | 作用 | 示例 |
|---|---|---|
@ConditionalOnClass | 当指定类存在于类路径时启用 | @ConditionalOnClass(DataSource.class) |
@ConditionalOnMissingClass | 当指定类不存在于类路径时启用 | @ConditionalOnMissingClass("com.example.SomeClass") |
@ConditionalOnBean | 当容器中存在指定 Bean 时启用 | @ConditionalOnBean(name = "dataSource") |
@ConditionalOnMissingBean | 当容器中不存在指定 Bean 时启用 | @ConditionalOnMissingBean(MyService.class) |
@ConditionalOnProperty | 当指定属性满足条件时启用 | @ConditionalOnProperty(name = "feature.enabled", havingValue = "true") |
@ConditionalOnResource | 当指定资源存在时启用 | @ConditionalOnResource(resources = "classpath:config.properties") |
@ConditionalOnExpression | 当 SpEL 表达式为 true 时启用 | @ConditionalOnExpression("#{true && false }") |
@ConditionalOnJava | 当运行时 Java 版本满足条件时启用 | @ConditionalOnJava(JavaVersion.NINE) |
@ConditionalOnWebApplication | 当应用是 Web 应用时启用 | @ConditionalOnWebApplication |
@ConditionalOnNotWebApplication | 当应用不是 Web 应用时启用 | @ConditionalOnNotWebApplication |
3. 自动配置原理
Spring Boot的自动配置就是@Import+@Conditional注解的配合使用。
3.1 问题一:如何开启自动配置
在Spring Boot程序中,我们通常在主程序上标注@SpringBootApplication注解,这是一个复合注解,其中有@EnableAutoConfiguration注解:
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}在@EnableAutoConfiguration注解上有@Import注解,其引入了AutoConfigurationImportSelector类,从其名字上看,其实现了ImportSelector类,实际上,是实现了DeferredImportSelector接口。
正是由AutoConfigurationImportSelector类实现了自动配置。
3.2 问题二:如何引入第三方配置类
在这一小节中,介绍Spring Boot如何引入第三方配置类。
在AutoConfigurationImportSelector的构造方法中,有一个关键属性autoConfigurationAnnotation,其默认值是AutoConfiguration这个注解类型:
java
AutoConfigurationImportSelector(Class<?> autoConfigurationAnnotation) {
this.autoConfigurationAnnotation = (autoConfigurationAnnotation != null) ? autoConfigurationAnnotation
: AutoConfiguration.class;
}AutoConfiguration的全限定类名为:org.springframework.boot.autoconfigure.AutoConfiguration。
在AutoConfigurationImportSelector中,最终会调用ImportCandidates.load()方法,这个方法
java
// annotation 就是 AutoConfigurationImportSelector中的autoConfigurationAnnotation
public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
// 确定类加载器
ClassLoader classLoaderToUse = decideClassloader(classLoader);
// location 实际为 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
String location = String.format(LOCATION, annotation.getName());
// 找类路径下路径为location的文件
Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
// 解析找到的文件
List<String> importCandidates = new ArrayList<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
importCandidates.addAll(readCandidateConfigurations(url));
}
return new ImportCandidates(importCandidates);
}以上方法就是去类路径为META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports的文件中获取每一行的配置,在这些文件中,每一行就代表一个配置类。
Spring Boot 2.7 introduced a new
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsfile for registering auto-configurations, while maintaining backwards compatibility with registration inspring.factories. With this release, support for registering auto-configurations inspring.factoriesusing theorg.springframework.boot.autoconfigure.EnableAutoConfigurationkey has been removed in favor of the imports file. Other entries inspring.factoriesunder other keys are unaffected.Libraries targeting both Spring Boot 3.x and 2.x can safely list their auto-configuration classes in both
spring.factoriesandAutoConfiguration.imports. Spring Boot 2.7, which supports both locations, will de-duplicate any entries that are listed twice.
最终,解析出来的全类名就是要加入到容器中的组件。
总结,第三方库将要加入容器的配置类,按行写入META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中,Spring Boot会通过DeferredImportSelector机制,从这些文件中读取类名,将其加入到容器中。
3.3 按条件加入
由于AutoConfigurationImportSelector实现的是DeferredimportSelector接口,所以本项目的配置类会先解析生效,并且在Spring Boot中,禁止了组件定义覆盖,所以第三方的配置类,会按条件生效。
以org.springframework.boot.autoconfigure.web.servlet包中的DispatcherServletAutoConfiguration自动配置类为例,如下:
java
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@AutoConfiguration(after = ServletWebServerFactoryAutoConfiguration.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
public class DispatcherServletAutoConfiguration {
...
}看到在类上有注解@ConditionalOnWebApplication(type = Type.SERVLET)和@ConditionalOnClass(DispatcherServlet.class),含义如下:
- 当应用类型为
SERVLET时,该配置类才生效; - 类路径存在
DispatcherServlet类时,该配置类才生效;
只有同时满足了以上条件,DispatcherServletAutoConfiguration才生效,因此与WEB开发相关的组件才会注入到容器中。
4. 自定义starter
基于以上的自动配置原理,我们就可以自己定义starter,只要引入自定义的starter,那么就可以使用其中的组件。
我们在自定义的starter中定义一个计算器,只要引入starter,就可以使用计算器。
4.1 总体结构
首先看看总体结构,在一个项目中创建两个模块:
- my-calculator-starter:提供功能类、配置类;
- my-starter-autoconfigure:主要提供自动配置类的位置;

4.2 my-calculator-starter
首先定义功能类:
java
public class Calculator {
public int add(int num1, int num2){
return num1 + num2;
}
}之后,定义自动配置类,首先引入spring-boot-starter:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>java
@Configuration
@ConditionalOnMissingBean(Calculator.class)
public class CalculatorAutoConfiguration {
@Bean
public Calculator calculator(){
return new Calculator();
}
}4.3 my-starter-autoconfigure
这个模块主要是为了提供META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,当然写在my-calculator-starter模块中也可以。
文件内容如下,就是自动配置类全限定名:
txt
org.example.CalculatorAutoConfiguration4.4 my-calculator-starter(2)
在该项目中引入自动配置依赖:
xml
<dependency>
<groupId>org.example</groupId>
<artifactId>my-starter-autoconfigure</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>4.5 引入依赖
最后,只需要在我们的项目中引入以下依赖:
xml
<dependency>
<groupId>org.example</groupId>
<artifactId>my-calculator-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>就可以在项目中正常使用:
java
@SpringBootApplication
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(Main.class, args);
Calculator bean = applicationContext.getBean(Calculator.class);
System.out.println(bean.add(1, 2)); // 3
}
}也可以使用自动注入功能。