一张图帮你记忆,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
查看运行结果:

到这里我们可以看出几个问题:
- 命令行传入的参数并没有被解析,而只是显示出我们传入的字符串内容
--foo=bar,--name=rgyb,我们可以通过ApplicationRunner解析,我们稍后看 - 在重写的
run()方法上有throws Exception标记,Spring Boot 会将CommandLineRunner作为应用启动的一部分,如果运行run()方法时抛出 Exception,应用将会终止启动 - 我们在类上添加了
@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
运行结果如下:

到这里我们可以看出:
- 同
MyCommandLineRunner相似,但ApplicationRunner可以通过 run 方法的ApplicationArguments对象解析出命令行参数,并且每个参数可以有多个值在里面,因为getOptionValues方法返回 List 数组 - 在重写的
run()方法上有throws Exception标记,Spring Boot 会将CommandLineRunner作为应用启动的一部分,如果运行run()方法时抛出 Exception,应用将会终止启动 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");
}
}
运行程序查看结果:

到这我们可以看出:
ApplicationReadyEvent当且仅当 在应用程序就绪之后才被触发,甚至是说上面的 Listener 要在本文说的所有解决方案都执行了之后才会被触发,最终结论请稍后看- 代码中我用
Order(0)来标记,显然 ApplicationListener 也是可以用该注解进行排序的,按数字大小排序,应该是最先执行。但是,这个顺序仅用于同类型的 ApplicationListener 之间的排序,与前面提到的ApplicationRunners和CommandLineRunners的排序并不共享
小结
如果我们不需要获取命令行参数,我们可以通过 ApplicationListener<ApplicationReadyEvent> 创建一些全局的启动逻辑,我们还可以通过它获取 Spring Boot 支持的 configuration properties 环境变量参数
如果你看过我之前写的 Spring Bean 生命周期三部曲:
那么你会对下面两种方式非常熟悉了
@PostConstruct
创建启动逻辑的另一种简单解决方案是提供一种在 bean 创建期间由 Spring 调用的初始化方法。我们要做的就只是将 @PostConstruct 注解添加到方法中:
@Component
@Slf4j
@DependsOn("myApplicationListener")
public class MyPostConstructBean {
@PostConstruct
public void testPostConstruct(){
log.info("MyPostConstructBean");
}
}
查看运行结果:

从上面运行结果可以看出:
- Spring 创建完 bean之后 (在启动之前),便会立即调用
@PostConstruct注解标记的方法,因此我们无法使用@Order注解对其进行自由排序,因为它可能依赖于@Autowired插入到我们 bean 中的其他 Spring bean。 - 相反,它将在依赖于它的所有 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()");
}
}
查看运行结果:

从上面的运行结果中,我们得到了和 @PostConstruct 一样的效果,但二者还是有差别的
⚠️
@PostConstruct和afterPropertiesSet区别
- afterPropertiesSet,顾名思义「在属性设置之后」,调用该方法时,该 bean 的所有属性已经被 Spring 填充。如果我们在某些属性上使用
@Autowired(常规操作应该使用构造函数注入),那么 Spring 将在调用afterPropertiesSet之前将 bean 注入这些属性。但@PostConstruct并没有这些属性填充限制- 所以
InitializingBean.afterPropertiesSet解决方案比使用@PostConstruct更安全,因为如果我们依赖尚未自动注入的@Autowired字段,则@PostConstruct方法可能会遇到 NullPointerExceptions
小结
如果我们使用构造函数注入,则这两种解决方案都是等效的
源码分析
请打开你的 IDE (重点代码已标记注释):
MyCommandLineRunner和ApplicationRunner是在何时被调用的呢?
打开 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-boot-execute-on-startup.html)

灵魂追问
- 上面程序运行结果,
afterPropertiesSet方法调用先于@PostConstruct方法,但这和我们在 Spring Bean 生命周期之缘起 中的调用顺序恰恰相反,你知道为什么吗? MyPostConstructBean通过@DependsOn("myApplicationListener")依赖了 MyApplicationListener,为什么调用结果前者先与后者呢?- 为什么不建议
@Autowired形式依赖注入
在写 Spring Bean 生命周期时就有朋友问我与之相关的问题,显然他们在概念上有一些含混,所以,仔细理解上面的问题将会帮助你加深对 Spring Bean 生命周期的理解
欢迎持续关注公众号:「日拱一兵」
- 前沿 Java 技术干货分享
- 高效工具汇总 | 回复「工具」
- 面试问题分析与解答
- 技术资料领取 | 回复「资料」
以读侦探小说思维轻松趣味学习 Java 技术栈相关知识,本着将复杂问题简单化,抽象问题具体化和图形化原则逐步分解技术问题,技术持续更新,请持续关注......

