从零开始 Spring Boot 37:初始化 ApplicationContext

news2024/11/15 15:59:16

从零开始 Spring Boot 37:初始化 ApplicationContext

spring boot

图源:简书 (jianshu.com)

从前文可以知道,作为 Ioc 容器的 ApplicationContext,需要进行一系列步骤来初始化以最终就绪(对于 Web 应用来说就是可以提供Http服务)。

这些步骤大概可以分为以下内容:

  1. 准备上下文关联的Environment
  2. 初始化 ApplicationContext(ApplicationContextInitializers被调用)。
  3. 加载 Bean 定义(通过注解或XML)。
  4. 刷新容器。
  5. 就绪。

Application 事件

Spring 用一系列事件来表示这些行为,并且在框架内通过发布和监听相应的事件来完成各种任务。

这些事件可以用下图表示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OPYhbrEC-1686566310630)(D:\image\typora\Application-events.png)]

因此,如果我们要在 Spring Application 启动时候的特定阶段执行某段代码,可以通过监听相应事件的方式来完成。

绘图时少了一个事件,如果最后 Application 启动失败,会产生一个ApplicationFailedEvent事件。

最简单的方式是创建一个实现了ApplicationListener接口的 Spring Bean:

@Log4j2
@Configuration
public class WebConfig {
	@Bean
    public ApplicationListener<ContextRefreshedEvent> contextRefreshedEventApplicationListener(){
        return event -> log.debug("ContextRefreshedEvent is called.");
    }

    @Bean
    public ApplicationListener<WebServerInitializedEvent> webServerInitializedEventApplicationListener(){
        return event -> log.debug("WebServerInitializedEvent is called.");
    }

    @Bean
    public ApplicationListener<ApplicationStartedEvent> applicationStartedEventApplicationListener(){
        return event -> log.debug("ApplicationStartedEvent is called.");
    }

    @Bean
    public ApplicationListener<AvailabilityChangeEvent> applicationAliveListener(){
        return event -> {
            AvailabilityState state = event.getState();
            if (state == LivenessState.CORRECT){
                log.debug("AvailabilityChangeEvent is called, and now Application is lived.");
            }
        };
    }

    @Bean
    public ApplicationListener<ApplicationReadyEvent> applicationReadyEventApplicationListener(){
        return event -> log.debug("ApplicationReadyEvent is called.");
    }

    @Bean
    public ApplicationListener<AvailabilityChangeEvent> applicationAllReadyListener(){
        return event -> {
            AvailabilityState state = event.getState();
            if (state == ReadinessState.ACCEPTING_TRAFFIC){
                log.debug("AvailabilityChangeEvent is called, and now application is all ready.");
            }
        };
    }
}

执行上面这个示例就能看到事件依次被调用的过程。

需要注意的是,这种方式只对部分事件有用,对于某些“早期”事件,比如ApplicationStartingEvent,事件发生的时候 ApplicationContext 还没有创建和初始化,更别提加载 bean 定义了,因此即使你定义了相应事件的监听器 bean,相应的代码也不可能会被执行。

如果我们要监听这些早期事件,可以:

@SpringBootApplication
@Log4j2
public class IniApplication {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(IniApplication.class);
        application.addListeners(applicationStartedEventApplicationListener(),
                applicationEnvironmentPreparedEventApplicationListener(),
                applicationContextInitializedEventApplicationListener(),
                applicationPreparedEventApplicationListener());
        application.run(args);
    }

    private static ApplicationListener<ApplicationStartingEvent> applicationStartedEventApplicationListener(){
        return event -> System.out.println("ApplicationStartingEvent is called.");
    }

    private static ApplicationListener<ApplicationEnvironmentPreparedEvent> applicationEnvironmentPreparedEventApplicationListener(){
        return event -> log.debug("ApplicationEnvironmentPreparedEvent is called.");
    }

    private static ApplicationListener<ApplicationContextInitializedEvent> applicationContextInitializedEventApplicationListener(){
        return event -> log.debug("ApplicationContextInitializedEvent is called.");
    }

    private static ApplicationListener<ApplicationPreparedEvent> applicationPreparedEventApplicationListener(){
        return event -> log.debug("ApplicationPreparedEvent is called.");
    }
}

