SpringBoot自动装配原理

一、前言

SpringBoot 的诞生,简化了 Spring 应用程序的初始设置和开发过程,使开发人员可以更快速、更便捷地构建和运行 Spring 应用程序。相信大家在使用或学习 SSM 整合开发 Spring 应用程序时,会感受到写一堆 XML 配置文件的繁琐,即便是基于注解的配置,也无法避免一些重复而冗余的工作。而 SpringBoot 的出现,解决了这一问题,使得开发者可以更专注于业务逻辑的开发。

Spring Boot采用了"约定优于配置 "的理念,大大减少了开发者的配置工作。它提供了一种自动装配机制,即自动的将 Spring 应用程序及其所需的依赖关系进行绑定和装配,如引入了spring-boot-starter-web依赖后,我们无需写 web 相关的 xml 或者配置类,就拥有了 web 环境,这就得益于 SpringBoot 的自动装配机制。 但是,Spring Boot 的自动装配并不是黑箱操作,它的工作原理是开放透明的。通过理解 Spring Boot 的自动装配原理,我们可以更好地利用这个框架,更灵活地定制和扩展我们的应用程序,我们也能创作出自定义的 starter。

那么在接下来的内容中,我将详细介绍Spring Boot的自动装配原理,希望能够帮助到大家。

二、从启动类出发

还是以 web 环境为例,当引入spring-boot-starter-web依赖后,我们仅需在启动类上加上@SpringbootApplication 注解后,web相关的配置就生效了,也就有了 web 环境。那我们就来研究一下@SpringbootApplication 注解为我们做了什么吧。

@SpringbootApplication 其实是一个组合注解。除了元注解外,还有有3个注解,分别是@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan。@SpringbootApplication的定义如下:

java 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {...}
  • @SpringBootConfiguration表示当前类是 Spring Boot 应用程序的主配置类,他的本质其实就是@Configuration。所以理论上你可以将所有的 @Configuration 替换为 @SpringBootConfiguration ,因为 @SpringBootConfiguration 是 @Configuration 的一个特殊形式,只是我们约定只有 Spring Boot 应用程序的主配置类即启动类使用@SpringBootConfiguration。
  • @ComponentScan 和单 Spring 环境中的 @ComponentScan 注解是一样的,Spring 会从声明该注解的类的包开始,扫描该包及其子包。注意这里排除了 @AutoConfiguration 声明的 Bean,别不小心写错了导致被排除。
  • @EnableAutoConfiguration 表示开启自动配置。而今天我们的主题就是探讨SpringBoot的自动配置,也就是研究 @EnableAutoConfiguration 的原理。

三、@EnableAutoConfiguration

@EnableAutoConfiguration 的定义如下:

java 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {...}
  • @AutoConfigurationPackage:它会注册一个AutoConfigurationPackages.Registrar Bean,这个 Bean 会将 @EnableAutoConfiguration 注解所在的类的包路径注册为自动配置的基础路径(basePackage)。这个 basePackage 可以被用的其他配置类中使用。比如spring-boot-starter-data-jpa就会将这个basePackage设置为实体类的扫描路径。
  • @Import({AutoConfigurationImportSelector.class}):这个注解导入了AutoConfigurationImportSelector,而 AutoConfigurationImportSelector 就是自动装配的最终实现。

AutoConfigurationImportSelector 实现了 ImportSelector 接口,ImportSelector 的 selectImports方法返回一个字符串数组,这个数组中的每个字符串都是一个要导入的配置类的全类名。

那接下来,我们看看 AutoConfigurationImportSelector 是怎么实现 selectImports 这个方法吧。

java 复制代码
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!this.isEnabled(annotationMetadata)) {
        // 如果自动配置没有开启,返回空数组
        return NO_IMPORTS;
    } else {
        // 核心:调用 getAutoConfigurationEntry 方法找到 AutoConfigurationEntry
        AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
        // 返回配置类集合
        return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }
}

