在 Spring Boot 中引入依赖后什么都不用配置,是真的爽!

哎呦嘞,想记录一下八股文的答案,标题想了半天。大伙早上中午晚上好,又到了背诵八股文的时间了,今天的题目是:请描述一下 Spring Boot 的自动装配原理。今天就来详细聊聊这个话题把!

本文演示的 Spring Boot 版本为 2.6.6,工程使用 Spring Initializr 创建,没有进行任何修改

  1. 本文不涉及源码解析,只提供源码阅读的引导
  2. 文中包含一些源码阅读任务,大家带着问题一边调试一边阅读源码把

自动装配流程如下:

  1. Spring Boot 记载主配置类
  2. Spring 通过 PostProceeor 处理主配置类
  3. Spring 解析到主配置类上的注解,触发自动装配

Spring Boot 如何加载主配置类

在此之前还是得回顾一下 Spring 的几个基础概念

BeanDefinition

Bean 的定义信息,比如是否单例、作用范围、beanName 等,以前我们用 Spring 时编写的 XML 配置文件中的 bean 标签内容最后会转换为 BeanDefinition、值得注意的是,打开 BeanDefinition 的源文件,注释上有这么一句话

This is just a minimal interface: The main intention is to allow a {@link BeanFactoryPostProcessor} to introspect and modify property values and other bean metadata.

翻译:这只是一个最小实现,他的设计意图是为了让 BeanFactoryPostProcessor 能够修改它的属性和数据

拓展阅读(不看也行):Spring 源码第四弹!深入理解 BeanDefinition

ApplicationContext

应用程序运行时提供上下文信息(Context 这个词大家不要想的太高深,你可以简单认为是进程运行过程中的一些状态信息需要找个地方保存,这个地方被称为 Context),而 ApplicationContext 接口定义了下面几种能力:

  1. 随时可以 getBean 的能力,你可以调用它获取已经成熟的 Bean —— 能力来自 BeanFactory 接口
  2. 加载资源文件的能力——能力来自 ResouceLoader 接口
  3. 向监听者发布事件的能力——能力来自 ApplicationEventPublisher 接口
  4. 国际化能力——能力来自 MessageSource 接口
  5. Namespace 隔离能力,这个在 Web 项目中很有用,当一个 Tomcat 需要部署多个 Spring 应用时可以将通用的 Bean 注册到父 ApplicationContext 中,应用自身的 Bean 放到自己的 ApplicationContext 中
上面的内容都可以在注释中找到,这样的设计符合设计模式中的接口隔离原则

这里是不是也回答了 Spring 的一个经典面试题:ApplicationContext 和 BeanFactory 的关系

源码阅读任务:ApplicationContext 是在什么时候被创建,Web 项目中使用的是哪个实现呢?
答案是 AnnotationConfigServletWebServerApplicationContext,看 SpringApplication 中的 run 方法

Spring Boot 将启动类作为主配置文件进行初始化

不知道大家有没有想过我们启动 Spring Boot 时没有指定 XML 或者 JavaConfig,那么 Spring 的配置从哪里来呢?其实你仔细看,我们在启动时将启动类的 Class 对象传递给了 StringApplication,打开注解 @SpringBootApplication -> @SpringBootConfiguration 会发现确实包含了 @Configuration 注解。所以调用 StringApplication 的 run 方法时传递的 Class 就是主配置类,在 Spring Boot 中我们通常会将启动类作为主配置类

@SpringBootApplication
public class RenrenApplication {

    public static void main(String[] args) {
        SpringApplication.run(RenrenApplication.class, args);
    }
}

那问题来了,Spring Boot 是怎么启动类其交给 Spring 的呢

源码阅读任务:Spring Boot 是怎么将其交给 Spring 的 AppicationContrext 的呢

  1. 从启动类开始看,先看 StringApplication 的构造方法
  2. 再看 StringApplication 的 run 方法

