likes
comments
collection
share

【重写SpringFramework】配置类概述(chapter 3-5)在早期的 Spring 项目中,使用 XML

作者站长头像
站长
· 阅读数 44

1. 前言

在早期的 Spring 项目中,使用 XML 文件进行配置。如下所示,在 applicationContext.xml 配置文件中,使用 bean 标签注册组件,使用 context:component-scan 标签批量扫描组件,使用 import 标签引入其他的 XML 文件,使用 context:property-placeholder 标签加载配置文件,等等。此外,有的标签带有前缀,需要引入相应的名称空间,操作起来异常繁琐。鉴于此,Spring 提供了一套声明式的解决方案,以配置类为核心,为我们提供了更加方便快捷、灵活多样的配置应用的方式。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <-- 1. 注册组件 !-->
    <bean id="foo" class="cn.stimd.spring.Foo" >
        <property name="bar" value="bar"/>
    </bean>

    <bean id="bar" class="cn.stimd.spring.Bar" />

    <-- 2. 批量扫描 !-->
    <context:component-scan base-package="cn.stimd.spring" use-default-filters="true"/>

    <-- 3. 导入配置文件 !-->
    <import resource="other.xml"/>

    <-- 4. 加载属性文件 !-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
</beans>

Spring 将配置类分为 Full 模式与 Lite 模式,其中 Lite 模式表示轻量级,仅拥有配置类的部分功能,Full 模式为全量级,拥有配置类的全部功能。两者的区别如下:

  • Full 模式:声明了 @Configuration 注解的类
  • Lite 模式:类上声明了 @Component@ComponentScan@Import 等注解,或者方法上声明了 @Bean 注解,也可认为是配置类

为了简化代码实现,我们不区分轻量级与全量级的配置类,所讨论的配置类也仅以声明了 @Configuration 注解为标准。@Configuration 注解声明了元注解 @Component,因此配置类可以通过扫描的方式来加载。

2. 配置类组件

2.1 继承结构

Spring 为了处理配置类,提供了三个重要的组件,此外还使用 CongigurationClass 来描述已解析的配置类。简单介绍如下:

  • ConfigurationClassPostProcessor:寻找容器中已存在的配置类,并对其进行解析。
  • ConfigurationClassParser:负责具体的解析过程,将已解析的配置类封装成 CongigurationClass 对象。
  • ConfigurationClassBeanDefinitionReader:对 CongigurationClass 集合进行处理,将所有的组件以 BeanDefinition 的方式注册到容器中。

【重写SpringFramework】配置类概述(chapter 3-5)在早期的 Spring 项目中,使用 XML

2.2 ConfigurationClass

ConfigurationClass 用于描述一个独立的配置类,并以扁平化的方式管理所有的 BeanMethod。我们首先要明确独立和扁平化这两个概念。其一,当前配置类、内部配置类、导入配置类、组件扫描的配置类,都是独立的配置类。但配置类的父类除外,属于当前配置类的一部分。其二,扁平化是指配置类和父类可能都存在若干 BeanMethod,不管有多少继承层次,都会将所有的 BeanMethod 放在一个集合中管理。这也说明了配置类和父类是一体的。

ConfigurationClass 类定义了一些属性来描述配置类的信息,简单介绍如下:

  • metadata:表示配置类的元数据
  • importedBy:表示配置类是由谁导入的,对于内部类来说是外部类导入的。对于导入类来说,是由声明了 @Import 注解的类导入的,可能是外部类或内部类。
  • beanMethods:缓存了配置类及其父类所有的工厂方法(BeanMethod 相关,待实现)
  • importBeanDefinitionRegistrars:缓存了 ImportBeanDefinitionRegistrar 接口的实现类集合(导入相关,待实现)
public class ConfigurationClass {
    private final AnnotationMetadata metadata;
    private final Set<ConfigurationClass> importedBy = new LinkedHashSet<>(1);
    private final Set<BeanMethod> beanMethods = new LinkedHashSet<>();
    private final Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> importBeanDefinitionRegistrars = new LinkedHashMap<>();
}