一张图帮你记忆,Spring Boot 应用在启动阶段执行代码的几种方式的更多相关文章
- 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件 内容简介:本文介绍 Spring Boot 的配置文件和配置管理,以及介绍了三种读取配置文 ...
- Spring Boot项目指定启动后执行的操作
Spring Boot项目指定启动后执行的操作: (1)实现CommandLineRunner 接口 (2)重写run方法 (3)声明执行顺序@Order(1),数值越小,优先级越高 (4)如果需要注 ...
- spring boot 项目在启动时执行指定sql文件
参考博客: https://www.jianshu.com/p/88125f1cf91c 1. 启动时执行 当有在项目启动时先执行指定的sql语句的需求时,可以在resources文件夹下添加需要执行 ...
- 上手spring boot项目(三)之spring boot整合mybatis进行增删改查的三种方式。
1.引入依赖. <!--springboot的web起步依赖--><dependency> <groupId>org.springframework.boot< ...
- spring boot, 容器启动后执行某操作
常有在spring容器启动后执行某些操作的需求,现做了一个demo的实现,做一下记录,也希望可以给需要的同学提供参考. 1.spring启动后,以新线程执行后续需要的操作,所以执行类实现Runnabl ...
- Spring Boot学习--项目启动时执行指定service的指定方法
Springboot给我们提供了两种“开机启动”某些方法的方式:ApplicationRunner和CommandLineRunner. 这两种方法提供的目的是为了满足,在项目启动的时候立刻执行某些方 ...
- Spring Boot学习--项目启动时执行特定方法
Springboot给我们提供了两种"开机启动"某些方法的方式:ApplicationRunner和CommandLineRunner. 这两种方法提供的目的是为了满足,在项目启动 ...
- Spring boot 梳理 - SpringBoot中注入ApplicationContext对象的三种方式
直接注入(Autowired) @Configuration public class OAConfig { @Autowired private ApplicationContext applica ...
- 产品经理-需求分析-用户故事-敏捷开发 详解 一张图帮你了解Scrum敏捷流程
产品经理-需求分析-用户故事-敏捷开发 详解 用户故事是从用户的角度来描述用户渴望得到的功能.一个好的用户故事包括三个要素:1. 角色:谁要使用这个功能.2. 活动:需要完成什么样的功能.3. 商业价 ...
随机推荐
- Java面向对象----继承概念,super关键字
继承概念: 继承需要符合的关系 is-a , 父类通用更抽象,子类更特殊更具体 类之间的关系 继承体现 组合体现 实现接口体现 继承的意义 代码重用 体现不同抽象层次 extends关键字 Sup ...
- osgi实战学习之路:4.Bundle
</pre></h1><h1 style="margin:0 0 0 40px; border:none; padding:0px"><p ...
- shell爬虫
#!/bin/bash curl_str='curl -x "http://http-pro.abuyun.com:9010" --proxy-basic --proxy-user ...
- 红帽Linux6虚拟机克隆后操作
1.首先需要修改root密码 开机后按2次e进入以下界面 按e编辑 在quiet后输入single 1 输入好了之后,“回车”,返回到了刚刚的界面,再输入“b”,让boot引导进入系统. 进入单用户模 ...
- div 禁止点击
今天需要这个需求,原来真的有 style="pointer-events: none;"
- CSS中的“>”是什么意思
#quickSummary p{color:red;} #quickSummary >p+p{color:red;} #quickSummary>p+p+p{color:inherit;} ...
- python不得不知的几个开源项目
1.Trac Trac拥有强大的bug管理 功能,并集成了Wiki 用于文档管理.它还支持代码管理工具Subversion ,这样可以在 bug管理和Wiki中方便地参考程序源代码. Trac有着比较 ...
- HDU 1114 完全背包问题
题意:有一个存钱罐,空罐时的重量是e,满罐时的重量是f,现在有n种硬币,每一种有无限个,现在给出每一种硬币的价值p和重量w,问存钱罐中最少钱,输出最小钱,否则输出... 思路:变形的完全背包问题,只是 ...
- springmvc 多文件/文件夹上传 下载
注入依赖 <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding&g ...
- Python--day27--几个内置方法:__repr__()/__str__()/__del__()/__call__()/__getitem__/__setitem/delitem/__new__/__eq__/__hash__
repr方法() 双下方法__str__: 打印对象就相当于打印对象.__str__ __repr__(): __repr__是__str__的备胎,没有__str__的时候,就调用__repr__: ...