首页 > 编程语言 >SpringBoot源码初学者(一):SpringBoot功能扩展接口的使用与源码分析

SpringBoot源码初学者(一):SpringBoot功能扩展接口的使用与源码分析

时间:2023-07-04 22:37:21浏览次数:53  
标签:SpringBoot 步骤 ApplicationContextInitializer 源码 初学者 SpringFactoriesLoader public

 在长期的源码学习中,我一直没有找到一个讲解SpringBoot源码的文章,适合刚开始阅读源码的新手,包括我之前的写的一些文章,说实话SpringBoot的源码的确不适合新手,跳转多、结构复杂混乱,很容易迷乱其中。
长时间的学习当中,也总结出这些文章的部分问题:

  1. 没有说明用法,直接讲解源码,其实很多新手对SpringBoot了解不够多,还不明白怎么用,更不要想能理解源码了
  2. 源码阅读跳跃大,没有说清楚来龙去脉,只列出重要的部分源码
  3. 不够有趣,让人很难耐心的看下去,我也不是一个非常幽默的人,当我会尽可能的把这个文章写的有趣一些,也是对自己的配音
  4. 不成体系,SpringBoot是一个庞大的项目,类与类之间的关系错综复杂。如果不能很好的说明类之间的协作配合,就很难理解相互的流程
  5. 没有说明方法的参数,我最初阅读源码的时候就很纠结,这到底传了什么过来,我都不知道传参传了什么,更不要提搞清楚做了什么

  不管如何,我决定尝试克服这些问题,写一份真正适合阅读源码的新手来看的SpringBoot源码讲解,如果你真的想读懂SpringBoot源码,可以按照以下推荐的方式来阅读文章

  1. 打开ide,打开SpringBoot源码,跟着文章一起写注释,写自己的注释
  2. 不要过于纠结没讲到的地方,毕竟SpringBoot源码那么多,想全讲完是不可能的,只要跟着文章认真阅读,SpringBoot是如何运行的一定可以有一个较为深刻的理解
  3. 文章适合通篇阅读,不适合跳读,跳跃性的阅读很容易错过重要的东西
  4. 同样的如果之前的文章没有读过,还是最好先去看之前的文章
  5. 阅读源码必然少不了大段大段的源码,一定要耐心,不要翻翻了事,往往是那些最长的方法中才是真正需要学习的
  6. 如果断更了请用点赞、收藏、评论的方式激励我
    @TOC

一、ApplicationContextInitializer接口

  ApplicationContextInitializer,直译过来就是“应用程序上下文初始值设定接口”,在官方文档中描述其作用为“Spring容器刷新前执行的一个回调函数”,如果是对SpringBoot了解较深的老司机一定知道在SpringBoot初始化的时候有两个极为重要的步骤:准备上下文(prepareContext)和刷新上下文(refreshContext),那么ApplicationContextInitializer接口就在刷新上下文(refreshContext)前期准备工作的时候起作用,如果你还是一个“萌新”SpringBoot的初学者不知道这两个步骤也不要着急,我们后面细细道来。
  废话这么多,那么ApplicationContextInitializer接口到底是用来做什么的呢??又需要怎么使用它呢??
  ApplicationContextInitializer接口主要的作用是向SpringBoot的容器中注入属性,我们一共有3中方式来使用它,姿势很多直接开干。

1、将属性注入到容器

(1)spring.factories文件注入

步骤1:新建一个类并实现ApplicationContextInitializer接口


@Order(1)
public class TestInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        //从上下文中获取到程序环境
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        //创建需要注入的属性
        HashMap<String, Object> map = new HashMap<>();
        map.put("key1","value1");
        //将属性封装成配置项
        MapPropertySource testPropertySource = new MapPropertySource("testInitializer", map);
        //添加到项目环境配置的最末端
        environment.getPropertySources().addLast(testPropertySource);
        //完成控制台输出提示
        System.out.println("TestInitializer执行完成");
    }
}

步骤2:将编写好的类注入到容器中,在resources文件夹下新建META-INF文件夹,META-INF文件夹中创建一个spring.factories的文件,里面填写以下内容

#等号后面是需要加载的类,就是我们实现了ApplicationContextInitializer的类的全路径
org.springframework.context.ApplicationContextInitializer=com.gyx.test.TestInitializer

(2) SpringApplication手动注入