3. ConfigurationClassPostProcessor

3.1 概述

ConfigurationClassPostProcessor 类实现了 BeanDefinitionRegistryPostProcessor 接口,这是用于处理 BeanFactory 的后处理器。beans 模块定义了该接口,当时没有用武之地,所以没有展开来讲。ConfigurationClassPostProcessor 的作用是解析配置类,并注册 BeanDefinition,其重要性不言而喻。

AnnotationConfigUtils 工具类注册 ConfigurationClassPostProcessor 作为默认的组件。这样一来,我们在创建 AnnotationConfigApplicationContext 实例的时候,自动拥有了处理配置类的能力。

public class AnnotationConfigUtils {
	public static final String CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME = "internalConfigurationAnnotationProcessor";

    public static void registerAnnotationConfigProcessors(BeanDefinitionRegistry registry) {

        //注册支持@Configuration注解的组件
        if(!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)){
            RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
            registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME);
        }

        //其余略
    }
}

3.2 加载流程

AnnotationConfigApplicationContext 的创建到配置类的处理,中间经过了哪些流程,我们结合时序图来简单分析一下。整个流程由以下步骤组成:

  1. AnnotationConfigApplicationContext 的构造函数中创建 AnnotatedBeanDefinitionReader 组件的实例
  2. 在构造 AnnotatedBeanDefinitionReader 的过程中,通过工具类 AnnotationConfigUtils 让容器注册一些组件
  3. 将解析 ConfigurationClassPostProcessor 注册到容器中
  4. 回到 AnnotationConfigApplicationContext 的构造函数,调用父类的 refresh 方法,刷新容器
  5. AbstractApplicationContext 在刷新的过程中,调用 invokeBeanFactoryPostProcessors 方法回调 BeanFactoryPostProcessor
  6. ConfigurationClassPostProcessorpostProcessBeanDefinitionRegistry 方法完成配置类的解析流程

【重写SpringFramework】配置类概述(chapter 3-5)在早期的 Spring 项目中,使用 XML

3.3 代码实现

postProcessBeanDefinitionRegistry 方法是处理配置类的入口,该方法定义了处理配置类的主流程,大体可以分为三步:

  1. 寻找容器中已存在的配置类(引导配置类)

  2. 解析引导配置类,加载所有的组件(包括其他配置类)

  3. 此时已经拿到了所有组件的元数据,包装成 BeanDefinition 注册到容器中

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws RuntimeException {

        //1. 寻找容器中已存在的配置类
        List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
        String[] names = registry.getBeanDefinitionNames();
        for (String name : names) {
            BeanDefinition definition = registry.getBeanDefinition(name);
            //检查是否声明了@Configuration等注解,如果是进一步区分full模式和lite模式
            if(ConfigurationClassUtils.checkConfigurationClassCandidate(definition, this.metadataReaderFactory)) {
                configCandidates.add(new BeanDefinitionHolder(definition, name));
            }
        }

        if(configCandidates.isEmpty()){
            return;
        }

        //2. 解析引导配置类,加载所有的配置类
        Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
        ConfigurationClassParser parser = new ConfigurationClassParser(metadataReaderFactory, environment, resourceLoader, registry);
        parser.parse(candidates);

        //3. 注册BeanDefinition
        if(this.reader == null){
            this.reader = new ConfigurationClassBeanDefinitionReader(registry, this.resourceLoader, this.environment);
        }
        this.reader.loadBeanDefinitions(parser.getConfigurationClasses());
    }
}

第二步和第三步有专门的组件来处理,逻辑比较复杂,下边单独介绍,我们先来看第一步。虽然第一步的逻辑并不复杂,关键问题是 ConfigurationClassPostProcessor 组件在 refresh 方法中的执行顺序相当靠前,几乎在 BeanFactory 准备完毕后就被调用,此时容器中不存在配置类。因此我们需要事先注册一个配置类,为了与后来加载的配置类区分开,我们把提前注册的配置类称为引导配置类。所谓引导(bootstrap)是指 Spring 以引导配置类为切入点,加载其他的配置类,从而完成对所有组件的加载。