示例中的ApplicationStartingEvent事件监听器没有使用log.debug输出日志,因为实际测试发现这样做不会产生任何输出(也没有报错),只能猜测是此阶段日志模块还没有被正常加载。如果有谁了解更多,可以留言说明,谢谢。

除了通过SpringApplication.addListeners()添加监听器,还可以通过SpringApplicationBuilder.listeners()添加:

@SpringBootApplication
@Log4j2
public class IniApplication {
    public static void main(String[] args) {
        new SpringApplicationBuilder()
                .sources(IniApplication.class)
                .listeners(applicationEnvironmentPreparedEventApplicationListener(),
                        applicationStartedEventApplicationListener(),
                        applicationContextInitializedEventApplicationListener(),
                        applicationPreparedEventApplicationListener())
                .run(args);
    }
    // ...
}

排序

所有的事件监听器都是在主线程上依次执行的,因此很容易为它们指定一个顺序,以控制监听同一事件的监听器的先后执行顺序。

比如:

@Log4j2
@Configuration
public class WebConfig {
    @Order(1)
    @Bean
    public ApplicationListener<ApplicationReadyEvent> applicationReadyEventApplicationListener1(){
        return event -> log.debug("ApplicationReadyEvent1 is called.");
    }

    @Order(2)
    @Bean
    public ApplicationListener<ApplicationReadyEvent> applicationReadyEventApplicationListener2(){
        return event -> log.debug("ApplicationReadyEvent2 is called.");
    }
}

两个都是监听ApplicationReadyEvent事件的监听器,其中用@Order(1)标记的监听器会先于@Order(2)标记的监听器被执行。

多层结构

Application 事件是通过 Spring 框架的事件发布机制发布的,该机制确保了如果 ApplicationContext 是一个多层级的,那么一个子级的 ApplicationContext 产生的事件同样会发布给其父容器。在这种结构下,我们可能需要在监听器中区分事件是由子容器还是当前容器产生的,这点可以通过对比事件关联的 ApplicationContext 以及当前的 ApplicationContext 来区分:

@Component
@Log4j2
public class MyApplicationStartedEventListener implements ApplicationListener<ApplicationStartedEvent> {
    private final ApplicationContext applicationContext;

    public MyApplicationStartedEventListener(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        ApplicationContext eventCtx = event.getApplicationContext();
        if (eventCtx == applicationContext){
            //当前 ApplicationContext 发送的事件
            log.debug("Current ctx's ApplicationStartedEvent is called.");
        }
        else{
            log.debug("Sub ctx's ApplicationStartedEvent is called.");
        }
    }
}

在这个示例中,可以简单地通过依赖注入在 bean 中获取当前的ApplicationContext,如果无法通过这种方式获取(比如 context),可以通过实现ApplicationContextAware来注入。

更多关于 Aware 接口的说明见从零开始 Spring Boot 27:IoC - 红茶的个人站点 (icexmoon.cn)。

ApplicationRunner 和 CommandLineRuner

就像前面展示的,如果要在 Spring Application 初始化的某个阶段执行代码,我们只需要使用相应事件的监听器即可。但 Spring 官方并不推荐在事件监听器中运行潜在的耗时任务,因为这些监听器实际上都是在主线程上依次执行的,如果其中某个监听器的执行比较耗时,就会拖累整个 Spring Application 启动。

因此,Spring 官方推荐使用ApplicationRunnerCommandLineRuner接口完成某些需要在 Spring Application应用初始化后,但还未真正工作(对于Web应用来说就是执行Http响应)前需要完成的任务。

比如:

@Log4j2
@Configuration
public class WebConfig {
	// ...
	@Bean
    public ApplicationRunner applicationRunner() {
        return args -> log.debug("ApplicationRunner is called, args:%s".formatted(args));
    }

