一张图帮你记忆,Spring Boot 应用在启动阶段执行代码的几种方式

简介: 一张图帮你记忆,Spring Boot 应用在启动阶段执行代码的几种方式

前言


有时候我们需要在应用启动时执行一些代码片段,这些片段可能是仅仅是为了记录 log,也可能是在启动时检查与安装证书 ,诸如上述业务要求我们可能会经常碰到


Spring Boot 提供了至少 5 种方式用于在应用启动时执行代码。我们应该如何选择?本文将会逐步解释与分析这几种不同方式


CommandLineRunner


CommandLineRunner 是一个接口,通过实现它,我们可以在 Spring 应用成功启动之后 执行一些代码片段


@Slf4j
@Component
@Order(2)
public class MyCommandLineRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        log.info("MyCommandLineRunner order is 2");
        if (args.length > 0){
            for (int i = 0; i < args.length; i++) {
                log.info("MyCommandLineRunner current parameter is: {}", args[i]);
            }
        }
    }
}


当 Spring Boot 在应用上下文中找到 CommandLineRunner bean,它将会在应用成功启动之后调用 run() 方法,并传递用于启动应用程序的命令行参数


通过如下 maven 命令生成 jar 包:


mvn clean package


通过终端命令启动应用,并传递参数:


java -jar springboot-application-startup-0.0.1-SNAPSHOT.jar --foo=bar --name=rgyb


查看运行结果:


微信图片_20220510155610.jpg


到这里我们可以看出几个问题:


  1. 命令行传入的参数并没有被解析,而只是显示出我们传入的字符串内容 --foo=bar--name=rgyb,我们可以通过 ApplicationRunner 解析,我们稍后看


  1. 在重写的 run() 方法上有 throws Exception 标记,Spring Boot 会将 CommandLineRunner 作为应用启动的一部分,如果运行 run() 方法时抛出 Exception,应用将会终止启动


  1. 我们在类上添加了 @Order(2) 注解,当有多个 CommandLineRunner 时,将会按照 @Order 注解中的数字从小到大排序 (数字当然也可以用复数)


⚠️不要使用 @Order 太多


看到 order 这个 "黑科技" 我们会觉得它可以非常方便将启动逻辑按照指定顺序执行,但如果你这么写,说明多个代码片段是有相互依赖关系的,为了让我们的代码更好维护,我们应该减少这种依赖使用


小结


如果我们只是想简单的获取以空格分隔的命令行参数,那 MyCommandLineRunner 就足够使用了


ApplicationRunner


上面提到,通过命令行启动并传递参数,MyCommandLineRunner 不能解析参数,如果要解析参数,那我们就要用到 ApplicationRunner 参数了


@Component
@Slf4j
@Order(1)
public class MyApplicationRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("MyApplicationRunner order is 1");
        log.info("MyApplicationRunner Current parameter is {}:", args.getOptionValues("foo"));
    }
}


重新打 jar 包,运行如下命令:


java -jar springboot-application-startup-0.0.1-SNAPSHOT.jar --foo=bar,rgyb


运行结果如下:


微信图片_20220510155740.jpg


到这里我们可以看出:


  1. MyCommandLineRunner 相似,但 ApplicationRunner 可以通过 run 方法的 ApplicationArguments 对象解析出命令行参数,并且每个参数可以有多个值在里面,因为 getOptionValues 方法返回 List 数组


  1. 在重写的 run() 方法上有 throws Exception 标记,Spring Boot 会将 CommandLineRunner 作为应用启动的一部分,如果运行 run() 方法时抛出 Exception,应用将会终止启动


  1. ApplicationRunner 也可以使用 @Order 注解进行排序,从启动结果来看,它与 CommandLineRunner 共享 order 的顺序,稍后我们通过源码来验证这个结论


小结


如果我们想获取复杂的命令行参数时,我们可以使用 ApplicationRunner


ApplicationListener


如果我们不需要获取命令行参数时,我们可以将启动逻辑绑定到 Spring 的 ApplicationReadyEvent


@Slf4j
@Component
@Order(0)
public class MyApplicationListener implements ApplicationListener<ApplicationReadyEvent> {
    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
        log.info("MyApplicationListener is started up");
    }
}


运行程序查看结果:


微信图片_20220510155823.jpg


到这我们可以看出:


  1. ApplicationReadyEvent当且仅当 在应用程序就绪之后才被触发,甚至是说上面的 Listener 要在本文说的所有解决方案都执行了之后才会被触发,最终结论请稍后看


  1. 代码中我用 Order(0) 来标记,显然 ApplicationListener 也是可以用该注解进行排序的,按数字大小排序,应该是最先执行。但是,这个顺序仅用于同类型的 ApplicationListener 之间的排序,与前面提到的 ApplicationRunnersCommandLineRunners 的排序并不共享


小结