Spring 加载主配置类是一个将 Class 对象转换为 BeanDefinition 的过程,其中会涉及到下面几个类:

BeanDefinitionLoader

从 XML 或 JavaConfig 加载 bean 定义。 充当 AnnotatedBeanDefinitionReader、XmlBeanDefinitionReader 和 ClassPathBeanDefinitionScanner 的简单外观。说白了就是一个配置文件加载器的封装,可以加载 XML 和 JavaConfig

BeanDefinitionRegistry

该接口主要定义了保存和获取 BeanDefinition 的方法,至于将 BeanDefinition 保存到哪里就要看子类的实现,AnnotationConfigServletWebServerApplicationContext 会先择将它保存到内存,再具体点就是保存在 AnnotationConfigServletWebServerApplicationContext 实例中的 beanfactory 字段内部的 Map 中

BeanDefinitionReader

是 BeanDefinitionRegistry 的简单适配,负责将 Class 对象转换为 BeanDefinition 并通调用 BeanDefinitionRegistry 内部方法进行 Bean 注册

他们之间的关系大概是这样子的,大家可以在后面阅读源码的时候验证一下

在 Spring Boot 中引入依赖后什么都不用配置,是真的爽!_第1张图片

加载主配置类的过程大概如下:

在 Spring Boot 中引入依赖后什么都不用配置,是真的爽!_第2张图片

断点验证一下

public class SpringApplication {
    ...
    private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
            ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments, Banner printedBanner) {
        ...
        // sources 会包含启动类
        Set sources = getAllSources();
        ...
        // 该方法执行完毕后,启动类的 BeanDefinition 会被注册到 context 上
        load(context, sources.toArray(new Object[0]));
        // 可以下一行下断点,调用一下 context.getDefinition("")
        ...
    }
}

在 Spring Boot 中引入依赖后什么都不用配置,是真的爽!_第3张图片

确实已经将主配置类注册到了 ApplicationContext 中

Spring 如何解析配置类上的注解

既然已经成功加载了主配置类,接下来我们就要看 Spring 怎么解析 @Configuration 等注解了,不过在阅读之前需要先了解几个概念

BeanFactoryPostProcessor

这个接口在 BeanDefinition 接口的注释中有提及到,实现者可以对 BeanDefinition 进行修改

比如 Spring 在初始化 ApplucationContext 时会偷偷注册几个 BeanFactoryPostProcessor,其中包含了关于处理配置注解的 PostProcessor

public interface ApplicationContextFactory {

    ApplicationContextFactory DEFAULT = (webApplicationType) -> {
        try {
            switch (webApplicationType) {
            case SERVLET:
                // 初始化 ApplicationContext
                return new AnnotationConfigServletWebServerApplicationContext();
            ...
        }
        catch (Exception ex) {
            ...
        }
    };
}
public class AnnotationConfigServletWebServerApplicationContext extends ServletWebServerApplicationContext
        implements AnnotationConfigRegistry {

    public AnnotationConfigServletWebServerApplicationContext() {
        this.reader = new AnnotatedBeanDefinitionReader(this);
        ...
    }
}
public class AnnotatedBeanDefinitionReader {

    public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
        ...
        // 这里注册了一堆关于处理 Configuration 注解的 PostProcessor,与本文相关的有如下几个:
        // 1. ConfigurationClassPostProcessor
        // 2. AutowiredAnnotationBeanPostProcessor
        // 3. CommonAnnotationBeanPostProcessor
        AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
    }
}

本文重点 ConfigurationClassPostProcessor 就是用于处理 JavaConfig 的后置处理器,

源码阅读任务:找到 ConfigurationClassPostProcessor 被注册的位置

invokeBeanFactoryPostProcessors

在 Spring 启动并执行 refresh 方法时,这些后置处理器就会被执行