    @Bean
    public CommandLineRunner commandLineRunner() {
        return args -> log.debug("commandLineRunner is called, args:%s".formatted(Arrays.toString(args)));
    }
}

观察输出:

... AvailabilityChangeEvent is called, and now Application is lived.
... ApplicationRunner is called,args:
... commandLineRunner is called, args:[]
... ApplicationReadyEvent1 is called.
... ApplicationReadyEvent2 is called.

可以看到,就像上面的 Application 事件流程图中表述的,ApplicationRunnerCommandLineRuner都是在Application 处于活动状态后,以及ApplicationReadyEvent事件发生前被调用的。

命令行参数

ApplicationRunnerCommandLineRuner没有本质上的区别,唯一的区别是它们接收的参数类型不同:

@FunctionalInterface
public interface ApplicationRunner {
    void run(ApplicationArguments args) throws Exception;
}

@FunctionalInterface
public interface CommandLineRunner {
    void run(String... args) throws Exception;
}

查看源码就可以发现,实际上String... args就是ApplicationArguments.getSourceArgs()

public class SpringApplication {
    // ...
    private void callRunner(CommandLineRunner runner, ApplicationArguments args) {
        try {
            runner.run(args.getSourceArgs());
        } catch (Exception var4) {
            throw new IllegalStateException("Failed to execute CommandLineRunner", var4);
        }
    }
    // ...
}

ApplicationArguments实际上是通过封装 Java 的命令行参数获得的:

public class SpringApplication {
    public ConfigurableApplicationContext run(String... args) {
    	// ...
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // ...
    }
    // ...
}

DefaultApplicationArguments的主要源码:

public class DefaultApplicationArguments implements ApplicationArguments {
    private final DefaultApplicationArguments.Source source;
    private final String[] args;

    public DefaultApplicationArguments(String... args) {
        Assert.notNull(args, "Args must not be null");
        this.source = new DefaultApplicationArguments.Source(args);
        this.args = args;
    }

    public String[] getSourceArgs() {
        return this.args;
    }
    // ...
}

因此,使用封装后的ApplicationArguments作为命令行参数的ApplicationRuner相比CommandLineRuner在处理命令行参数时更灵活,比如:

@Bean
public ApplicationRunner applicationRunner() {
    return args -> {
        String s = Arrays.toString(args.getSourceArgs());
        log.debug("ApplicationRunner is called, source args:%s".formatted(s));
        log.debug("ApplicationRunner is called, non option args:%s".formatted(args.getNonOptionArgs()));
        log.debug("ApplicationRunner is called, option names:%s".formatted(args.getOptionNames()));
        log.debug("ApplicationRunner is called, option values:%s".formatted(args.getOptionValues("spring.profiles.active")));
    };
}

添加命令行参数并运行:

./java -jar D:\workspace\learn_spring_boot\ch37\ini-application\target\ini-application-0.0.1-SNAPSHOT.jar --spring.profiles.activ

可以看到类似下面的输出:

... source args:[--spring.profiles.active=test]
... non option args:[]
... option names:[spring.profiles.active]
... option values:[test]

可以看到,ApplicationArguments有以下用于处理命令行参数的方法:

  • getSourceArgs,用于获取原始的命令行参数(带--前缀)
  • getOptionNames,用于获取命令行参数的key(不带--前缀)
  • getOptionValues,用于获取指定参数的值(可能有多个)
  • getNonOptionArgs,获取非选项参数(不带key的)

并发

虽然 Spring 官方建议使用ApplicationRunerCommandLineRuner执行比较耗时的任务,但实际上查看源码就会发现,相应的代码依然是在主线程上执行,并没有采用并发,因此同样会拖慢整个 Application 的创建和初始化。

比如下面的示例:

