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

这篇具有很好参考价值的文章主要介绍了从零开始 Spring Boot 37:初始化 ApplicationContext。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

图源:简书 (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,谢谢阅读。

本文的所有示例代码可以从这里获取。文章来源地址https://www.toymoban.com/news/detail-480834.html

参考资料

  • 从零开始 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遇到的坑

到了这里,关于从零开始 Spring Boot 37:初始化 ApplicationContext的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Spring Boot 源码学习】ConditionEvaluationReport 日志记录上下文初始化器

    《Spring Boot 源码学习系列》 上篇博文《共享 MetadataReaderFactory 上下文初始化器》, Huazie 带大家详细分析了 SharedMetadataReaderFactoryContextInitializer 。而在 spring-boot-autoconfigure 子模块中预置的上下文初始化器中,除了共享 MetadataReaderFactory 上下文初始化器,还有一个尚未分析。 那么

    2024年04月13日
    浏览(43)
  • Spring Boot进阶(52):Spring Boot 如何集成Flyway并初始化执行 SQL 脚本?| 超级详细,建议收藏

           在我们的认知中,我们会使用 SVN 或 Git 进行代码的版本管理。但是,我们是否好奇过,数据库也是需要进行版本管理的呢?         在每次发版的时候,我们可能都会对数据库的表结构进行新增和变更,又或者需要插入一些初始化的数据。而我们的环境不仅仅只

    2024年02月15日
    浏览(35)
  • 如何优雅地在Spring Boot项目启动时初始化数据,让你的Web应用快人一步

    🏅 欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正! 大多数Java Web应用程序中,项目在启动时都需要加载一些初始化数据,例如配置文件、数据库连接信息等。在Spring Boot中,我们可以通过将数据缓存到内存中来提高Web应用程序的性能。本篇博客旨在通过一个实例来介绍如

    2024年02月02日
    浏览(57)
  • 从零构建深度学习推理框架-2 从CSV文件初始化Tensor

    概念 CSV(逗号分隔值)文件是一种特殊的文件类型,可在 Excel 中创建或编辑。CSV文件采用逗号分隔的形式来存储文本和数字信息,总体来说,这种形式的文件格式具有扩展性好,移植性强的特点。 目前许多主流程序采用CSV文件作为数据导入导出的 中间格式 ,例如MySQL数据库

    2024年02月15日
    浏览(46)
  • 【树莓派初始化】教你从0开始搭建树莓派的使用环境

    为了完善本专栏的内容,这次我把树莓派的初始化配置也给大家加上。 干货满满,跟着我一步一步配置,从无到有玩转树莓派!😋 当然前提是你要有一个树莓派,2022年的树莓派价格可太魔幻了,涨价1倍,堪比显卡市场…… 不扯这些没用的了,本篇博客,带你走入树莓派这

    2024年02月04日
    浏览(51)
  • Spring初始化项目

    访问地址:https://start.spring.io idea配置:https://start.spring.io 访问地址:https://start.aliyun.com/bootstrap.html idea配置:https://start.aliyun.com 官网 阿里巴巴 版本 最新 稍旧 国内软件 大部分没有(mybatis plus) 有的支持(如:mybatis plus)

    2024年02月09日
    浏览(47)
  • spring事物初始化过程分析

    1.注入4个bd 2.执行 org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessBeforeInstantiation 逻辑分析:遍历所有的bd: 获取beanname-判断beanname是否有长度并且没有被处理过-是否遍历放入advisedBeans- 是否是基础类-是否该跳过  4大基础类 3.执行org.springframework.aop.framework.autoprox

    2024年02月02日
    浏览(38)
  • Spring Bean初始化方式

    对于Spring Bean 的初始化归纳了下,主要可以归纳一下三种方式 @PostConstruct 标注方法 自定义初始化方法 实现 initializingBean 接口的afterPropertiesSet()方法 执行顺序:Constructor @PostConstruct InitializingBean init-method @PostConstruct是Java自己的注解 假设类UserController有个成员变量UserService被**

    2024年02月01日
    浏览(45)
  • 从零实现一套低代码(保姆级教程) --- 【1】初始化项目,实现左侧组件列表

    低代码作为前端一个比较热门的方向,讨论度或者热度都算是拉满了。 如果你想了解低代码,可以在网上找一些相关的网站。像阿里等公司都有开源的项目和在线体验。 但是因为他们的代码比较牛逼,其实没有那么通俗易懂。 那博主是想,通过一系列文章的讲解,从零实现

    2024年02月04日
    浏览(60)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包