一般情况下,引导配置类只有一个,主要通过 AnnotatedBeanDefinitionReader 来注册。这里也解决了我们一个疑惑,AnnotatedBeanDefinitionReader 只能注册一个 BeanDefinition,并没有比手动注册 BeanDefinition 方便多少,感觉有些鸡肋。现在我们知道,AnnotatedBeanDefinitionReader 的主要作用就是注册引导配置类,实际上 SpringBoot 的启动类就是如此处理的。

4. ConfigurationClassParser

4.1 配置类解析

在拿到引导配置类的集合之后,交给 ConfigurationClassParser 组件来解析。parse 方法是解析配置类的入口,由于 BeanDefinition 有多种加载方式,需要区分不同的情况,因此需要调用不同的 parse 重载方法,但最终都会调用 processConfigurationClass 方法。我们先不考虑具体的解析流程是如何处理的,经过处理之后,configurationClasses 字段保存了所有已解析的配置类。

class ConfigurationClassParser {
    private final MetadataReaderFactory metadataReaderFactory;
    //已解析的配置类集合
    private final Set<ConfigurationClass> configurationClasses = new LinkedHashSet<>();

    /**
     * 解析引导配置类,唯一入口方法
     * @param configCandidates  引导配置类集合
     */
    public void parse(Set<BeanDefinitionHolder> configCandidates) {
        for (BeanDefinitionHolder holder : configCandidates) {
            BeanDefinition bd = holder.getBeanDefinition();
            //1. 通过注解声明的方式创建,根据注解元数据(AnnotationMetadata)来加载
            if (bd instanceof AnnotatedBeanDefinition) {
                parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
            }
            //2. 通过编程的方式创建,比如RootBeanDefinition,根据class属性来加载
            else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
                parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
            }
            //3. 其余情况,BeanDefinition的class属性未确定,根据className来加载
            else{
                parse(bd.getBeanClassName(), holder.getBeanName());
            }
        }
        //处理延迟导入(TODO 先略过,详见Import一节)
    }

    //注解元数据已知,可能是反射或ASM的方式
    protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
        processConfigurationClass(new ConfigurationClass(metadata, beanName));
    }

    //Class信息已知,通过反射的方式解析配置类
    protected final void parse(Class<?> clazz, String beanName) throws IOException {
        processConfigurationClass(new ConfigurationClass(clazz, beanName));
    }

    //类名已知,通过ASM的方式解析配置类
    protected final void parse(String className, String beanName) throws IOException {
        MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className);
        processConfigurationClass(new ConfigurationClass(reader.getAnnotationMetadata(), beanName));
    }
}

4.2 processConfigurationClass 方法

processConfigurationClass 方法起到了辅助的作用,首先判断是否应该处理配置类,这一功能与条件判定有关,暂时先跳过。然后以循环的方式处理配置类及其父类,当一个配置类处理完毕,会被加入到缓存中保存起来。在 doProcessConfigurationClass 方法的执行过程中,可能需要解析新的配置类,则以递归的方式调用 processConfigurationClass 方法,也就是说该方法可能多次执行,直到所有的配置类都被处理。

/**
* 对指定的配置类进行解析
* @param configClass 表示一个配置类,有多种来源,可以是引导配置类、内部配置类、组件扫描的配置类、导入的配置类
*/
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
    //根据@Conditional判断是否应该处理配置类(TODO 先略过,详见条件判定一节)

    //遍历配置类及其父类
    SourceClass sourceClass = asSourceClass(configClass);
    do{
        sourceClass = doProcessConfigurationClass(configClass, sourceClass);
    }
    while (sourceClass != null);

    //将已解析的配置类添加到集合中
    this.configurationClasses.add(configClass);
}