步骤1:新建一个类并实现ApplicationContextInitializer接口(与之前的完全一样)
步骤2:修改SpringBoot启动类

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(Application.class);
        //添加到初始化配置项中
        springApplication.addInitializers(new TestInitializer());
        springApplication.run(args);
    }
}

(3) 配置文件(application.properties)注册

步骤1:新建一个类并实现ApplicationContextInitializer接口(与之前的完全一样)
步骤2:修改SpringBoot的配置文件(Application.properties)

#等号后面是需要加载的类,就是我们实现了ApplicationContextInitializer的类的全路径
context.initializer.classes=com.gyx.test.TestInitializer

(4)注意事项

  1. 多个ApplicationContextInitializer可以使用@Order注解指定执行优先级,Order值越小越有限执行
  2. 配置文件(application.properties)的注册方式优先于其他的方式(具体原因,接着往下看)

2、将属性从容器中读出

@Component
public class TestRead implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext=applicationContext;
    }
    
    public String test(){
        return applicationContext.getEnvironment().getProperty("key1");
    }
}

  那么ApplicationContextInitializer是如何被注册到SpringBoot容器中的呢??有请第二位嘉宾闪亮登场!!伴随着音乐向我们走来的是“SpringFactoriesLoader”!

二、SpringFactoriesLoader抽象类

  SpringFactoriesLoader是个内敛害羞的汉子,是springboot框架中通用工厂加载机制的一种实现,会从classpath下多个jar包指定的位置读取文件并实例化类,读取的文件内容必须是key-value形式的,key是接口或者抽象的权限定名,value必须是对应的实现,可以用“,”分割。
  SpringFactoriesLoader是SpringBoot门派的守门大将,几乎是SpringBoot初始化最先接触到的类,让我们从SpringBoot的启动类跟着运行顺序看看能在哪里逮到SpringFactoriesLoader。

1、工厂加载机制源码解析

  通过这次阅读会了解SpringFactoriesLoader完整的调用过程,以及SpringFactoriesLoader与ApplicationContextInitializer接口是怎样互动的

步骤1:查看SpringBoot启动类

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        //进入run方法的源码,按着ctrl戳它
        SpringApplication.run(Application.class, args);
    }
}

步骤2:这里可以看到一层简单的调用

public static ConfigurableApplicationContext run(Class<?> primarySource,
            String... args) {
       //进入这个同名方法,继续戳run方法,两个参数分别是传入的启动类和java的启动配置
        return run(new Class<?>[] { primarySource }, args);
}

步骤3:这里就比较有意思了,注意一下注释

public static ConfigurableApplicationContext run(Class<?>[] primarySources,
            String[] args) {
       //这里将SpringBoot的启动分成了两个重要的部分
       //创建一个SpringApplication对象,这个构造方法是在配置SpringBoot自己的一些基本参数,做一些基本数据的获取
       //run方法,正式的启动,开始管理SpringBoot的生命周期
       //点这个SpringApplication构造方法
        return new SpringApplication(primarySources).run(args);
}

步骤4:没有什么用的封装,对构成函数复用

public SpringApplication(Class<?>... primarySources) {
       //点this,查看最重要的构造函数
        this(null, primarySources);
}

步骤5:这里我们可以看到两个熟悉的名字getSpringFactoriesInstances方法和ApplicationContextInitializer接口

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();
       //目光聚焦到这里,上面的部分先不要管他,就是在做简单的赋值
       //setInitializers方法:设置初始化项
       //getSpringFactoriesInstances方法:怎么获取需要的初始化项,当然是通过SpringFactoriesInstances来获取,所以我们先要获取到SpringFactoriesInstances
       //我们要告诉SpringFactoriesInstances这些初始化项都要哪些特征啊,哦!原来他们都是ApplicationContextInitializer接口的实现类
       //点进 getSpringFactoriesInstances方法
        setInitializers((Collection) getSpringFactoriesInstances(
                ApplicationContextInitializer.class));
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = deduceMainApplicationClass();
}

步骤6:又是一层同名封装,SpringBoot难看懂有一大半原因就是他喜欢叠千层饼

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
       //不读多说直接戳这个getSpringFactoriesInstances方法
        return getSpringFactoriesInstances(type, new Class<?>[] {});
    }

