《SpringBoot启动流程一》:万字debug梳理SpringBoot如何加载并处理META-INF/spring.factories文件中的信息

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 1、SpringBoot如何加载并处理spring.factories中的文件?2、加载出的spring.factories文件中的内容如何排序?3、多个spring.factories文件内容重复怎么处理?

一、前言

看Spring Boot源码的时候,发现在SpringApplication初始化阶段会加载Spring应用上下文初始化器(ApplicationContextInitializer)、加载Spring应用事件监听器(ApplicationListener);而ApplicationContextInitializer 和 ApplicationListener内建的实现类预置在spring-boot jar包的META-INF/spring.factories文件中;
在这里插入图片描述
此外,在spring-boot-autoconfigure jar包的META-INF/spring.factories文件中也有一部分:
在这里插入图片描述
所以,在Spring Boot中一共内建了11个ApplicationListener、7个ApplicationContextInitializer。

那么SpringBoot是怎么将其加载到Spring容器中的呢?怎么加载到SpringApplication中的呢?我们就此展开研究。

二、正文

入口

无论是在SpringApplication初始化阶段时加载Spring事件监听器ApplicationListener、Spring应用上下文初始化器ApplicationContextInitializer,还是在SpringApplication准备阶段时加载Spring运行时监听器SpringApplicationRunListener、异常报告器SpringBootExceptionReporter,都要从SpringApplication#getSpringFactoriesInstances()重载方法开始,并且进入到getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args)
在这里插入图片描述
以Spring应用上下文初始化器为例,此处的typeApplicationContextInitializer

整体的处理流程为:
在这里插入图片描述

下面我们分开来看;

1、找到type的所有实现类

使用Spring工厂加载机制方法SpringFactoriesLoader.loadFactoryName(Class,ClassLoader)来做这个操作;
在这里插入图片描述
SpringFactoriesLoader.loadFactoryName(Class,ClassLoader)中首先会根据类加载器加载出所有spring.factories中的所有内容。

1)loadSpringFactories(ClassLoader)

loadSpringFactories(ClassLoader)会解析所有加载的jar包中 META-INF/spring.factories配置文件的配置内容,并组装为Map<String, List>数据结构,方法返回。具体流程如下:

  1. 首先,去缓冲中查询是否有入参classLoader对应的配置信息(仅第一次加载spring.factories文件时不走缓存),如果存在,则表明服务之前解析过配置文件 并 方法返回。如果不存在,则进行解析操作。
  2. 其次,获得所有依赖jar包中,具有META-INF/spring.factories配置文件的jar文件URI,并依次进行遍历。
  3. 接着,将spring.factories配置的内容转化成properties实例;遍历properties实例,将key和value维护到Map<String, List<String>> result数据结构中,如果多个spring.factories中的key相同,则value取合集。
  4. 最后,将result维护到缓冲cache中——key=ClassLoader value=result;并将result作为返回值返回。

<1> 缓存cache数据结构:

static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap<>();
AI 代码解读

<2> 方法主体:

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    // 1、去缓存中,查询是否有入参classLoader对应的配置信息,
    // 如果存在,则表明服务之前解析过配置文件。如果不存在,则进行解析操作
    MultiValueMap<String, String> result = cache.get(classLoader);
    // 缓存中存在则直接返回
    if (result != null) {
        return result;
    }

    try {
        // 2、获得所有依赖jar包中,具有META-INF/spring.factories配置文件的jar文件URI
        // todo 问自己一个问题,它是怎么找到的?
        Enumeration<URL> urls = (classLoader != null ?
                classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        result = new LinkedMultiValueMap<>();
        // 遍历所有的URI
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            // 通过url获得资源resource
            UrlResource resource = new UrlResource(url);
            // 3、将spring.factories配置的内容转化成properties实例
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            // 4、遍历properties实例,将key和value维护到Map<String, List<String>> result数据结构中
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                String factoryTypeName = ((String) entry.getKey()).trim();
                // StringUtils.commaDelimitedListToStringArray只是单纯的将字符串转为String[]数组
                for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                    result.add(factoryTypeName, factoryImplementationName.trim());
                }
            }
        }
        // 4、将result维护到缓冲cache中——key=ClassLoader value=result
        cache.put(classLoader, result);
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}
AI 代码解读

在遍历properties实例,将key和value维护到Map<String, List> result数据结构的过程中,可以发现一个问题:如果多个spring.factories文件中针对同一个key有相同的value值,那岂不就是重复添加了。

