Spring Boot源码阅读——spring.factories的加载机制

news2024/9/21 16:26:00

Spring Boot源码阅读——spring.factories的加载
提到 SpringBoot 的自动装配,不管是文章还是视频,都会提到 spring.factories 这个文件,这篇文章就来简单讲讲 spring.factories 的作用,以及它是怎么被加载的

简介
位置
以 SpringBoot 本体为例,spring.factories 在 jar 包的 META-INF 目录下
在这里插入图片描述
其他第三方库例如 mybatis-spring-boot-starter 等也都遵循这个规则
在这里插入图片描述
之所以要放在这个目录下,是 SpringBoot 提前约定好的,在 SpringFactoriesLoader 中有这样一个常量,项目启动时,扫描的就是这个路径:

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

类型
.factories 是 SpringBoot 专属的类型,但从文件的格式来看,其实就是一个 .properties 文件

实际上 SpringBoot 在读取解析 spring.factories 文件时,用的就是 properties 的解析器

内容
截取一部分如下

org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener

反斜杠 \ 可以换行,并且新的一行依旧属于上面那行

所以上面截取的这部分信息,实际只有两条数据

等号 = 左边的 key 是接口(也可以是注解)的全限定名,等号 = 右边的 value 是以逗号分隔的实现类的全限定名

作用
先讲讲 SpringBoot 自己的 spring.factories,其中定义了各种接口的实现类,在 SpringBoot 运行的某个过程中,会需要用到一些接口的功能,这时候就会从 spring.factories 中获取对应的实现类并实例化来进行调用

好处是极大地降低了耦合度

当版本升级的时候,只要修改 spring.factories 中的内容,而不用修改代码,就可以替换实现类

举个例子,SpringBoot 的生命周期中有监听器的参与,同一阶段,可能会触发好几个监听器的事件,而将来如果某个版本的 SpringBoot 要对“启动完成”这个阶段添加一个监听器,并作出一些处理,那么只需要写好这个新的监听器,然后加入 spring.factories 即可,不用修改 SpringBoot 原本的代码,并且原本的代码会从 spring.factories 读取到这个新的监听器并进行事件传播

当然,这是只针对 SpringBoot 自身而言的作用

对用户,或者第三方插件提供商,它的作用类似于 Java 的 SPI 机制

SPI 的全名是 Service Provider Interface,大概意思是,JDK 只提供某个模块的接口规范,而具体实现由厂商来完成

耳熟能详的有 JDBC 模块、日志模块等等

以 JDBC 为例,初学 JDBC 时肯定很熟悉一行代码:

Class.forName("com.mysql.cj.jdbc.Driver");

我们使用厂商提供的 JDBC 实现,都需要手动去注册并实例化

而 spring.factories 相比于 SPI,还提供了服务发现的机制,那就是我们只需要导入第三方库的 starter,SpringBoot 就能自动扫描并且帮助我们注册为 bean,相信只要熟悉 SpringBoot,就不用我多讲了

那么作为第三方库的开发者需要做些什么呢

除了在 META-INF 目录下留存一个 spring.factories 文件外,在文件内容中,还要加上其自身的自动配置类

以 mybatis 为例:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

key 是 SpringBoot 的 @EnableAutoConfiguration 注解,这个注解就是自动装配的关键,这篇文章的内容注重于 spring.factories 的加载,关于自动装配将写在下一篇文章

加载
从启动类的 run 方法进入,我们第一次见到与 factories 相关的代码是在 SpringApplication 的构造方法

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // 下面三行都调用了 getSpringFactoriesInstances 方法,从方法名可以大致理解到作用是获取 factories 文件中某个接口的实现类
    this.bootstrapRegistryInitializers = new ArrayList<>(
        getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = deduceMainApplicationClass();
}
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = getClassLoader();
    // loadFactoryNames 方法获取 spring.factories 定义的对应接口的所有实现类的全限定名
    Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    // 根据全限定名,和参数类型,实例化上面获取到的那些类
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    // 根据 @Order 注解排序
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

SpringFactoriesLoader#loadFactoryNames

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    ClassLoader classLoaderToUse = classLoader;
    if (classLoaderToUse == null) {
        classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    }
    String factoryTypeName = factoryType.getName();
    // loadSpringFactories 是 spring.factories 加载的核心方法
    // 返回值是一个 Map,包含 spring.factories 所有的键值对,key 是接口名,value 是实现类的名字数组
    // 这里获取到所有键值对后,根据需要的接口名称,获取到相应的实现数组
    return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