@Log4j2
@Component
public class MyCommandLineRuner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        log.debug("MyCommandLineRuner is begin.");
        new LongTimeTask().run();
        log.debug("MyCommandLineRuner is end.");
    }

    private static class LongTimeTask implements Runnable {
        private Random random = new Random();

        @Override
        public void run() {
            int delay = random.nextInt(10) + 1;
            try {
                Thread.sleep((20 + delay) * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

这里的内嵌类LongTimeTask代表一个比较耗时的任务,具体用Thread.sleep进行模拟,在执行时会阻塞当前进程20~30秒。

最初的示例我们在主线程直接执行这个任务(new LongTimeTask().run()),这样做将导致应用启动后输出会卡在MyCommandLineRuner is begin.,之后很长时间才能看到后续的日志打印和输出。

如果有需要的话,当然可以用单独的线程来执行这个耗时任务,比如:

@Log4j2
@Component
public class MyCommandLineRuner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        log.debug("MyCommandLineRuner is begin.");
        new Thread(new LongTimeTask()).start();
        log.debug("MyCommandLineRuner is end.");
    }
    // ...
}

现在这个耗时任务并不会影响到主线程的执行,所有的事件日志都很多全部输出。

一切都看起来很美妙,但真的如此吗?

让我们回顾一下 Spring 的事件设置,事件及其监听器的调用是有着先后顺序的意义的,比如ContextRefreshedEvent会在ApplicationPreparedEvent之后以及ApplicationStartedEvent之前发生。

换言之,一个ContextRefreshedEvent监听器也应当在ApplicationStartedEvent事件发生前完成调用。

如果我们使用了多线程,就可能无法确保这一点。比如在上面这个示例中,显然后续的ApplicationReadyEventAvailabilityChangeEvent都已经触发,但CommandLineRunner触发的子线程依然没有完成调用。

如果应用中的后续监听器或者业务代码依赖于CommandLineRunner中的任务完成结果,就很可能出现问题。

我们可以通过同步主线程和子线程来解决这个问题:

@Log4j2
@Component
public class MyCommandLineRuner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        log.debug("MyCommandLineRuner is begin.");
        Thread thread = new Thread(new LongTimeTask());
        thread.start();
        thread.join();
        log.debug("MyCommandLineRuner is end.");
    }
    // ...
}

当然,这里实质上在效率方面已经“退化”成了单线程,这种情况下使用多线程是得不偿失的,反而可能造成性能浪费。但如果有多个耗时任务可以并行执行,此时就显得很有意义:

@Log4j2
@Component
public class MyCommandLineRuner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        log.debug("MyCommandLineRuner is begin.");
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new LongTimeTask());
            thread.start();
            threads.add(thread);
        }
        for (Thread t : threads) {
            t.join();
        }
        log.debug("MyCommandLineRuner is end.");
    }
    // ...
}

当然,这里只是一个最简单的演示,实际使用中使用线程池和Future会更好。

关于 Java 并发的更多内容,可以阅读我的系列文章:

  • Java学习笔记21:并发(1) - 红茶的个人站点 (icexmoon.cn)
  • Java学习笔记22:并发(2) - 红茶的个人站点 (icexmoon.cn)
  • Java编程笔记23:并发(3) - 红茶的个人站点 (icexmoon.cn)
  • Java编程笔记24:并发(4) - 红茶的个人站点 (icexmoon.cn)

总结一下,在 Application 事件监听或者 CommandLineRunner、ApplicationRunner 中使用多线程需要额外注意,要明确这里的子线程处理结果会不会影响到后续事件监听或者程序运行,如果是,要么放弃使用多线程,要么进行线程同步。

从这个角度考虑,或许 Spring 框架在这里没有使用多线程调用是有意为之。

The End,谢谢阅读。

本文的所有示例代码可以从这里获取。

参考资料

  • 从零开始 Spring Boot 27:IoC - 红茶的个人站点 (icexmoon.cn)
  • 核心特性 (springdoc.cn)
  • SpringBoot中CommandLineRunner的源码实现
  • ApplicationRunner (Spring Boot 3.1.0 API)
  • CommandLineRunner (Spring Boot 3.1.0 API)
  • ApplicationArguments (Spring Boot 3.1.0 API)
  • 使用Spring Boot 的CommandLineRunner遇到的坑

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

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

相关文章

[PyTorch][chapter 39][nn.Module]