假设,我依赖的某一个jar包的META-INF/factories中和 spring-boot jar包的META-INF/factories中都有 `org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\`。则 ConfigurationWarningsApplicationContextInitializer会被添加两次到 ApplicationContextInitializer对应的 List<String>中。

spring不会有这种bug吧?显然是不可能的,往上追,追到SpringApplication类中,在获取到type类所有实现类的类名时会用Set集合做一个去重。
在这里插入图片描述
思考一下为什么不能在最底层就做去重呢?而需要每个调用方都自己去重!

2)classLoader.getResources(FACTORIES_RESOURCE_LOCATION)

我很好奇,classLoader.getResources(FACTORIES_RESOURCE_LOCATION)它是如何找到所有META-INF/spring.factories文件的?所以这里特意写一小节。

此处的classLoader为AppClassLoader;比较有意思的是getResources(FACTORIES_RESOURCE_LOCATION)方法使用F7进不去,要进去到AppClassLoader里打断点。
在这里插入图片描述

AppClassLoaderLauncher的静态内部类,其类图如下:
在这里插入图片描述
即,AppClassLoader间接继承自ClassLoader,而getResources(String name)方法在其类结构中只出现在ClassLoader中,所以要去ClassLoader中打断点;
在这里插入图片描述
加载Resource资源时也会用到父类加载器。递归由AppClassLoader 的父加载器 ExtClassLoader 负责加载Resource资源;最终体现为:Enumeration<URL>[]数组的0下标所表示其父类、祖父类加载器加载到的Resources资源,而1下标处表示自己加载到Resources资源,这也和双亲委派机制的不一样的点。

AppClassLoader 的父类、祖父类加载器并没有加载到任何资源(因为META-INF/Spring.factories文件也只存在于AppClassLoader的扫描的目录下)。
在这里插入图片描述
最后看一下AppClassLoader是怎么找到所有的META-INF/spring.factories文件的?

1> 因为ClassLoader#findResources(String)是一个抽象方法,具体逻辑由子类实现,结合AppClassLoader的类图,定位到URLClassLoader#findResources(String)

在这里插入图片描述
在URLClassLoader内部会调用其组合的URLClassPath类的findResources(String, boolean)方法去做一个真正的资源扫描操作。
在这里插入图片描述
最终效果如下,但是不建议追(太深了,并且很不好debug)。
在这里插入图片描述
但是有一点我们可以记住:hasMoreElements() 和 nextElement()方法均出自sun.misc包下的CompoundEnumeration类。

有兴趣的建议参考博主打断点的思路继续深追如下代码段:
在这里插入图片描述

3)loadSpringFactories(ClassLoader)返回结果

在这里插入图片描述
接着通过Map的getOrDefault()方法获取到result中key为ApplicationContextInitializer的value。
在这里插入图片描述
最后回到SpringApplication#getSpringFactoriesInstances()方法中,使用Set集合来接返回值,以达到一个去重的效果。
在这里插入图片描述

2、实例化type的所有实现类

紧接着上面进入到createSpringFactoriesInstances()方法根据类的全路径名做实例化操作。

List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AI 代码解读

遍历所有的全路径类名,使用AppClassLoader将相应Class文件从磁盘装载到内存中,然后利用反射获取Class类的无参构造函数、实例化对象。

注意:要实例化的类必须要有无参构造函数。

private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes,
        ClassLoader classLoader, Object[] args, Set<String> names) {
    List<T> instances = new ArrayList<>(names.size());
    for (String name : names) {
        try {
            // 装载class文件到内存
            Class<?> instanceClass = ClassUtils.forName(name, classLoader);
            Assert.isAssignable(type, instanceClass);
            Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
            // 利用反射实例化对象
            T instance = (T) BeanUtils.instantiateClass(constructor, args);
            instances.add(instance);
        }
        catch (Throwable ex) {
            throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex);
        }
    }
    return instances;
}
AI 代码解读

3、做Order排序

将type类对应的所有实现类实例化完毕之后,要对他们做一个根据Order的排序。

AnnotationAwareOrderComparator.sort(instances);
AI 代码解读

sort()方法中直接使用List集合的sort()方法,但需要自定义Comparator为当前类实例AnnotationAwareOrderComparator
在这里插入图片描述
再看AnnotationAwareOrderComparator的类结构:
在这里插入图片描述
AnnotationAwareOrderComparator继承自OrderComparator,自定义Comparator需要实现Comparator的抽象方法compare(T o1, T o2)。AnnotationAwareOrderComparator自身没有compare()方法,所以看其父类OrderComparator中的compare方法;

在这里插入图片描述
因为OrderComparator#doCompare(Object o1, Object o2, OrderSourceProvider sourceProvider)方法中的入参sourceProvider为null,所以进入到getOrder()方法时,后续直接调用子类的findOrder(Object obj)方法去查找相应类的顺序值。
在这里插入图片描述
AnnotationAwareOrderComparator#findOrder()方法中先调用父类OrderComparator#findOrder()方法,如果找到顺序值直接返回,否者从类的@Order注解中取到顺序值。

1> 先看OrderComparator#findOrder()方法:

  • 该方法判断obj有没有实现Ordered接口,实现Ordered接口之后,有没有重写其getOrder()方法,如果重写了,则直接从getOrder()中获取到序列值。
@Nullable
protected Integer findOrder(Object obj) {
    return (obj instanceof Ordered ? ((Ordered) obj).getOrder() : null);
}
AI 代码解读

以ContextIdApplicationContextInitializer为例,其getOrder()方法返回的序列值为2147483637
在这里插入图片描述

2> 再看AnnotationAwareOrderComparator#findOrder()方法:

  • 如果通过OrderComparator#findOrder()获取不到序列值,则通过findOrderFromAnnotation()方法从@Order注解中获取序列值。
@Override
@Nullable
protected Integer findOrder(Object obj) {
    Integer order = super.findOrder(obj);
    if (order != null) {
        return order;
    }
    return findOrderFromAnnotation(obj);
}
AI 代码解读

以ConfigurationWarningsApplicationContextInitializer为例:
在这里插入图片描述

findOrderFromAnnotation()方法中通过OrderUtils.getOrderFromAnnotations(element, annotations);获取@Order注解中的值:
在这里插入图片描述
OrderUtils.getOrderFromAnnotations(element, annotations);方法返回之后,如果order为null,则直接返回null。
在这里插入图片描述
最后返回到OrderComparator#getOrder()方法,如果order为null,则将Order设置为Integer.MAX_VALUE
在这里插入图片描述
如果两个对象的Order序列值一样,则按原本在集合中的顺序先后排列。

1)总述

利用List集合自身的排序,通过传入自定义的Comparator 实现排序规则。规则具体如下:

  • AnnotationAwareOrderComparator类继承自OrderComparator,排序规则体现在OrderComparator类中的compare()方法;
  • 每个排序对象都会通过OrderComparator#getOrder()方法获取排列的序列值。
  • getOrder()方法中首先通过findOrder()方法查找序列值,而AnnotationAwareOrderComparator重写了findOrder()方法。所以调用findOrder()方法会先进入到AnnotationAwareOrderComparator#findOrder()方法。
  • 在AnnotationAwareOrderComparator#findOrder()方法中,会先调用其父类OrderComparator#findOrder()方法判断对象是否实现Ordered接口 并 重写了getOrder()方法,有则返回getOrder()的值。

    • 否则通过findOrderFromAnnotation()方法从对象的@Order注解中获取具体的值,如果对象没有被@Order注解标注,则返回null。
  • 最后到OrderComparator#getOrder()层面,如果findOrder()返回了具体的Integer值,则返回,否者返回Integer.MAX_VALUE。

下篇接着聊SpringBoot启动流程之SpringApplication准备阶段。

目录
打赏
0
1
0
0
43
分享
相关文章
微服务——SpringBoot使用归纳——Spring Boot 中集成Redis——Redis 介绍
本文介绍在 Spring Boot 中集成 Redis 的方法。Redis 是一种支持多种数据结构的非关系型数据库(NoSQL),具备高并发、高性能和灵活扩展的特点,适用于缓存、实时数据分析等场景。其数据以键值对形式存储,支持字符串、哈希、列表、集合等类型。通过将 Redis 与 Mysql 集群结合使用,可实现数据同步,提升系统稳定性。例如,在网站架构中优先从 Redis 获取数据,故障时回退至 Mysql,确保服务不中断。
27 0
微服务——SpringBoot使用归纳——Spring Boot 中集成Redis——Redis 介绍
|
4天前
|
微服务——SpringBoot使用归纳——Spring Boot集成 Swagger2 展现在线接口文档——Swagger2 的使用
本文详细介绍了Swagger2的使用方法,包括在Spring Boot项目中的配置与应用。重点讲解了Swagger2中常用的注解,如实体类上的`@ApiModel`和`@ApiModelProperty`,Controller类上的`@Api`、`@ApiOperation`以及参数上的`@ApiParam`等。通过示例代码展示了如何为实体类和接口添加注解,并在页面上生成在线接口文档,实现接口测试。最后总结了Swagger的优势及其在项目开发中的重要性,提供了课程源代码下载链接供学习参考。
34 0
微服务——SpringBoot使用归纳——Spring Boot集成 Swagger2 展现在线接口文档——Swagger2 的使用
|
4天前
|
微服务——SpringBoot使用归纳——Spring Boot中集成 Shiro——Shiro 身份和权限认证
本文介绍了 Apache Shiro 的身份认证与权限认证机制。在身份认证部分,分析了 Shiro 的认证流程,包括应用程序调用 `Subject.login(token)` 方法、SecurityManager 接管认证以及通过 Realm 进行具体的安全验证。权限认证部分阐述了权限(permission)、角色(role)和用户(user)三者的关系,其中用户可拥有多个角色,角色则对应不同的权限组合,例如普通用户仅能查看或添加信息,而管理员可执行所有操作。
27 0
微服务——SpringBoot使用归纳——Spring Boot中集成 Shiro——Shiro 三大核心组件
本课程介绍如何在Spring Boot中集成Shiro框架,主要讲解Shiro的认证与授权功能。Shiro是一个简单易用的Java安全框架,用于认证、授权、加密和会话管理等。其核心组件包括Subject(认证主体)、SecurityManager(安全管理员)和Realm(域)。Subject负责身份认证,包含Principals(身份)和Credentials(凭证);SecurityManager是架构核心,协调内部组件运作;Realm则是连接Shiro与应用数据的桥梁,用于访问用户账户及权限信息。通过学习,您将掌握Shiro的基本原理及其在项目中的应用。
28 0
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——基于 xml 的整合
本教程介绍了基于XML的MyBatis整合方式。首先在`application.yml`中配置XML路径,如`classpath:mapper/*.xml`,然后创建`UserMapper.xml`文件定义SQL映射,包括`resultMap`和查询语句。通过设置`namespace`关联Mapper接口,实现如`getUserByName`的方法。Controller层调用Service完成测试,访问`/getUserByName/{name}`即可返回用户信息。为简化Mapper扫描,推荐在Spring Boot启动类用`@MapperScan`注解指定包路径避免逐个添加`@Mapper`
23 0
微服务——SpringBoot使用归纳——Spring Boot集成Thymeleaf模板引擎——Thymeleaf 介绍
本课介绍Spring Boot集成Thymeleaf模板引擎。Thymeleaf是一款现代服务器端Java模板引擎,支持Web和独立环境,可实现自然模板开发,便于团队协作。与传统JSP不同,Thymeleaf模板可以直接在浏览器中打开,方便前端人员查看静态原型。通过在HTML标签中添加扩展属性(如`th:text`),Thymeleaf能够在服务运行时动态替换内容,展示数据库中的数据,同时兼容静态页面展示,为开发带来灵活性和便利性。
20 0
微服务——SpringBoot使用归纳——Spring Boot中集成ActiveMQ——ActiveMQ安装
本教程介绍ActiveMQ的安装与基本使用。首先从官网下载apache-activemq-5.15.3版本,解压后即可完成安装,非常便捷。启动时进入解压目录下的bin文件夹,根据系统选择win32或win64,运行activemq.bat启动服务。通过浏览器访问`http://127.0.0.1:8161/admin/`可进入管理界面,默认用户名密码为admin/admin。ActiveMQ支持两种消息模式:点对点(Queue)和发布/订阅(Topic)。前者确保每条消息仅被一个消费者消费,后者允许多个消费者同时接收相同消息。
27 0
微服务——SpringBoot使用归纳——Spring Boot中集成ActiveMQ——ActiveMQ安装
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
本文介绍了在Spring Boot项目中如何通过创建`GlobalExceptionHandler`类来全局处理系统异常。通过使用`@ControllerAdvice`注解,可以拦截项目中的各种异常,并结合`@ExceptionHandler`注解针对特定异常(如参数缺失、空指针等)进行定制化处理。文中详细展示了处理参数缺失异常和空指针异常的示例代码,并说明了通过拦截`Exception`父类实现统一异常处理的方法。虽然拦截`Exception`可一劳永逸,但为便于问题排查,建议优先处理常见异常,最后再兜底处理未知异常,确保返回给调用方的信息友好且明确。
28 0
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
微服务——SpringBoot使用归纳——Spring Boot集成Thymeleaf模板引擎——Thymeleaf 的使用
本文介绍了 Thymeleaf 在 Spring Boot 项目中的使用方法,包括访问静态页面、处理对象和 List 数据、常用标签操作等内容。通过示例代码展示了如何配置 404 和 500 错误页面,以及如何在模板中渲染对象属性和列表数据。同时总结了常用的 Thymeleaf 标签,如 `th:value`、`th:if`、`th:each` 等,并提供了官方文档链接以供进一步学习。
27 0
微服务——SpringBoot使用归纳——Spring Boot集成Thymeleaf模板引擎——Thymeleaf 的使用
微服务——SpringBoot使用归纳——Spring Boot中集成ActiveMQ——发布/订阅消息的生产和消费
本文详细讲解了Spring Boot中ActiveMQ的发布/订阅消息机制,包括消息生产和消费的具体实现方式。生产端通过`sendMessage`方法发送订阅消息,消费端则需配置`application.yml`或自定义工厂以支持topic消息监听。为解决点对点与发布/订阅消息兼容问题,可通过设置`containerFactory`实现两者共存。最后,文章还提供了测试方法及总结,帮助读者掌握ActiveMQ在异步消息处理中的应用。
30 0

热门文章

最新文章