SpringBoot 之 EnableAutoConfiguration 实践和源码学习

EnableAutoConfiguration 是SpringBoot的Enable系列中一个比较基础的的功能模块,现在我们就来学习如何使用,以及分析源码学习其工作原理

EnableAutoConfiguration 从名字也可以很容易看出来其功能是能够自动装配配置,在SpringBoot中如果需要为其他人提供SDK等接口使用,使用方本身必须实例化接口类才可调用,如果每一个使用方都单独去实例化该接口,必然导致使用成本的增加,EnableAutoConfiguration就能很好的解决这个问题,使用方通过这个就可以直接使用,避免额外操作。

1、EnableAutoConfiguration 源码学习

先提个问题,如果现在只能使用Spring Framework,该如何实现类似的功能呢?
或许能想到的只有BPP,BeanPostProcessor或者BeanFactoryPostProcessor,只是他们处理的范畴不一样,BeanPostProcessor更多的是处理单个bean,而BeanFactoryPostProcessor是处理context上下文的

Spring包含了多种类型的BPP,在spring的生命周期的多个位置提供了对外的钩子便于扩展更多功能,关于BPP可以看看BPP的内容
在官方文档中,对BeanFactoryPostProcessor方法的简述也说的非常清楚,Modify the application context's internal bean factory after its standard initialization. All bean definitions will have been loaded, but no beans will have been instantiated yet. This allows for overriding or adding properties even to eager-initializing beans.

事实上,SpringBoot也确实是这样干的,ConfigurationClassPostProcessor 就是实例化了一个BeanDefinitionRegistryPostProcessor,从而拥有了修改context上下文的bean信息以及注册bean的功能

如下图由Spring的BPP处理器调用到ConfigurationClassPostProcessor,然后来到了AutoConfigurationImportSelector 类中

image

1.1、ConfigurationClassPostProcessor 处理

先了解下ConfigurationClassPostProcessor 这个BPP是如何被注入到spring容器的

springboot启动学习笔记中 已经介绍了spring的context上下文创建是由context = createApplicationContext(); 实现的,深入该代码直到AnnotationConfigUtils 类可以发现其操作是如果没发现org.springframework.context.annotation.internalCommonAnnotationProcessor这个bean的name,那就添加一个ConfigurationClassPostProcessor bean,具体如下图

image

这样我们就清楚了ConfigurationClassPostProcessor 这个类是如何装载进spring容器中的,接下来就是ConfigurationClassPostProcessor这个类具体的调用操作了

ConfigurationClassPostProcessor 类

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
    List<BeanDefinitionHolder> configCandidates = new ArrayList<BeanDefinitionHolder>();
    String[] candidateNames = registry.getBeanDefinitionNames();

    for (String beanName : candidateNames) {
           // 遍历spring容器所有的beanName信息,此时还未装载业务bean,
           // 只有spring&springboot本身框架层次需要的一些特定bean存在(特别注意包含主启动类)
           // 这点在之前的关于springboot的启动学习笔记中已经介绍了
        BeanDefinition beanDef = registry.getBeanDefinition(beanName);
        if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
                ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
                // 确认该beandefinition是否存在org.springframework.context.annotation.ConfigurationClassPostProcessor.configurationClass 键值对信息
                // 如果存在则假定是已经经过配置类处理过了
            if (logger.isDebugEnabled()) {
                logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
            }
        }
        else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
               // 否则检查一下当前的beandefinition是否符合config 配置类
              // 具体实现原理就是获取到bean的注解信息,然后查看是否存在 @Configuration 注解类或者 @Bean 注解
              // 如果有,则返回true
              // 当然在这里只会有主启动类才包含了@Configuration 的信息
            configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
        }
    }

    // Return immediately if no @Configuration classes were found
    if (configCandidates.isEmpty()) {
        return;
    }
    // 一般情况下,到这里只会有主启动类一个configCandidates信息存在

    // Sort by previously determined @Order value, if applicable
    Collections.sort(configCandidates, new Comparator<BeanDefinitionHolder>() {
        @Override
        public int compare(BeanDefinitionHolder bd1, BeanDefinitionHolder bd2) {
            int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
            int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
            return (i1 < i2) ? -1 : (i1 > i2) ? 1 : 0;
        }
    });

    // Detect any custom bean name generation strategy supplied through the enclosing application context
    SingletonBeanRegistry sbr = null;
    if (registry instanceof SingletonBeanRegistry) {
        sbr = (SingletonBeanRegistry) registry;
        if (!this.localBeanNameGeneratorSet && sbr.containsSingleton(CONFIGURATION_BEAN_NAME_GENERATOR)) {
             // 如果当前类不包含beanName 生成器 同时 context包含了单一的beanName生成器
             // 设置当前bean的生成器信息
            BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
            this.componentScanBeanNameGenerator = generator;
            this.importBeanNameGenerator = generator;
        }
    }

    // 生成配置解析类parses,开始解析每一个包含了@Configuration 的类
    ConfigurationClassParser parser = new ConfigurationClassParser(
            this.metadataReaderFactory, this.problemReporter, this.environment,
            this.resourceLoader, this.componentScanBeanNameGenerator, registry);

    Set<BeanDefinitionHolder> candidates = new LinkedHashSet<BeanDefinitionHolder>(configCandidates);
    Set<ConfigurationClass> alreadyParsed = new HashSet<ConfigurationClass>(configCandidates.size());
    do {
        parser.parse(candidates);
        // 关键的地方来了,这里就会去解析真正包含了@Configuration 的类
        parser.validate();

        Set<ConfigurationClass> configClasses = new LinkedHashSet<ConfigurationClass>(parser.getConfigurationClasses());
        // 所有通过@Configuration 装载进来的类集合
        configClasses.removeAll(alreadyParsed);
        // 已经装载的就移除掉

        // Read the model and create bean definitions based on its content
        if (this.reader == null) {
            this.reader = new ConfigurationClassBeanDefinitionReader(
                    registry, this.sourceExtractor, this.resourceLoader, this.environment,
                    this.importBeanNameGenerator, parser.getImportRegistry());
        }
        // reader是配置类装载类beandefinition
        this.reader.loadBeanDefinitions(configClasses);
        // 装载到Spring容器中,包含了那些使用@Bean的类信息
        alreadyParsed.addAll(configClasses);
            ......
            // 到这里就可以认为@Configuration 的导入基本完成了
}