步骤7:这里正式出现了SpringFactoriesLoader,仔细阅读,先来看一下这3个参数,
Class<T> type:是之前传进来的“ApplicationContextInitializer.class”
Class<?>[] parameterTypes:是空的数组
Object... args:干脆啥都没有传

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
            Class<?>[] parameterTypes, Object... args) {
       //获取类加载器,不需要特别在意
        ClassLoader classLoader = getClassLoader();
        // 这个set里面是所有的SpringFactoriesLoader方法的实现的全限定名
        Set<String> names = new LinkedHashSet<>(
              //查找所有的SpringFactoriesLoader方法的实现的全限定名
              //进入loadFactoryNames,跳至步骤8
                SpringFactoriesLoader.loadFactoryNames(type, classLoader));
       //将这些实现类全部创建出来,我们获得到了一群“内敛害羞的汉子”
       //进入createSpringFactoriesInstances,跳至 步骤10
        List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
                classLoader, args, names);
       //在根据Order对实例进行排序(@Order注解或者Order接口)
        AnnotationAwareOrderComparator.sort(instances);
       //返回排序后的结果,SpringFactoriesInstances的源码告一段落,再往后就是另一位嘉宾的故事了
        return instances;
    }

步骤8:这里我们终于初次窥见SpringFactoriesLoader的真面目,这里开始正式的进入SpringFactoriesLoader类的内部

public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
       //还记factoryClass是什么吗?没错!是ApplicationContextInitializer.class
       //获取它的类名,这里是ApplicationContextInitializer接口和SpringFactoriesLoader第一次牵手
        String factoryClassName = factoryClass.getName();
       //点击loadSpringFactories进入 步骤9
       //getOrDefault方法:找到key是ApplicationContextInitializer接口的集合,有就返回这个集合,如果没有就返回一个空集合
       //返回到 步骤7,到这里就走出了SpringFactoriesLoader
        return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

步骤9:看起来loadSpringFactories是个很复杂的方法,其实不然,loadSpringFactories非常简单,仔仔细细的一起来读一下

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
       //SpringFactoriesLoader内部的缓存,第一次进来的时候为空,后面进来就可以直接返回结果,优化加载速度
        MultiValueMap<String, String> result = cache.get(classLoader);
        if (result != null) {
            return result;
        }

        try {
           //在步骤7中获取到了classLoader,所以classLoader不为空
           //用类加载器读取资源
           //FACTORIES_RESOURCE_LOCATION这常量是啥呢??如果小伙伴们已经迫不及待的点进去看了,可以很惊奇的看到这不就是"META-INF/spring.factories"!
           //文章往上翻(或者搜索)找到“spring.factories文件注入”的步骤2,我们是不是创建的就是这个文件,路径和文件名完全一样,我们似乎要接近真相了
            Enumeration<URL> urls = (classLoader != null ?
                    classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                    ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
            result = new LinkedMultiValueMap<>();
           //这里把所有找到的资源循环
           //聪明的小朋友可能就有疑惑了!为什么这里要用循环啊,我们不是只有一个spring.factories文件吗?
           //相信所有的小伙伴们都有开发经验,当我们引入一个叫做“XXX-stater”的jar包时,jar包里面就会有一个spring.factories文件,并且SpringBoot自己也有一个spring.factories文件
           //别着急,spring.factories文件的功能可不止注入ApplicationContextInitializer的实现类,我们以后还会和他见面
            while (urls.hasMoreElements()) {
              //获取一条数据(这里占时也只能拿到2条数据,一个我们写的,一个是SpringBoot自身的)
                URL url = urls.nextElement();
                UrlResource resource = new UrlResource(url);
              //借助Properties类型的文件读取工具,读取spring.factories文件中配置的ApplicationContextInitializer的实现类,当然现在他们还只是一条条可怜的全限定名 
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);
              //循环所有的获取到的内容
                for (Map.Entry<?, ?> entry : properties.entrySet()) {
                  //先获取key,就是接口或抽象类的全限定名(就是我们写在spring.factories文件中的“org.springframework.context.ApplicationContextInitializer”等号前面的这段)
                    String factoryClassName = ((String) entry.getKey()).trim();
                 //使用工具根据“,”将value进行切割成字符串集合(不知道原因的小伙伴在文章中搜索“value必须是对应的实现,可以用“,”分割”,然后仔细读几遍)
                    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);
        }
    }

