SpringBoot 自动配置原理

SpringBoot 自动配置原理

这篇文章主要学习SpringBoot的自动配置实现原理(经典面试题目👀️ ,一言不合就问到了😕 )

SpringBoot Starter

说自动配置实现之前,得先说一下 Spring Boot Starter, Spring Boot Starter 是一系列依赖关系的集合,因为它的存在,项目的依赖之间的关系对我们来说变的更加简单了。说白了就是SpringBoot帮我们直接统一了依赖的引入以及版本管理,使我们开发人员不需要把太多工作放在依赖管理上面,减少依赖冲突问题的出现。比如我们web项目开发会引入如下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>  

点进去

image-ipco.png

里面引入了spring web相关的依赖,再点进 spring-boot-starter

image-fcix.png

里面是spring的核心依赖以及springboot自动配置的核心依赖,这就是SpringBoot的启动器了

SpringBoot 自动配置实现原理

弄清楚几个关键注解以及SpringBoot启动流程中做了什么事儿,也就明白了自动配置的实现原理了

@SpringBootApplication 注解

@SpringBootApplication 是 Spring Boot 框架中的核心注解之一,用于标识我们的主配置类,通常是项目的入口类。它整合了多个关键的功能,大大简化了 Spring Boot 应用的开发和配置过程 ,@SpringBootApplication 注解实际上包含了以下三个注解的功能:

  • @Configuration:标注该类是一个配置类,允许使用 @Bean 注解来定义和注册 Bean。
  • @EnableAutoConfiguration:启用 Spring Boot 的自动配置机制。Spring Boot 会根据项目的依赖和配置来决定应用程序的配置,从而减少了手动配置的工作量。
  • @ComponentScan:启用组件扫描,自动扫描并加载符合条件的组件,包括 @Controller@Service@Repository 等。

点进 @SpringBootApplication 注解可以看到

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(...)
public @interface SpringBootApplication {
    //.....
}

@SpringBootConfiguration 注解点开就是 @Configuration 注解,声明当前类可以作为配置类

@EnableAutoConfiguration 注解

前面说了,这个注解就是用来开启自动配置的功能,先看看定义

@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 {};
}

@AutoConfigurationPackage 注解

@AutoConfigurationPackage 注解主要作用是指定 Spring Boot 扫描的包。默认情况下,它会扫描启动类(即带有 @SpringBootApplication 注解的类)所在的包及其子包下的所有组件。通过这个注解,Spring Boot 能够自动发现并加载这些包下的所有 Spring 组件,如使用 @Service@Controller@Repository 等注解的类,并将它们注册到 Spring 容器中。

也许你会疑惑 @AutoConfigurationPackage@ComponentScan 区别是什么(我也疑惑😄 ),请看解释:

  • @ComponentScan:这个注解用于自动扫描并加载Spring应用程序上下文中的组件,比如@Component、@Service、@Repository、@Controller等。它告诉Spring在哪里查找Spring管理的组件
  • @AutoConfigurationPackage:这个注解用于指定基础包,用于扫描Spring Boot组件以进行自动配置。通常与@SpringBootApplication一起使用,用于定义主应用程序类所在的包

总之,@ComponentScan用于扫描和加载应用程序上下文中的组件,而@AutoConfigurationPackage用于指定用于自动配置的组件的基础包。(我理解直白一点就是 一个扫面咱们开发中使用的注解,一个是扫描SpringBoot框架本身组件,功能有重叠,场景不一样)

@Import 注解

@Import 注解的作用是导入配置类或者 Bean 到当前类中(关于这个注解的学习准备单独整理一篇笔记,这里里知道它的作用就行),前面我们看到 @EnableAutoConfiguration 注解实现中使用了 @Import({AutoConfigurationImportSelector.class}) 导入了 AutoConfigurationImportSelector 类,自动装配核心功能的实现就是通过 AutoConfigurationImportSelector 类,我们重点关注这个类

AutoConfigurationImportSelector 类

先看一下 AutoConfigurationImportSelector 类的继承关系

//实现 DeferredImportSelector 接口
public class AutoConfigurationImportSelector implements DeferredImportSelector, 
             BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
     //......
}

//DeferredImportSelector接口继承ImportSelector接口
public interface DeferredImportSelector extends ImportSelector {

}

//ImportSelector接口定义
public interface ImportSelector {

    String[] selectImports(AnnotationMetadata var1);

    @Nullable
    default Predicate<String> getExclusionFilter() {
        return null;
    }
}

AutoConfigurationImportSelector 类实现了 ImportSelector接口,也就实现了这个接口中的 selectImports方法,该方法主要用于**获取所有符合条件的类的全限定类名,这些类需要被加载到 IOC 容器中,**看该方法的实现

    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
            return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
        }
    }