doProcessConfigurationClass 方法是解析配置类的核心方法,实现了大量功能,逻辑也比较复杂,我们将花费大量篇幅对该方法进行解读。配置类的解析主要有六种情况需要处理,如下所示:

  • 内部类:解析声明了 @Configuration 注解的内部类
  • 属性文件:如果配置类声明了 @PropertySource 注解,则加载指定的属性文件
  • 组件扫描:如果配置类声明了 @ComponentScan 注解,则通过扫描包的方式加载 Bean
  • 导入:如果配置类声明了 @Import 注解,则导入相应的组件
  • 工厂方法:如果方法声明了 @Bean 注解,则以工厂方法的方式加载 Bean
  • 父类:如果配置类继承了父类,则尝试解析父类
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
    //1. 处理内部类
    //2. 处理配置文件
    //3. 组件扫描
    //4. 处理导入
    //5. 处理工厂方法
    //6. 处理父类
}

4.3 SourceClass

SourceClassConfigurationClassParser 的内部类,作用是以统一地方式来描述配置类。所谓的统一有两层含义,一是来源不同,可以是当前配置类、内部配置类、配置类的父类、导入的配置类,组件扫描加载的配置类等;二是加载方式的不同,可能是基于反射或 ASM 机制加载的。

  • source 属性表示配置类对应的资源,从构造方法可以看出 source 有两种类型,Class 表示通过反射加载的,MetadataReader 表示通过 ASM 加载的
  • metadata 属性表示配置类的相关信息,ConfigurationClass 也有这个属性
private class SourceClass {
    private final Object source;
    private final AnnotationMetadata metadata;

    public SourceClass(Object source) {
        this.source = source;
        if (source instanceof Class) {
            this.metadata = new StandardAnnotationMetadata((Class<?>) source, true);
        }
        else {
            this.metadata = ((MetadataReader) source).getAnnotationMetadata();
        }
    }
}

SourceClass 还有一些很有用的方法,列举如下。可以发现,这些方法都有一个特点,即返回的类型都是 SourceClass,这一点也印证了 SourceClass 的确是将各种形式的配置类给统一了起来。

  • Set<SourceClass> getAnnotations() : 获取配置类上的所有注解

  • Collection<SourceClass> getAnnotationAttributes(String annType, String attribute):获取指定注解上的指定属性值,比如 @Import 注解的 value 属性

  • SourceClass getSuperClass():获取父类

  • Collection<SourceClass> getMemberClasses():获取所有的内部类

5. ConfigurationClassBeanDefinitionReader

经过对引导配置类的解析,所有组件都加载完毕,接下来以 BeanDefinition 的形式注册到容器中,这一工作是由 ConfigurationClassBeanDefinitionReader 完成的。loadBeanDefinitions方法对传入的 ConfigurationClass 集合进行遍历,然后交由 loadBeanDefinitionsForConfigurationClass 方法处理。

//加载所有的BeanDefinition
public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
    for (ConfigurationClass configClass : configurationModel) {
        loadBeanDefinitionsForConfigurationClass(configClass);
    }
}

loadBeanDefinitionsForConfigurationClass 方法一共处理了三种情况,分别是配置类、工厂方法、导入类的注册。工厂方法和导入类的处理之后再讲,我们先来看配置类是如何注册的。首先需要判断当前配置类是不是「导入的」,我们在讲解 ConfigurationClass 类时说过,内部类和通过 @Import 注解加载的配置类是导入的,也就是说需要注册的是这两种配置类。下面列出了各种配置类的特点,有助于形成较为全面的认识:

  • 引导配置类:非导入的,且已经注册过

  • 通过组件扫描加载的配置类:非导入的,且已经注册过

  • 内部配置类:是通过外部类导入的,尚未注册(本节实现)

  • 导入配置类:通过 @Import 注解导入的,尚未注册(待实现)