如果我们不需要获取命令行参数,我们可以通过 ApplicationListener 创建一些全局的启动逻辑,我们还可以通过它获取 Spring Boot 支持的 configuration properties 环境变量参数

如果你看过我之前写的 Spring Bean 生命周期三部曲:





那么你会对下面两种方式非常熟悉了


@PostConstruct


创建启动逻辑的另一种简单解决方案是提供一种在 bean 创建期间由 Spring 调用的初始化方法。我们要做的就只是将 @PostConstruct 注解添加到方法中:


@Component
@Slf4j
@DependsOn("myApplicationListener")
public class MyPostConstructBean {
    @PostConstruct
    public void testPostConstruct(){
        log.info("MyPostConstructBean");
    }
}


查看运行结果:


微信图片_20220510160118.jpg


从上面运行结果可以看出:


  1. Spring 创建完 bean之后 (在启动之前),便会立即调用 @PostConstruct 注解标记的方法,因此我们无法使用 @Order 注解对其进行自由排序,因为它可能依赖于 @Autowired 插入到我们 bean 中的其他 Spring bean。


  1. 相反,它将在依赖于它的所有 bean 被初始化之后被调用,如果要添加人为的依赖关系并由此创建一个排序,则可以使用 @DependsOn 注解(虽然可以排序,但是不建议使用,理由和 @Order 一样)


小结


@PostConstruct 方法固有地绑定到现有的 Spring bean,因此应仅将其用于此单个 bean 的初始化逻辑;


InitializingBean


@PostConstruct 解决方案非常相似,我们可以实现 InitializingBean 接口,并让 Spring 调用某个初始化方法:


@Component
@Slf4j
public class MyInitializingBean implements InitializingBean {
    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("MyInitializingBean.afterPropertiesSet()");
    }
}


查看运行结果:


微信图片_20220510160257.jpg


从上面的运行结果中,我们得到了和 @PostConstruct 一样的效果,但二者还是有差别的


⚠️ @PostConstructafterPropertiesSet 区别


  1. afterPropertiesSet,顾名思义「在属性设置之后」,调用该方法时,该 bean 的所有属性已经被 Spring 填充。如果我们在某些属性上使用 @Autowired(常规操作应该使用构造函数注入),那么 Spring 将在调用afterPropertiesSet 之前将 bean 注入这些属性。但 @PostConstruct 并没有这些属性填充限制


  1. 所以 InitializingBean.afterPropertiesSet 解决方案比使用 @PostConstruct 更安全,因为如果我们依赖尚未自动注入的 @Autowired 字段,则 @PostConstruct 方法可能会遇到 NullPointerExceptions


小结


如果我们使用构造函数注入,则这两种解决方案都是等效的


源码分析


请打开你的 IDE (重点代码已标记注释):


MyCommandLineRunnerApplicationRunner 是在何时被调用的呢?


打开 SpringApplication.java 类,里面有 callRunners 方法


private void callRunners(ApplicationContext context, ApplicationArguments args) {
    List<Object> runners = new ArrayList<>();
    //从上下文获取 ApplicationRunner 类型的 bean
    runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
    //从上下文获取 CommandLineRunner 类型的 bean
    runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
    //对二者进行排序,这也就是为什么二者的 order 是可以共享的了
    AnnotationAwareOrderComparator.sort(runners);
    //遍历对其进行调用
    for (Object runner : new LinkedHashSet<>(runners)) {
        if (runner instanceof ApplicationRunner) {
            callRunner((ApplicationRunner) runner, args);
        }
        if (runner instanceof CommandLineRunner) {
            callRunner((CommandLineRunner) runner, args);
        }
    }
}


强烈建议完整看一下 SpringApplication.java 的全部代码,Spring Boot 启动过程及原理都可以从这个类中找到一些答案


总结