前言&#xff1a; pytorch.nn是专门为神经网络设计的模块化接口. nn构建于autograd之上,可以用来定义和运行神经网络.是所有类的父类. 目录&#xff1a; 基本结构 常用模块 container(容器&#xff09; CPU,GPU 部署 train-test 环境切换 flatten MyLinear 一…

【kali】设置系统方式为中文

目录 1、右击终端 2、输入命令回车 3、需要输入当前账户密码 4、选择语言 5、再次确定 6、输入命令重启 1、右击终端 2、输入命令回车 sudo dpkg-reconfigure locales 3、需要输入当前账户密码 4、选择语言 依次选中en_US.UTF-8 / zh_CN.GBK&#xff08;没找到&#xf…

野火启明RenesasRA4M2 UDS诊断bootloader 升级MCU

基于can总线的UDS软件升级 最近学习UDS诊断协议&#xff08;ISO14229&#xff09;&#xff0c;是一项国际标准&#xff0c;为汽车电子系统中的诊断通信定义了统一的协议和服务。它规定了与诊断相关的服务需求&#xff0c;并没有设计通信机制。ISO14229仅对应用层和会话层做出了…

微信一天可以加多少个好友?

微信作为最大的私域流量池&#xff0c;几乎所有的人都会往微信引流&#xff0c;而微信每天加好友数量是有严格限制的。微信每天加多少人不会封号&#xff1f;微信每天加多少好友才不会被限制&#xff1f;微信频繁加好友被限制怎么办&#xff1f;请跟随小编的脚步一起往下看吧。…

IP地址定位原理

IP地址定位是一种通过IP地址来确定位置的技术&#xff0c;在互联网和移动网络的应用十分广泛。本文将介绍IP地址定位的原理和实现方式。 IP地址定位原理 IP地址是Internet Protocol&#xff08;简称IP&#xff09;的缩写&#xff0c;是互联网上的一个地址标识符用于识别连接到…

合成化学物:169219-08-3,Fmoc-Thr(Ac4Manα)-OH,一种甘露糖苏氨酸

Fmoc-Thr(Ac4Manα)-OH&#xff0c;甘露糖苏氨酸&#xff0c;供应商&#xff1a;陕西新研博美生物科技有限公司产品结构式&#xff1a; 产品规格&#xff1a; 1.CAS号&#xff1a;169219-08-3 2.分子式&#xff1a;C33H37NO14 3.分子量&#xff1a;671.65 4.包装规格&#xff1…

K8s in Action 阅读笔记——【14】Securing cluster nodes and the network

K8s in Action 阅读笔记——【14】Securing cluster nodes and the network 迄今为止&#xff0c;创建了 Pod 而不考虑它们允许消耗多少 CPU 和内存。但是&#xff0c;正如将在本章中看到的那样&#xff0c;设置 Pod 预期消耗和允许消耗的最大数量是任何 Pod 定义的重要部分。…

如何进行JMeter分布式压测?一个案例教你详细解读!

目录 引言 一、什么是压力测试&#xff1f; 二、什么是分布式测试&#xff1f; 三、为什么要使用分布式压力测试&#xff1f; 四、主流压力测试工具对比 五、Jmeter分布式压测原理 六、Jmeter分布式压测前的准备工作 七、阿里云服务器上进行分布式压测 八、系统架构学…

ATTCK v13版本战术介绍——凭证访问(二)

一、引言 在前几期文章中我们介绍了ATT&CK中侦察、资源开发、初始访问、执行、持久化、提权、防御规避战术&#xff0c;本期我们为大家介绍ATT&CK 14项战术中凭证访问战术第7-12种子技术&#xff0c;后续会介绍凭证访问其他子技术&#xff0c;敬请关注。 二、ATT&…

这两个小众的资源搜索工具其实很好用

01 小不点搜索是一个中国网络技术公司开发的网盘搜索引擎&#xff0c;该网站通过与多个主流网盘进行整合&#xff0c;为用户提供一种快速查找和下载文件的方式。小不点搜索因其高效性、便利性和实用性受到了广大用户的喜爱。 在技术实现上&#xff0c;小不点搜索拥有先进的搜…