SpringFactoriesLoader#loadSpringFactories

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    // cache 是这个类的静态变量,所以是唯一的
    Map<String, List<String>> result = cache.get(classLoader);
    // 如果缓存中已经有了,那么直接返回
    if (result != null) {
        return result;
    }

    result = new HashMap<>();
    try {
        // 使用 classLoader 从 classpath 读取文件
        // 这个地址常量在上面已经贴出来过了,也就是 META-INF/spring.factories
        // 如果引用了第三方库,那么就会获取到很多 url
        Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
        // 所以要循环遍历每一个库里的 spring.factories 文件
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            // 用 properties 解析器读取文件
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            // 下面的内容就是把文件中的每一行数据,变成 result 数据,也就是 key(文件#String) 变成 key(Map#String)
            // value(文件#String) 变成 value(Map#List<String>)
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                String factoryTypeName = ((String) entry.getKey()).trim();
                // 这个 commaDelimitedListToStringArray 就是以逗号为分隔符,解析成字符串数组
                String[] factoryImplementationNames =
                    StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
                for (String factoryImplementationName : factoryImplementationNames) {
                    result.computeIfAbsent(factoryTypeName, key -> new ArrayList变成
                        .add(factoryImplementationName.trim());
                }
            }
        }

        // Replace all lists with unmodifiable lists containing unique elements
        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;
}

这个方法执行成功后,spring.factories 文件就已经被加载进来,并存放在 SpringFactoriesLoader 的缓存中

在 SpringBoot 启动流程的后续步骤中,也会多次从这个缓存获取相关数据

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2100380.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

交换机自动化备份配置(H3C_无人值守)

介绍&#xff1a; 在日常运维过程中&#xff0c;需要定时备份设备的配置&#xff0c;在设备数量过于庞大的情况下&#xff0c;对我们的运维工作会造成极大地不便&#xff0c;通过python自动化能够完美解决人工手动保存设备配置的问题。而且自动化运维在未来也一定是大势所趋&a…

集成电路学习:什么是IEEE电气和电子工程师学会

IEEE&#xff1a;电气和电子工程师学会 IEEE&#xff0c;全称是Institute of Electrical and Electronics Engineers&#xff0c;即电气和电子工程师学会&#xff0c;是一个国际性的电子技术与信息科学工程师的协会&#xff0c;也是目前全球最大的非营利性专业技术学会。IEEE成…

2024版mybatis基础入门学习详情(上)

目录 1、mybatis简介 2、mybatis日志设置 3、取值符号#{}和${}的区别 4、mybatis数据传入 4.1传入单个简单类型 4.2传入实体对象 4.3传入多个简单类型参数 4.4传入Map类型参数 5、mybatis数据返回 5.1返回单个简单类型 5.2返回单个自定义实体类型 5.3返回Map类型 …

快速申请代码签名证书

在当今的互联网世界中&#xff0c;软件开发者面临着越来越高的安全要求。为了保护用户免受恶意软件的侵害&#xff0c;并保证自己的应用程序不会被篡改&#xff0c;许多开发者选择为他们的软件进行代码签名。代码签名证书是确保软件来源真实性和完整性的一种重要工具。下面是申…

pyro ExponentialLR 如何设置优化器 optimizer的学习率 pytorch 深度神经网络 bnn,

第一。pyro 不支持 “ReduceLROnPlateau” &#xff0c;因为需要Loss作为输入数值&#xff0c;计算量大 第二 &#xff0c;svi 支持 scheduler注意点&#xff0c; 属于 pyro.optim.PyroOptim的有三个 AdagradRMSProp ClippedAdam DCTAdam&#xff0c;但是还是会报错&#xff…

号外!软考刷题小工具助力软考和 PMP 等级考试

一. 背景 四年前&#xff0c;我通过培训机构学习了 PMP&#xff0c;系统的学习了项目管理知识体系&#xff0c;说实话&#xff0c;学完感觉确实是有用的&#xff0c;尤其在项目管理方面&#xff0c;一些管理思维确实能够帮助到自己。 如果说 PMP 是国外的项目管理知识体系认证…

数据安全新纪元:Ftrans跨网跨域数据安全交换创新方案

随着业务的不断扩张和发展&#xff0c;大型组织企业&#xff0c;需要在不同的地理区域建设分支机构或办事处&#xff0c;用以覆盖更广泛的市场和客户群体&#xff0c;因此必然存在跨网跨域数据安全交换的场景需求。企业内部会同时存在下述一个或多个跨域文件交换场景&#xff1…

淘宝/天猫的拍立淘API:taobao.item_search_img返回值

淘宝/天猫的拍立淘&#xff08;Taobao Image Search&#xff09;功能允许用户通过上传图片来搜索相似的商品。然而&#xff0c;直接通过API使用这个功能&#xff08;如taobao.item_search_img&#xff09;在淘宝/天猫的官方API中并不直接提供。淘宝/天猫的开放平台&#xff08;…

k8s中pod基础及https密钥、horber仓库

