SpringBoot 源码解析 —— 自动装配的奥秘(DeferredImportSelector)


title: SpringBoot 源码解析 —— 自动装配的奥秘(DeferredImportSelector)
date: 2021/01/15 09:22
remark: SpringBoot 版本为 2.2.6, Spring 版本为 5.2.5


简介

SpringBoot 的自动装配与 @Configuration 注解的处理类 ConfigurationClassPostProcessor 息息相关,所以建议先看下这篇文章再继续向下看。

image

上图中的第 576-578 行就是今天我们要将的重点:DeferredImportSelector。

继续将之前,我们先了解一下 spring.factories:

spring.factories

spring.factories 是 Spring 仿造 Java SPI 实现的一种类加载机制。它在 META-INF/spring.factories 文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化。这种自定义的SPI机制是 Spring Boot Starter 实现的基础。

spring-core 包里定义了 SpringFactoriesLoader 类,这个类实现了检索META-INF/ spring.factories 文件,并获取指定接口的配置的功能。在这个类中定义了两个对外的方法:

  • loadFactories 根据接口类获取其实现类的实例,这个方法返回的是对象列表。
  • loadFactoryNames 根据接口获取其接口类的名称,这个方法返回的是类名的列表。

上面两个方法的关键都是从指定的 ClassLoader 中获取 spring.factories 文件,并解析得到类名列表,具体代码如下:

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }

    try {
        Enumeration<URL> urls = (classLoader != null ?
                classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        result = new LinkedMultiValueMap<>();
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                String factoryClassName = ((String) entry.getKey()).trim();
                for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                    result.add(factoryClassName, factoryName.trim());
                }
            }
        }
        cache.put(classLoader, result);
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

从代码中可以看到,在这个方法中会遍历类路径下的所有 Jar 包中的 spring.factories 文件。spring.factories 是通过 Properties 解析得到的,所以我们在写文件中的内容都是按照下面这种方式配置的,如果一个接口希望配置多个实现类,可以用","分割。

com.xxx.interface=com.xxx.classname

在日常工作中,我们可能需要实现一些SDK 或者Sring boot starter 给别人用的时候,我们就可以使用Factories机制,但是 Factories 机制可以让 SDK 或者 Stater 的使用只需要很少或者不需要进行配置,只需要在服务中引入我们的 Jar 包即可。

image

spring-autoconfigure-metadata.properties 文件示例:

# 配合上图中的 OnClassCondition 使用,表示引入 RabbitAutoConfiguration 配置类需要在类路径中存在 Channel 和 RabbitTemplate。
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration.ConditionalOnClass=com.rabbitmq.client.Channel,org.springframework.amqp.rabbit.core.RabbitTemplate

为什么不直接在配置类上用 @Condition 注解来实现呢?

官方说,这样可以增加效率。

更多可以参考这篇文章:Springboot自动装配之spring-autoconfigure-metadata.properties和spring.factories

开始吧 @SpringBootApplication

本部分测试demo,虽然没啥东西。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration    // 其实就是 @Configuration,只不过把它的 proxyBeanMethods 属性的默认值改成了 false
@EnableAutoConfiguration    // 重点
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

@EnableAutoConfiguration

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage   // 他也 Import 一个类,不过不是本文重点
@Import(AutoConfigurationImportSelector.class)  // 重点
public @interface EnableAutoConfiguration {

AutoConfigurationImportSelector

image

我们看到了他继承了 DeferredImportSelector,好,我们回到 @Configuration 的处理类 ConfigurationClassPostProcessor 中。

image

看下 deferredImportSelectorHandler#handle()

image

那么后面是在哪里调用的这些“延迟的” ImportSelector 的呢?

image

看下 deferredImportSelectorHandler#process()

image

tag1 handler::register

class ConfigurationClassParser {
    ...

    private class DeferredImportSelectorGroupingHandler {
        // key:组类型(在这里 AutoConfigurationGroup) value:组
        private final Map<Object, DeferredImportSelectorGrouping> groupings = new LinkedHashMap<>();
        // key:配置类的注解属性 value:配置类信息(在这里是 AppBootstrap 的信息)
        private final Map<AnnotationMetadata, ConfigurationClass> configurationClasses = new HashMap<>();
        //注册分组
        public void register(DeferredImportSelectorHolder deferredImport) {
            Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup(); // 这个方法有默认(default)实现,返回的是 null

            /*
            创建组
            1. 其中 createGroup(group) 就是创建了上面的 group 对象,如果为空,则创建一个默认的组对象 DefaultDeferredImportSelectorGroup。
            2. 这个方法的意思是,如果 map 中没有这个元素则用后面的方法创建,如果有则直接取出来
            */
            DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent(
                    (group != null ? group : deferredImport),
                    key -> new DeferredImportSelectorGrouping(createGroup(group)));
            grouping.add(deferredImport);//创建一个组,并加入DeferredImportSelectorHolder
            this.configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(),
                    deferredImport.getConfigurationClass());//将注解属性和ConfigurationClass映射
        }
image

这里一层套一层的可能看着有点乱,看下面的这张图应该能捋清楚一点:

image

tag2 handler.processGroupImports()

image

先看下 entry 是啥吧:

class Entry {
    // 引入当前类的配置类元数据,这里就是 appBootstrap 的注解元数据
    private final AnnotationMetadata metadata;

    // 引入类的全类名,例如:org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration
    private final String importClassName;
image
image

tag 到重点了 AutoConfigurationImportSelector#getAutoConfigurationEntry()

image
image
image
image
image

看下 ThreadedOutcomesResolver 吧:

image

最后看图过一下整个的流程吧

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

推荐阅读更多精彩内容