C++项目打包成可调用dll文件python调用

目录 1.原项目如图 2.直接在项目对应地方新增dll.h,dll.cpp 3.改变工程的配置类型---动态库(.dll) 4.生成解决方案----可调用dll文件 5.查找dll依赖的其他dll 6.python调用dll 7.python调用dll打包成exe 相关dll要放一个文件夹 1.原项目如图 包括头文件uiaccess.h&#xff0…

Linux中Crontab(定时任务)命令详解及使用教程

Crontab介绍&#xff1a;Linux crontab是用来crontab命令常见于Unix和类Unix的操作系统之中&#xff0c;用于设置周期性被执行的指令。该命令从标准输入设备读取指令&#xff0c;并将其存放于“crontab”文件中&#xff0c;以供之后读取和执行。该词来源于希腊语 chronos(χρ?…

【云计算】Ubuntu多种安装docker方式

文章目录 前言一、docker官网二、安装docker1、第一种方式&#xff08;官方&#xff09;2、使用脚本安装&#xff08;阿里云&#xff09;&#xff1a;3、使用官方脚本安装&#xff1a;拉取镜像&#xff08;solo博客部署&#xff09; 前言 Docker是一款开源的容器化平台&#x…

Misc(4)

RAR 打开是个加密压缩文件&#xff0c;给了提示说是4位纯数字加密 暴力破解为4位数字&#xff0c;获得flag QR 下载下来是一个二维码 很简单&#xff0c;利用工具一下就解出来了 镜子里的世界 打开后是一张图片&#xff0c;根据题目的提示&#xff0c;猜想可能是镜像翻转后得到…

java springboot整合MyBatis做数据库查询操作

首先 我们还是要搞清楚 MyBatis 工作中都需要那些东西 首先是基础配置 你要连哪里的数据 连什么类型的数据库 以什么权限去连 然后 以 注解还是xml方式去映射sql 好 我们直接上代码 我们先创建一个文件夹 然后打开idea 新建一个项目 然后 按我下图的操作配置一下 然后点下一…

小米秋招笔试题(强化基础)

1、已知const arr [A, B, C, D, E, F, G]&#xff0c;下面可以获取数组最后一项的表达式有 A arr[6] B arr.pop() C arr.shift() D arr.unshift() 答案&#xff1a; AB 解析&#xff1a; shift() 方法用于把数组的第一个元素从其中删除&#xff0c;并返回第一个元素的值。…

Python自动化测试框架:Pytest和Unittest的区别

pytest和unittest是Python中常用的两种测试框架&#xff0c;它们都可以用来编写和执行测试用例&#xff0c;但两者在很多方面都有所不同。本文将从不同的角度来论述这些区别&#xff0c;以帮助大家更好地理解pytest和unittest。 1. 原理 pytest是基于Python的assert语句和Pyth…

leetcode 124.二叉树中的最大路径和

1.题目 二叉树中的 路径 被定义为一条节点序列&#xff0c;序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点&#xff0c;且不一定经过根节点。 路径和 是路径中各节点值的总和。 给你一个二叉树的根节点 root &…

【Spring Cloud】Spring Cloud Alibaba-- 分布式事务Seata原理

文章目录 前言一、Seata 介绍1.1、Seata 简介1.2、Seata 的核心组件1.3、Seata 的整体执行流程 二、Seata 的 AT 模式原理2.1、AT 模式的整体执行流程2.2、AT 模式两阶段详细流程2.2.1、第一阶段的详细执行流程2.2.2、第二阶段提交的详细执行流程2.2.3、第二阶段回滚的详细执行…

独家揭秘:Kotlin K2编译器的前世今生

独家揭秘&#xff1a;Kotlin K2编译器的前世今生 也许您已经观看了最近的 KotlinConf 2023 主题演讲&#xff0c;关于 K2 编译器的更新。什么是 K2 编译器&#xff1f; 在搞清楚这个问题之前&#xff0c;我们需要了解Kotlin 使用的不同种类的编译器及其差异&#xff0c;以及编…