步骤10:经过步骤9,获取到所有的指定的ApplicationContextInitializer实现类的全限定名,这这一步中把它们逐一创建出来
回顾一下参数:
Class<T> type:是之前传进来的“ApplicationContextInitializer.class”
Class<?>[] parameterTypes:是空的数组
Object[] args:干脆啥都没有传是个null
names:ApplicationContextInitializer所有实现类的全限定名

标签:SpringBoot,步骤,ApplicationContextInitializer,源码,初学者,SpringFactoriesLoader,public
From: https://www.cnblogs.com/xmsz118/p/17527238.html

相关文章

  • Java源码系列4——HashMap扩容时究竟对链表和红黑树做了什么?
    Photobyhippopx.com我们知道HashMap的底层是由数组,链表,红黑树组成的,在HashMap做扩容操作时,除了把数组容量扩大为原来的两倍外,还会对所有元素重新计算hash值,因为长度扩大以后,hash值也随之改变。如果是简单的Node对象,只需要重新计算下标放进去就可以了,如果是链表和红黑......
  • AI 实战篇 |基于 AI开放平台实现 人脸识别对比 功能,超详细教程【附带源码】
    ......
  • 从零搭建SpringBoot3一,手动编写一套属于自己风格的代码生成器一键生成系统
    简介虽然java的代码生成工具有很多,可是很多时候不是自己喜欢的风格,改起来比较困难,所以我准备从零和大家一起搭建一套基于springboot3.0的框架,这次就先搞定一套代码生成功能,后续再不断的完善其它我们使用到的三方库:beelt模版引擎,用于生成代码。官网:http://ibeetl.com......
  • SpringBoot 3.0.x使用SpringDoc
    为什么使用SpringDoc在Springfox3.0停更的两年里,SpringBoot进入3.0时代,SpringFox出现越来越多的问题,最为明显的就是解析器的问题,已经在上文中解释清楚,这里就不再赘述。SpringDoc是Spring官方推荐的API,相信不会轻易停更。如何引入SpringDocSpringDoc有多个版本,如果你使用的......
  • SpringBoot3.0从入门到项目实战:解决Web应用痛点的最新解决方案
    SpringBoot3.0从入门到项目实战:解决Web应用痛点的最新解决方案SpringBoot是当前Java领域中应用最广的框架之一,而随着SpringBoot3.0的发布,它迎来了更加全面和强大的一次升级。本文将深入浅出地介绍SpringBoot3.0的新特性,同时结合实际项目经验,分享Web应用的痛点以及解决方案,帮......
  • Springboot开发no.1
    springboot是一个简化spring初始化和开发spring创建空工程,检查Maven,创建模块springinitializr  使用rest模式:@RestController,@RequestMapping("/books")结果  运行出现问题:第一个问题:java:无法访问org.springframework.web.bind.annotation.GetMa......
  • rabbitmq在springboot中实战技巧
    一.简介rabbitmq是基于AMQP(AdvancedMessageQueuingProtocol:高级消息队列协议),采用Erlang语言编写的消息队列。二、mq能用来做什么异步处理:将非核心业务(比如日志、邮件、监控等)从主流程剥离,提升主流程的响应时效。削峰:当并发大的情况下,可以将消息暂存在消息队列中,消费者按照......
  • Springboot : 连接ldap超时问题
    Err:java.net.ConnectException:Connectiontimedoutwhenconnectingtoldap使用springbootldap连接账号所属ldap目录验证时,出现如上报错经检查,host,username,password等信息均无误,如下为代码中的配置信息示例hashEnv.put(Context.SECURITY_AUTHENTICATION,"simple"......
  • 2023最新ChatGPT网站源码/支持用户付费套餐+赚取收益
    第一步-配置APIKEY:在"index.php"最顶部配置自己的APIKEY,不然网站无法使用!第一步-配置数据库:lib/config.php第三步-导入数据库第四步-PHP选择:7.3第五步-访问网页即可!下载链接:https://pan.saipancloud.com/s/Ij6NFbKhdI解压密码:UUdlNGWEacOclJWP      ......
  • 全新ChatGPT3.5小程序开源后端+前端源码
    首发ChatGPT3.5小程序开源vue!这一版本ui比较好看回复速度也快了小程序是java的带后台本来准备给你们带上接口的然后么后台是和接口连接的我改什么内容你们前段都会显示所以开源自己搭建下吧,腾讯云买个国外服务器就可以了几十块钱!而且最近openkey封号比较频繁所以大家自己......