最后画一张图用来总结这几种方式(高清大图请查看原文:https://dayarch.top/p/spring-...


微信图片_20220510160353.png


灵魂追问


  1. 上面程序运行结果, afterPropertiesSet 方法调用先于 @PostConstruct 方法,但这和我们在 Spring Bean 生命周期之缘起 中的调用顺序恰恰相反,你知道为什么吗?


  1. MyPostConstructBean 通过 @DependsOn("myApplicationListener") 依赖了 MyApplicationListener,为什么调用结果前者先与后者呢?


  1. 为什么不建议 @Autowired 形式依赖注入

在写 Spring Bean 生命周期时就有朋友问我与之相关的问题,显然他们在概念上有一些含混,所以,仔细理解上面的问题将会帮助你加深对 Spring Bean 生命周期的理解

相关文章
|
29天前
|
安全 Java 应用服务中间件
Spring Boot + Java 21:内存减少 60%,启动速度提高 30% — 零代码
通过调整三个JVM和Spring Boot配置开关,无需重写代码即可显著优化Java应用性能:内存减少60%,启动速度提升30%。适用于所有在JVM上运行API的生产团队,低成本实现高效能。
180 3
|
1月前
|
SQL Java 数据库连接
Spring Data JPA 技术深度解析与应用指南
本文档全面介绍 Spring Data JPA 的核心概念、技术原理和实际应用。作为 Spring 生态系统中数据访问层的关键组件,Spring Data JPA 极大简化了 Java 持久层开发。本文将深入探讨其架构设计、核心接口、查询派生机制、事务管理以及与 Spring 框架的集成方式,并通过实际示例展示如何高效地使用这一技术。本文档约1500字,适合有一定 Spring 和 JPA 基础的开发者阅读。
197 0
|
2月前
|
监控 Java API
Spring Boot 3.2 结合 Spring Cloud 微服务架构实操指南 现代分布式应用系统构建实战教程
Spring Boot 3.2 + Spring Cloud 2023.0 微服务架构实践摘要 本文基于Spring Boot 3.2.5和Spring Cloud 2023.0.1最新稳定版本,演示现代微服务架构的构建过程。主要内容包括: 技术栈选择:采用Spring Cloud Netflix Eureka 4.1.0作为服务注册中心,Resilience4j 2.1.0替代Hystrix实现熔断机制,配合OpenFeign和Gateway等组件。 核心实操步骤: 搭建Eureka注册中心服务 构建商品
483 3
|
15天前
|
人工智能 监控 Java
零代码改造 + 全链路追踪!Spring AI 最新可观测性详细解读
Spring AI Alibaba 通过集成 OpenTelemetry 实现可观测性,支持框架原生和无侵入探针两种方式。原生方案依赖 Micrometer 自动埋点,适用于快速接入;无侵入探针基于 LoongSuite 商业版,无需修改代码即可采集标准 OTLP 数据,解决了原生方案扩展性差、调用链易断链等问题。未来将开源无侵入探针方案,整合至 AgentScope Studio,并进一步增强多 Agent 场景下的观测能力。
782 28
|
8天前
|
安全 Java 测试技术
《深入理解Spring》单元测试——高质量代码的守护神
Spring测试框架提供全面的单元与集成测试支持,通过`@SpringBootTest`、`@WebMvcTest`等注解实现分层测试,结合Mockito、Testcontainers和Jacoco,保障代码质量,提升开发效率与系统稳定性。
|
17天前
|
消息中间件 缓存 Java
Spring框架优化:提高Java应用的性能与适应性
以上方法均旨在综合考虑Java Spring 应该程序设计原则, 数据库交互, 编码实践和系统架构布局等多角度因素, 旨在达到高效稳定运转目标同时也易于未来扩展.
77 8
|
1月前
|
安全 IDE Java
Spring 的@FieldDefaults和@Data:Lombok 注解以实现更简洁的代码
本文介绍了如何在 Spring 应用程序中使用 Project Lombok 的 `@Data` 和 `@FieldDefaults` 注解来减少样板代码,提升代码可读性和可维护性,并探讨了其适用场景与限制。
Spring 的@FieldDefaults和@Data:Lombok 注解以实现更简洁的代码
|
2月前
|
Java 应用服务中间件 开发者
Spring Boot 技术详解与应用实践
本文档旨在全面介绍 Spring Boot 这一广泛应用于现代企业级应用开发的框架。内容将涵盖 Spring Boot 的核心概念、核心特性、项目自动生成与结构解析、基础功能实现(如 RESTful API、数据访问)、配置管理以及最终的构建与部署。通过本文档,读者将能够理解 Spring Boot 如何简化 Spring 应用的初始搭建和开发过程,并掌握其基本使用方法。
247 2
|
2月前
|
人工智能 监控 安全
Spring AOP切面编程颠覆传统!3大核心注解+5种通知类型,让业务代码纯净如初
本文介绍了AOP(面向切面编程)的基本概念、优势及其在Spring Boot中的使用。AOP作为OOP的补充,通过将横切关注点(如日志、安全、事务等)与业务逻辑分离,实现代码解耦,提升模块化程度、可维护性和灵活性。文章详细讲解了Spring AOP的核心概念,包括切面、切点、通知等,并提供了在Spring Boot中实现AOP的具体步骤和代码示例。此外,还列举了AOP在日志记录、性能监控、事务管理和安全控制等场景中的实际应用。通过本文,开发者可以快速掌握AOP编程思想及其实践技巧。
|
2月前
|
人工智能 监控 安全
如何快速上手【Spring AOP】?核心应用实战(上篇)
哈喽大家好吖~欢迎来到Spring AOP系列教程的上篇 - 应用篇。在本篇,我们将专注于Spring AOP的实际应用,通过具体的代码示例和场景分析,帮助大家掌握AOP的使用方法和技巧。而在后续的下篇中,我们将深入探讨Spring AOP的实现原理和底层机制。 AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的核心特性之一,它能够帮助我们解决横切关注点(如日志记录、性能统计、安全控制、事务管理等)的问题,提高代码的模块化程度和复用性。