//从ConfigurationClass中加载BeanDefinition
private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass) {
    //1. 注册配置类
    if(configClass.isImported()){
        registerBeanDefinitionForImportedConfigurationClass(configClass);
    }

    //2. 注册BeanMethod(TODO)
    //3. 注册导入的类(TODO)
}

//注册导入的配置类(包括内部配置类)
private void registerBeanDefinitionForImportedConfigurationClass(ConfigurationClass configClass) {
    AnnotationMetadata metadata = configClass.getMetadata();
    AnnotatedGenericBeanDefinition configBeanDef = new AnnotatedGenericBeanDefinition(metadata);
    AnnotationConfigUtils.processCommonDefinitionAnnotations(configBeanDef, metadata);
    String configBeanName = this.beanNameGenerator.generateBeanName(configBeanDef);
    configClass.setBeanName(configBeanName);
    this.registry.registerBeanDefinition(configBeanName, configBeanDef);
}

6. 讲解思路

6.1 概述

在一个典型的配置类中,上述六种情况可能同时存在。在实际应用中,Spring Boot 中的 WebMvcAutoConfiguration 涵盖了其中的四种情况。在示例代码中,精简了 WebMvcAutoConfiguration 的实现。此外,第五种和第六种情况在源码中并不存在,这里仅仅是为了模拟完整的使用场景。

//5) 组件扫描(源码无,仅展示)
@ComponentScan
//6) 加载属性文件(源码无,仅展示)
@PropertySource("application.properties")
@Configuration
public class WebMvcAutoConfiguration {

    //1) 通过工厂方法的方式Bean
    @Bean
    @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
    public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
        return new OrderedHiddenHttpMethodFilter();
    }

    //2) 内部类处理,WebMvcAutoConfigurationAdapter
    //3)父类处理,WebMvcConfigurerAdapter
    //4) 使用导入的方式加载配置类
    @Configuration
    @Import(EnableWebMvcConfiguration.class)
    public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter {}
}

根据功能的不同,六种情况还可以分为三类。第一,内部类和父类都属于配置类的结构,不涉及具体的解析过程。第二,属性文件是用于处理配置信息的,与加载组件无关。第三,组件扫描、导入、工厂方法都是加载组件的,是整个解析过程的关键所在。我们将根据从易到难的顺序对各项功能进行介绍,而不是按照代码中原有的顺序,本节先来看最简单的内部类和父类的处理。

【重写SpringFramework】配置类概述(chapter 3-5)在早期的 Spring 项目中,使用 XML

6.2 内部类处理

在处理内部类时,首先调用 ConfigurationClassgetMemberClass 方法,获取所有的内部类。然后判断内部类是不是一个配置类,如果是则递归调用 processConfigurationClass 方法,进入解析内部配置类的流程。从这里可以看出,内部配置类是作为单独的配置类进行解析的。

//所属类[cn.stimd.spring.context.annotation.ConfigurationClassParser]
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
    //1. 处理内部类
    processMemberClasses(configClass, sourceClass);
    ......
}

//step-1 处理内部类
private void processMemberClasses(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
    for (SourceClass memberClass : sourceClass.getMemberClasses()) {
        if(ConfigurationClassUtils.isConfigurationCandidate(memberClass.getMetadata())){
            //将内部类转换成ConfigurationClass
            processConfigurationClass(memberClass.asConfigClass(configClass));
        }
    }
}

6.3 父类处理

父类的处理是在整个解析流程的末尾,优先级最低。首先检查配置类的全类名是否以 java 开头,由于以 java 开头的包名是 JDK 自用的,是受保护的,因此非 java 开头的全类名代表是用户自定义的类。也就是说,如果父类存在且是自定义的类则返回,由外层方法进行递归处理,即再次执行 doProcessConfigurationClass 方法的逻辑。从这里可以看出,父类是作为当前配置类的一部分存在的,它不是一个单独的配置类。由于父类的低优先级,其作用是提供一些兜底的配置信息,这对框架来说有一定的意义。

