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。

六、结束语

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

相关推荐
摇滚侠2 小时前
Spring Boot 3零基础教程,IOC容器中组件的注册,笔记08
spring boot·笔记·后端
程序员小凯5 小时前
Spring Boot测试框架详解
java·spring boot·后端
你的人类朋友5 小时前
什么是断言?
前端·后端·安全
程序员小凯6 小时前
Spring Boot缓存机制详解
spring boot·后端·缓存
i学长的猫7 小时前
Ruby on Rails 从0 开始入门到进阶到高级 - 10分钟速通版
后端·ruby on rails·ruby
用户21411832636027 小时前
别再为 Claude 付费!Codex + 免费模型 + cc-switch,多场景 AI 编程全搞定
后端
茯苓gao7 小时前
Django网站开发记录(一)配置Mniconda,Python虚拟环境,配置Django
后端·python·django
Cherry Zack7 小时前
Django视图进阶:快捷函数、装饰器与请求响应
后端·python·django
爱读源码的大都督8 小时前
为什么有了HTTP,还需要gPRC?
java·后端·架构
码事漫谈8 小时前
致软件新手的第一个项目指南:阶段、文档与破局之道
后端