public abstract class AbstractApplicationContext extends DefaultResourceLoader
        implements ConfigurableApplicationContext {

    @Override
    public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            ...
            try {
                ...
                // 执行 BeanFactoryPostProcessors,读取或编辑 BeanDefinition
                invokeBeanFactoryPostProcessors(beanFactory);
                ...
            }
        }
    }

    protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
        // Spring 执行 BeanFacotryPostProcessor 的位置
        PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
        ...
    }
}

invokeBeanFactoryPostProcessors:该方法是后置处理器运行的核心方法,在阅读源代码的时候会发现源码中提及到 BeanDefinitionRegistryPostProcessors

BeanDefinitionRegistryPostProcessors:接口是闲着可以动态的向 ApplicationContext 新增 BeanFactoryPostProcessor 或者 BeanDefinitionRegistryPostProcessor,而该接口本身也继承 BeanFactoryPostProcesso,也可以处理 BeanDefinition

源码阅读任务:阅读 PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors 方法,找到上面提到的 ConfigurationClassPostProcessor 在什么时候被执行

我也来概括一下这个方法的作用:

  • 先看 ApplicationContext 中有没有 BeanDefinitionRegistryPostProcessors,有的话按顺序执行一下产生更多的 BeanFactoryPostProcessor 和 BeanDefinitionRegistryPostProcessor,
  • 让新产生的 BeanDefinitionRegistryPostProcessors 重复第一第一、二步,直到所有BeanDefinitionRegistryPostProcessors 被处理
  • 让 BeanDefinitionRegistryPostProcessors 和 BeanFactoryPostProcessor 按顺序处理 BeanDefinition。

拓展阅读(可以不看): Spring中refresh分析之invokeBeanFactoryPostProcessors方法详解_程序员小默的博客-CSDN博客_invokebeanfactorypostprocessors

ConfigurationClassPostProcessor

该后置处理器用于处理 JavaConfig,当解析到配置类上的

  1. @ComponentScan:会将 basepackage 下的所有的带有 @Component(包括 Configuraion)的类变成 BeanDefinition 并注册到 ApplicationContext 中
  2. @Import:会解析出 Import 标签上的 Class 对象,分三种情况:

    1. Class 只是一个被 Configuration 标记的类,直接加载 Class 为 BeanDefinition
    2. Class 实现了 ImportSelector 接口,将 selectImports 返回的全限定类名数组全部加载,转换为 BeanDefinition
    3. Class 实现了 ImportBeanDefinitionRegistrar,不做处理,暂存这个 Class,等到 Class 被转换为 BeanDefinition 才会执行 ImportBeanDefinitionRegistrar 接口中的 registerBeanDefinitions 方法
  3. @Bean:产生 BeanMethod 并暂存
源码阅读任务:看靠 ConfigurationClassPostProcessor 是如何处理类上的配置注解

Spring Boot 自动装配

通过上面分析我们知道了 Spring Boot 启动时会将启动类作为 JavaConfig 注册到 ApplicationContext 中,且会作为主配置类被 ConfigurationClassPostPorcessor 处理

@SpringBootApplication
public class RenrenApplication {
    public static void main(String[] args) {
        SpringApplication.run(RenrenApplication.class, args);
    }
}

那现在我们就来扒一扒这个主配置类到底配置了什么

@SpringBootApplication

我们打开 @SpringBootApplication

...
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {...}
  • ComponentScan 认识把,你应该会在 ConfigurationClassPostProcessor上看过它是怎么被解析的(没看的赶紧去看),它的作用是扫描指定包(默认时当前 JavaConfig 所在的目录及内部),将所有带有 @Component 转换为 BeanDefnition

剩下两个不太认识,我们分别来看

  • SpringBootConfiguration:打开发现里面包含 @Configuration,有了这个注解才能被 @ConfigurationClassPostProcessor 解析
  • EnableAutoConfiguration:开启自动装备

@EnableAutoConfiguration

继续看

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {...}

先看上面那个不认识的,点看 @AutoConfigurationPackage

@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {...}