//所属类[cn.stimd.spring.context.annotation.ConfigurationClassParser]
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
    //1. 处理内部类(略)
    //6. 处理父类
    if(sourceClass.getMetadata().hasSuperClass()){
        String superClass = sourceClass.getMetadata().getSuperClassName();
        if(!superClass.startsWith("java")){
            return sourceClass.getSuperClass();
        }
    }
    return null;
}

7. 测试

首先定义了一个配置类 OuterConfig 作为外部类,其内部类 InnerConfig 也声明了 @Configuration 注解,因此这是一个内部配置类。

//测试类:表示一个独立的配置类
@Configuration
public class OuterConfig extends AbstractConfig {

    @Configuration
    static class InnerConfig {
        public InnerConfig() {
            System.out.println("我是配置类的内部类...");
        }
    }
}

其次,OuterConfig 还有一个父类,需要注意的是,父类 AbstractConfig 并没有声明 Configuration 注解,说明它与子类是一体的。为了说明父类确实被解析了,我们在父类中也声明了一个内部类 InnerConfig2

//测试类:配置类的父类
public abstract class AbstractConfig {

    @Configuration
    static class InnerConfig2 {
        public InnerConfig2() {
            System.out.println("我是父类的内部类2...");
        }
    }
}

本次测试有两个目的,一是验证核心组件 ConfigurationClassPostProcessor 正常执行,二是配置类的内部类和父类确实被解析。测试方法的逻辑很简单,首先将配置类 OuterConfig 注册到 Spring 容器中,然后刷新容器,自动执行配置类的解析操作。

//测试方法
@Test
public void testInnerAndSuperClass() {
    AnnotationConfigApplicationContext context =  new AnnotationConfigApplicationContext();
    context.register(OuterConfig.class);	//注册引导配置类
    context.refresh();
}

从测试结果可以看到,内部类的解析是优先执行的,然后是对父类的解析。

我是配置类的内部类...
我是父类的内部类2...

8. 总结

本节初步讨论了配置类,典型的配置类使用 @Configuration 注解来标识,具体的解析过程由三个组件类完成。

  • ConfigurationClassPostProcessor 是整个流程的入口,寻找容器中已存在的配置类,并交给 ConfigurationClassParser 组件解析。
  • ConfigurationClassParser 负责具体的解析逻辑,将已解析的配置类包装成 ConfigurationClass 对象。
  • ConfigurationClassBeanDefinitionReader 负责处理 ConfigurationClass 集合,提取出 BeanDefnition 并注册到 Spring 容器中。

【重写SpringFramework】配置类概述(chapter 3-5)在早期的 Spring 项目中,使用 XML

配置类解析的过程最为繁杂,为了便于讲解,我们将主要功能分为六种,分别是内部类、父类、属性文件、组件扫描、工厂方法、导入。内部类和父类实际上是配置类的结构,并不涉及实际的解析逻辑,因此代码实现也是最简单的。本节通过对内部类和父类的处理,初步实现了对配置类的解析。

9. 项目结构

新增修改一览,新增(9),修改(1)。

context
└─ src
   ├─ main
   │  └─ java
   │     └─ cn.stimd.spring.context
   │        └─ annotation
   │           ├─ AnnotationConfigUtils.java (*)
   │           ├─ Configuration.java (+)
   │           ├─ ConfigurationClass.java (+)
   │           ├─ ConfigurationClassBeanDefinitionReader.java (+)
   │           ├─ ConfigurationClassParser.java (+)
   │           ├─ ConfigurationClassPostProcessor.java (+)
   │           └─ ConfigurationClassUtils.java (+)
   └─ test
      └─ java
         └─ context
            └─ config
               ├─ AbstractConfig.java (+)
               ├─ ConfigTest.java (+)
               └─ OuterConfig.java (+)

注:+号表示新增、*表示修改

注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。


欢迎关注公众号【Java编程探微】,加群一起讨论。

原创不易,觉得内容不错请关注、点赞、收藏。

转载自:https://juejin.cn/post/7402968643950329883
评论
请登录