protected static class AutoConfigurationEntry {
    // 配置类集合
    private final List<String> configurations;
    // 需要排除的类集合
    private final Set<String> exclusions;
    // ...
}

核心是调用 getAutoConfigurationEntry 方法找到 AutoConfigurationEntry,进入 getAutoConfigurationEntry 方法

java 复制代码
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    if (!this.isEnabled(annotationMetadata)) {
        // 如果自动配置没有开启,返回空的 AutoConfigurationEntry
        return EMPTY_ENTRY;
    } else {
        // 获取 @EnableAutoConfiguration 注解的属性
        AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
        // 核心:获取所有候选的配置类
        List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
        // 移除重复的配置类
        configurations = this.removeDuplicates(configurations);
        // 获取所有的排除项,即那些在 @EnableAutoConfiguration 注解的 exclude 属性中指定的类
        Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
        // 检查排除项,如果排除项存在但并不在配置类当中,也就是用户错误的配置了排除项,抛出异常
        this.checkExcludedClasses(configurations, exclusions);
        // 移除所有的排除项
        configurations.removeAll(exclusions);
        // 对配置类进行过滤,有的配置类会有条件注解,只保留那些实际需要进行自动配置的类。
        configurations = this.getConfigurationClassFilter().filter(configurations);
        // 触发自动配置导入事件。
        this.fireAutoConfigurationImportEvents(configurations, exclusions);
        // 创建并返回一个新的AutoConfigurationEntry,设置所有需要自动配置的类和排除的类。
        return new AutoConfigurationEntry(configurations, exclusions);
    }
}

核心是调用getCandidateConfigurations找到所有候选的配置类,进入 getCandidateConfigurations 方法

java 复制代码
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = new ArrayList<>(
         // 核心
         SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader())
    );
    // 核心
    ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
    Assert.notEmpty(configurations,
          "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
                + "are using a custom packaging, make sure that file is correct.");
    return configurations;
}

这里主要有两个方法,SpringFactoriesLoader.loadFactoryNamesImportCandidates.load,这两个方法就不全部拿出来了,简化出核心。

SpringFactoriesLoader.loadFactoryNames的核心:

java 复制代码
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    String factoryTypeName = factoryType.getName();
    // 加载 SpringFactories 并且拿到 key 为 factoryTypeName 的配置类
    return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
   
    // 从类路径下获取 FACTORIES_RESOURCE_LOCATION 资源
    Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories");
    // 组装成 map 返回
    return result;
}

也就是从类路径下的META-INF/spring.factories文件中加载所有的配置,其中factoryTypeName org.springframework.boot.autoconfigure.EnableAutoConfiguration。也就是说,META-INF/spring.factories文件中org.springframework.boot.autoconfigure.EnableAutoConfiguration后面的值(配置类的类名)将作为配置类自动装配。

ImportCandidates.load 的核心:

java 复制代码
ClassLoader classLoaderToUse = decideClassloader(classLoader);
String location = String.format("META-INF/spring/%s.imports", annotation.getName());
Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);

这个 annotation 是 @AutoConfiguration,那么这个 location 就是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration,也就是从类路径下的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中加载所有的配置类。

其实到这我们已经知道找到底了,就是从类路径下的META-INF/spring.factoriesMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中加载所有的配置类,也就实现了自动配置。

说明:我的 sprignboot 版本是 2.7.18,如果使用在这之前的版本,可能只能从META-INF/spring.factories加载所有的配置类,从最后的getCandidateConfigurations方法注释上可以看到:出于向后兼容的原因,它还将考虑SpringFactoriesLoader和getSpringFactoriesLoaderFactoryClass() 。即兼容从META-INF/spring.factories加载配置类。

大胆猜测:META-INF/spring.factories不仅仅是定义自动配置类的,他可以写上各种工厂,如ApplicationContextInitializerApplicationListener,所以在后来版本中,定义自定装配的类写在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中。