这里 import 了一个 ImportBeanDefinitionRegistrar 的实现,注册了一个 BasePackge 的 BeanDefinition,这个 BeanDefinition 生成的 Bean 用于存储自动配置包以供以后参考的类(例如,通过 JPA 实体扫描器)

再看认识的 @Import,这里采用的是 ImportSelector 的方式。其实这就是自动装配的关键。我们打开这个 ImportSelector,他的代码比较少,最值得注意的是【重点】

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
        ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        }
        AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
        return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }
    protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
        if (!isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        }
        // 获取 EnableAutoConfiguration 注解上的值,exclude 和 excludeName 都为空
        AnnotationAttributes attributes = getAttributes(annotationMetadata);
        // 【重点】获取所有配置类的全限定类名
        List configurations = getCandidateConfigurations(annotationMetadata, attributes);

        ...

        return new AutoConfigurationEntry(configurations, exclusions);
    }

    protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        // 【重点】SpringFactoriesLoader 可以加载指定 class 所在的包下的 spring.factory 文件内容
        List configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
                getBeanClassLoader());
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
                + "are using a custom packaging, make sure that file is correct.");
        return configurations;
    }
    protected Class getSpringFactoriesLoaderFactoryClass() {
        return EnableAutoConfiguration.class;
    }
}

这里调用了 SpringFactoriesLoader.loadFactoryNames 方法,入参为 EnableAutoConfiguration.class,点进去看看

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";


public static List loadFactoryNames(Class factoryType, @Nullable ClassLoader classLoader) {
    ClassLoader classLoaderToUse = classLoader;
    if (classLoaderToUse == null) {
        classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    }
    String factoryTypeName = factoryType.getName();
    return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

// 使用 ClassLoader 加载所有依赖中的 spring.factory
private static Map> loadSpringFactories(ClassLoader classLoader) {
    Map> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }

    result = new HashMap<>();
    try {
        Enumeration urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
        while (urls.hasMoreElements()) {
            // 在这里打个断点,url 为我们 pom 文件的依赖包下的 spring.factory
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);

            // 读取配置文件,result 中的 key 为工厂名称,value 为 Configuration 集合
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry entry : properties.entrySet()) {
                String factoryTypeName = ((String) entry.getKey()).trim();
                // 将 properties 中的 value 按逗号分割
                String[] factoryImplementationNames =
                        StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
                for (String factoryImplementationName : factoryImplementationNames) {
                    result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
                            .add(factoryImplementationName.trim());
                }
            }
        }
        result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
                    .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
        // 缓存
        cache.put(classLoader, result);
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
    return result;
} 

利用 ClassLoader 加载所有依赖下的 META-INF/spring.factories 文件,解析并缓存。我们可以随便打开一个依赖看靠,比如 Hutool 的 spring.factories

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.hutool.extra.spring.SpringUtil

AutoConfigurationImportSelector 会 Import 所有 spring.factories 中 Key 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 进行聚合,递交给 Spring 进行自动加载。

所以我们如果要开发 starter,也只需要将配置类写道 spring.factory 中就可以了,非常方便

总结

综上,自动转配的原理就有了:

  1. Spring Boot 在主配置类上加上了 @Configuration 的标记并将其注册到了 ApplicationContext
  2. Springt 的 ConfigurationClassPostProcessor 对主配置类上的注解进行解析,解析到了 @Import(AutoConfigurationImportSelector.class)
  3. Spring 会调用AutoConfigurationImportSelector 的 importSelector 方法,该方法返回一个需要被加载的类的全限定类名数组,Spring 会根据数组返回值加载这些类,而在自动装配的场景下,该方法的返回值来自于所以依赖的 META.INF/spring.factory 配置文件中 Key 为 EnableAutoConfiguration 的 Value 所组成的数组

以上就是本文的全部内容,希望对大家有所帮助。

你可能感兴趣的:(在 Spring Boot 中引入依赖后什么都不用配置,是真的爽!)