一、pod基础&#xff1a; 1.pod是k8s里面最小单位&#xff0c;pod也是最小化运行容器的资源对象&#xff1b;容器是基于pod在k8s集群中工作&#xff1b;在k8s集群当中&#xff0c;一个pod就代表着一个运行进程&#xff0c;k8s的大部分组件都是围绕pod进行的&#xff0c;对pod进…

转换器和其他运放电路(恒流源+电压-电流/电流-电压转换器+峰值检测器)+故障检测(比较器故障+求和器故障)

2024-9-3&#xff0c;星期二&#xff0c;7:20&#xff0c;天气&#xff1a;阴转多云&#xff0c;心情&#xff1a;目前晴。又是上班的一天&#xff0c;没啥说的&#xff0c;加油上班&#xff0c;加油学习。 今天完成了第八章的学习&#xff0c;主要学习内容为&#xff1a;转换…

探索LLM大模型奥秘,新书《大模型入门:技术原理与实战应用》助你快速上手(附PDF下载)

随着大模型技术的不断完善和普及&#xff0c;我们将进入一个由数据驱动、智能辅助的全新工作模式和生活模式。个人和企业将能够利用大模型来降本增效&#xff0c;并创造全新的用户体验。 人工智能是人类探索未来的重要领域之一&#xff0c;以GPT为代表的大模型应用一经推出在短…

网站建设完成后, 做seo必须知道的专业知识之--robots协议

robots协议&#xff0c;也称为爬虫协议或机器人排除标准&#xff0c;是一种用于指导搜索引擎蜘蛛如何在网站上抓取和访问内容的协议。 网站建设完成后&#xff0c; 做seo必须知道的专业知识之--robots协议 通过这个协议&#xff0c;网站可以告诉搜索引擎哪些页面可以抓取&…

轴承知识大全,详细介绍(附3D图纸免费下载)

轴承一般由内圈、外圈、滚动体和保持架组成。对于密封轴承&#xff0c;再加上润滑剂和密封圈&#xff08;或防尘盖&#xff09;。这就是轴承的全部组成。 根据轴承使用的工作状况来选用不同类型的轴承&#xff0c;才能更好的发挥轴承的功能&#xff0c;并延长轴承的使用寿命。我…

设计模式 -- 迭代器模式(Iterator Pattern)

1 问题引出 编写程序展示一个学校院系结构&#xff1a;需求是这样&#xff0c;要在一个页面中展示出学校的院系组成&#xff0c;一个学校有多个学院&#xff0c; 一个学院有多个系 传统方式实现 将学院看做是学校的子类&#xff0c;系是学院的子类&#xff0c;这样实际上是站在…

CPU性能对比 Intel 海光 鲲鹏920 飞腾2500

横向对比一下x86和ARM芯片&#xff0c;以及不同方案权衡下的性能比较 CPU基本信息 海光 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #lscpu Architecture: x86_64 CPU…

ssm基于微信小程序的食堂线上预约点餐系统论文源码调试讲解

2系统相关技术 2.1 Java语言简介 Java是由SUN公司推出&#xff0c;该公司于2010年被oracle公司收购。Java本是印度尼西亚的一个叫做爪洼岛的英文名称&#xff0c;也因此得来java是一杯正冒着热气咖啡的标识。Java语言在移动互联网的大背景下具备了显著的优势和广阔的前景&…

优化SQL查询之了解SQL执行顺序

文章目录 优化SQL查询之了解SQL执行顺序举例的SQL语句SQL执行顺序 - 文字解释SQL执行顺序 - 图示SQL执行顺序 - 动画演示distinct 子句会在哪个位置除了6个主要的关键字&#xff0c;还有哪些关键字总结 优化SQL查询之了解SQL执行顺序 SQL 查询的执行过程并非是简单的按照语句的…

话费接口API对接流程是什么?又有哪些优势?

话费接口 API 对接流程 前期准备 找一家专业做话费充值的公司&#xff0c;联系其商务了解对接的具体情况&#xff0c;包括合作模式、话费价格、消耗及打款金额是否可以开票、对接时是否有技术配合等 开户与对接 确定合作后在话费充值平台进行开户&#xff0c;获取账户参数及…

14、Django Admin的“Action(动作)”中添加额外操作

如图红框增加操作 将以下代码添加到HeroAdmin类中 actions ["mark_immortal"] def mark_immortal(self, request, queryset):queryset.update(is_immortalTrue) 修改后完整代码如下&#xff1a; admin.register(Hero) class HeroAdmin(admin.ModelAdmin):list_di…

c++返回一个pair类型

前言 Under the new standard we can list initialize the return value. 代码测试 #include<iostream> #include<string> #include<vector>std::pair<std::string, int> process(std::vector<std::string>& v) {if (!v.empty()){return …