真正进入到@Configuration 解析的入口处代码

ConfigurationClassParser 类

image

在经过processDeferredImportSelectors方法调用的之前,已经经过了parse处理,明确了@Configuration 包含的ImportSelectors 信息
如果需要自定义该注解则一定也要实现利用@Import注解导入的ImportSelector 实现类

private void processDeferredImportSelectors() {
    List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
    this.deferredImportSelectors = null;
    Collections.sort(deferredImports, DEFERRED_IMPORT_COMPARATOR);
    // 对获取的DeferredImportSelectorHolder 排序后进行遍历操作

    for (DeferredImportSelectorHolder deferredImport : deferredImports) {
        ConfigurationClass configClass = deferredImport.getConfigurationClass();
        // configClass 就是主启动类
        try {
            String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata());
            // 所有的需要导入的import类的selectImports方法执行,
            // 这个里面就是EnableAutoConfigurationImportSelector 
            // 具体的EnableAutoConfigurationImportSelector里面的selectImports后面说
            processImports(configClass, asSourceClass(configClass), asSourceClasses(imports), false);
            // 把获取的配置类信息进一步迭代处理,因为存在在类中包含了配置类的情况
            // 不过需要注意,这时候并未往spring容器中注入
        }
        catch (BeanDefinitionStoreException ex) {
            throw ex;
        }
        catch (Throwable ex) {
            throw new BeanDefinitionStoreException(
                    "Failed to process import candidates for configuration class [" +
                    configClass.getMetadata().getClassName() + "]", ex);
        }
    }
}

1.2、EnableAutoConfigurationImportSelector 的 selectImports 执行

来到AutoConfigurationImportSelector类

public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
          // 如果注解原信息未激活,则不可用,直接返回空
        return NO_IMPORTS;
    }
    try {
        AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
                .loadMetadata(this.beanClassLoader);
        AnnotationAttributes attributes = getAttributes(annotationMetadata);
        List<String> configurations = getCandidateConfigurations(annotationMetadata,
                attributes);
        // 利用SpringFactoriesLoader 获取系统中所有的META-INF/spring.factories 的 EnableAutoConfiguration 的键值对信息,其中就包含了上面我们自定义的类信息
        configurations = removeDuplicates(configurations);
        // 存在多处地方可能注册了相同的类信息,去重处理
        configurations = sort(configurations, autoConfigurationMetadata);
        Set<String> exclusions = getExclusions(annotationMetadata, attributes);
        // 匹配出包含exclude、excludeName 的列表信息,后续移除该config
        checkExcludedClasses(configurations, exclusions);
        configurations.removeAll(exclusions);
        configurations = filter(configurations, autoConfigurationMetadata);
        // 总之经过各种操作,最后产出了可用的配置类列表
        fireAutoConfigurationImportEvents(configurations, exclusions);
        // 配置导入的事件触发
        return configurations.toArray(new String[configurations.size()]);
        // 返回最后的配置类列表
    }
    catch (IOException ex) {
        throw new IllegalStateException(ex);
    }
}

1.3、总结

到这里对EnableAutoConfiguration的源码学习就算是结束了,利用Spring对外的BeanFactoryPostProcessor的钩子ConfigurationClassPostProcessor去解析出@Import引入的类,然后解析出所有被@Configuration的对象,然后注册到spring容器中,这样就完成了整个的自动装载过程

2、DEMO

image

如上图,可以发现在资源根目录下存放了一个"META-INF/spring.factories"的文件,里面的内容也很简单

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.demo.boot.api.CustomAutoConfiguration

CustomAutoConfiguration 类代码

@Configuration
public class CustomAutoConfiguration {

    @Bean
    public ApiStudent apiStudent() {
        ApiStudent apiStudent = new ApiStudent();
        // ApiStudent 只有一个name字段的基础POJO
        apiStudent.setName("auto-config");
        return apiStudent;
    }

}

非常简单的一个注册bean到spring的样例

现在我们在另一个服务中引入该服务,然后直接通过@resource 注解就可以引用到ApiStudent这个bean了

@Resource
private ApiStudent apiStudent;

@GetMapping("/custom-autoconfig")
@ResponseBody
public String autoConfig() {
    return apiStudent.getName();
}
image

可以看出网页上输出的内容就是起初在CustomAutoConfiguration中为apiStudent这个bean设置的属性值

到此demo就结束了,非常的基础的一个样例,在实际应用中也是如此,也使用的非常频繁。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容