if (!this.isEnabled(annotationMetadata)) 这一步骤很好理解,是否开启自动配置,如果没有就返回一个 NO_IMPORTS 空字符串类型的数组,看一下 isEnabled 方法的实现

    protected boolean isEnabled(AnnotationMetadata metadata) {
        return this.getClass() == AutoConfigurationImportSelector.class ? (Boolean)this.getEnvironment().getProperty("spring.boot.enableautoconfiguration", Boolean.class, true) : true;
    }

getAutoConfigurationEntry()方法

重点关注 getAutoConfigurationEntry() 方法,该方法的主要作用是用来加载自动配置类。源码如下:

    protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        } else {
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            configurations = this.removeDuplicates(configurations);
            Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
            this.checkExcludedClasses(configurations, exclusions);
            configurations.removeAll(exclusions);
            configurations = this.getConfigurationClassFilter().filter(configurations);
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return new AutoConfigurationEntry(configurations, exclusions);
        }
    }

执行流程

现在我们启动SpringBoot项目,通过调试分析一下启动流程

第一步:

判断自动装配开关是否打开。默认 spring.boot.enableautoconfiguration=true,可在 application.propertiesapplication.yml 中设置

image-yddb.png

第二步:

执行 AnnotationAttributes attributes = this.getAttributes(annotationMetadata); 获取 EnableAutoConfiguration注解中的 excludeexcludeName

image-qmtj.png

第三步:

List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes); 读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件内容(我测试用的是SpringBoot 3.x 以上的版本,跟2.x不一样,2.x版本应该是 META-INF/spring.factories),获取自动配置类的信息

image-ifbo.png

1719850766972.jpg

image-joso.png

第四步:

问题来了,前面我们加载了自动配置类的信息,足足152条之多,而我们依赖只导入了一个web应用的启动依赖,难不成每次要加载这么多配置吗?当然不是,SpringBoot会根据条件进行筛选,满足自动配置条件的类才会被保留,看截图:

image-lyry.png

  • this.removeDuplicates(configurations)这一步很简单,就是把集合去重。
  • this.getExclusions(annotationMetadata, attributes); 记得吗,我们之前获取的 @EnableAutoConfiguration注解中的两个属性 excludeexcludeName,就是用在这里,顾名思义里面是我们排除不进行自动装配的类。
  • this.getConfigurationClassFilter().filter(configurations);将不满足 @ConditionalOnXXX条件的配置类,全部过滤掉

@ConditionalOnXXX 注解就存在于配置类上面

image-qeqa.png

这些类随便点开进去

image-nlaf.png

可以看到类上存在各种条件注解,只有当所有条件全部满足时类才会被创建生效,比如:@ConditionalOnClass注解意思就是当类路径下有指定类的条件下,类才会被加载,那么这一步由谁决定呢?没错是我们👀️ ,当我们导入对应启动器依赖时,指定类它不就存在了吗,那我们自动配置类不就该生效了吗,完美!🎉️

以下是各种条件注解对应的含义:

  • @ConditionalOnBean:当容器里有指定 Bean 的条件下
  • @ConditionalOnMissingBean:当容器里没有指定 Bean 的情况下
  • @ConditionalOnSingleCandidate:当指定 Bean 在容器中只有一个,或者虽然有多个但是指定首选 Bean
  • @ConditionalOnClass:当类路径下有指定类的条件下
  • @ConditionalOnMissingClass:当类路径下没有指定类的条件下
  • @ConditionalOnProperty:指定的属性是否有指定的值
  • @ConditionalOnResource:类路径是否有指定的值
  • @ConditionalOnExpression:基于 SpEL 表达式作为判断条件
  • @ConditionalOnJava:基于 Java 版本作为判断条件
  • @ConditionalOnJndi:在 JNDI 存在的条件下差在指定的位置
  • @ConditionalOnNotWebApplication:当前项目不是 Web 项目的条件下
  • @ConditionalOnWebApplication:当前项目是 Web 项 目的条件下

通过前面分析的流程,最终SpringBoot通过 AutoConfigurationImportSelector类的 selectImports拿到了需要进行自动配置的类信息,最后就拿去创建各种Bean了。

总结

本篇文章中我们学习了SpringBoot自动配置功能的原理,通过源码跟踪直观看到了自动配置的加载流程。

有待补充的点有:

  • SpringBoot怎么读取条件注解
  • 如何实现一个自定义的starter
  • 能不能自定义条件注解
  • so on

菜鸡一枚,说的哪里有问题的话烦请大佬指正❤️