Skip to content

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{

}

在容器中,可以发现ConfigConfigBean1都是组件。

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{

}

在容器中,只存在ConfigBean1ImportSelector的实现类并没有加入到容器中。

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{

}

可以发现,容器中并没有Bean1Config1,是由于@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.imports file for registering auto-configurations, while maintaining backwards compatibility with registration in spring.factories. With this release, support for registering auto-configurations in spring.factories using the org.springframework.boot.autoconfigure.EnableAutoConfiguration key has been removed in favor of the imports file. Other entries in spring.factories under other keys are unaffected.

Libraries targeting both Spring Boot 3.x and 2.x can safely list their auto-configuration classes in both spring.factories and AutoConfiguration.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:主要提供自动配置类的位置;

image-20250706150711347

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.CalculatorAutoConfiguration

4.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
    }
}

也可以使用自动注入功能。