四、spring-boot-autoconfigure

现在我们知道了自动配置原理了,那赶紧看看spring-boot-starter-web有没有spring.factories或者org.springframework.boot.autoconfigure.AutoConfiguration.imports文件吧。当你兴冲冲地点开 jar 包,很遗憾,没有。

啊?我们咔咔咔分析了一大堆,这骗我呢?

没有骗你,我们知道,引入spring-boot-starter-web,其实它也引入了其他的依赖,比如spring-web spring-boot-starter-tomcat,以及spring-boot-starter。而spring-boot-starter又引入了spring-boot-autoconfigure

spring-boot-autoconfigure模块是实现 Spring Boot 自动配置功能的关键,它包含了大量的自动配置类,用于自动配置Spring应用的各种功能和组件。

可以看到,Spring 官方已经对各种各样的场景写好了配置类,当你引入某个 starter 如kafka,那对应的 kafka的配置类就会生效。而这个配置类能自动配置,确实是因为我们前面分析的那样,spring-boot-autoconfigure的类路径下存在spring.factories或者org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。

在这个文件中,定义了需要自动装配的配置类。总共144个:

五、自定义starter

知道原理后,就能自定义 starter 了。我们就做一个最简单的,让 SpringBoot 帮我自动装配一个 User 和一个 Dog,这个 User 和 Dog 对象只有 name、age 属性。

第一步:编写配置类。

java 复制代码
@AutoConfiguration
public class UserAutoConfiguration{
    @Bean
    public User user(){
        return new User("小小", 18);
    }
}
@AutoConfiguration
public class DogAutoConfiguration{
    @Bean
    public Dog dog(){
        return new Dog("大大", 2);
    }
}

第二步:编写自动装配的文件,上面说到 Spring 做了兼容,所以两种文件都可以,二选一即可,即使两个都写 Spring 也会帮你去重。

  • spring.factories
arduino 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.example.starter.config.UserAutoConfiguration,\
org.example.starter.config.DogAutoConfiguration
  • org.springframework.boot.autoconfigure.AutoConfiguration.imports
arduino 复制代码
org.example.starter.config.UserAutoConfiguration
org.example.starter.config.DogAutoConfiguration

第三步:将这个模块打成 jar 包 第四步:引入这个 jar 包并启动容器

java 复制代码
@SpringBootApplication
public class SystemApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SystemApplication.class);
        User user = context.getBean(User.class);
        Dog dog = context.getBean(Dog.class);
        System.out.println(user);
        System.out.println(dog);
    }
}

这样就完成了自定义 starter。

六、结束语

最后,愿你我都有自由的灵魂。

相关推荐
我叫啥都行41 分钟前
计算机基础知识复习9.7
运维·服务器·网络·笔记·后端
无名指的等待7121 小时前
SpringBoot中使用ElasticSearch
java·spring boot·后端
.生产的驴2 小时前
SpringBoot 消息队列RabbitMQ 消费者确认机制 失败重试机制
java·spring boot·分布式·后端·rabbitmq·java-rabbitmq
AskHarries3 小时前
Spring Boot利用dag加速Spring beans初始化
java·spring boot·后端
苹果酱05673 小时前
一文读懂SpringCLoud
java·开发语言·spring boot·后端·中间件
掐指一算乀缺钱3 小时前
SpringBoot 数据库表结构文档生成
java·数据库·spring boot·后端·spring
计算机学姐6 小时前
基于python+django+vue的影视推荐系统
开发语言·vue.js·后端·python·mysql·django·intellij-idea
JustinNeil6 小时前
简化Java对象转换:高效实现大对象的Entity、VO、DTO互转与代码优化
后端
青灯文案16 小时前
SpringBoot 项目统一 API 响应结果封装示例
java·spring boot·后端
微尘87 小时前
C语言存储类型 auto,register,static,extern
服务器·c语言·开发语言·c++·后端