Spring Boot 应用在启动时执行代码的五种方式(转)
原文:https://cloud.tencent.com/developer/article/1562471
作者:日拱一兵
前言
有时候我们需要在应用启动时执行一些代码片段,这些片段可能是仅仅是为了记录 log,也可能是在启动时检查与安装证书 ,诸如上述业务要求我们可能会经常碰到
Spring Boot 提供了至少 5 种方式用于在应用启动时执行代码。我们应该如何选择?本文将会逐步解释与分析这几种不同方式
1. 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
就足够使用了
2. 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
3. ApplicationListener<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 生命周期三部曲:
那么你会对下面两种方式非常熟悉了
4. @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 的初始化逻辑;
5. 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 启动过程及原理都可以从这个类中找到一些答案
总结
最后画一张图用来总结这几种方式(阅读原文查看高清大图)
灵魂追问
- 上面程序运行结果,
afterPropertiesSet
方法调用先于@PostConstruct
方法,但这和我们在 Spring Bean 生命周期之缘起 中的调用顺序恰恰相反,你知道为什么吗? MyPostConstructBean
通过@DependsOn("myApplicationListener")
依赖了 MyApplicationListener,为什么调用结果前者先与后者呢?- 为什么不建议
@Autowired
形式依赖注入
在写 Spring Bean 生命周期时就有朋友问我与之相关的问题,显然他们在概念上有一些含混,所以,仔细理解上面的问题将会帮助你加深对 Spring Bean 生命周期的理解
相关文章
- CSE 支持spring 4/5 以及spring boot 1/2 maven组件依赖关系配置参考
- Spring Boot Serverless 实战系列“部署篇” | Mall 应用
- Spring Boot 2.x :通过 spring-boot-starter-hbase 集成 HBase
- Spring Boot 应用部署流程
- spring boot:shardingsphere+druid整合seata分布式事务(spring boot 2.3.3)
- spring boot:redis+lua实现顺序自增的唯一id发号器(spring boot 2.3.1)
- 用 Docker、Gradle 来构建、运行、发布一个 Spring Boot 应用
- Spring Boot (九): 微服务应用监控 Spring Boot Actuator 详解
- 大叔问题定位分享(35)spring中session失效时间
- spring boot:从yaml配置文件中读取数据的四种方式(spring boot 2.4.3)
- spring boot:方法中使用try...catch导致@Transactional事务无效的解决(spring boot 2.3.4)
- spring boot:用redis+lua限制短信验证码的发送频率(spring boot 2.3.2)
- 利用神器BTrace 追踪线上 Spring Boot应用运行时信息
- Spring Boot应用的测试——Mockito
- Spring Boot应用的健康监控
- Spring Boot 应用使用 application.yml 和 application.properties 的区别
- 通过JMX监控Spring Boot应用
- 为什么Spring Boot推荐使用logback-spring.xml来替代logback.xml来配置logback日志的问题分析
- 探索Spring和Spring Boot的异同:从入门到精准,快速掌握双方的区域和应用场景
- 已解决:解决 Spring Boot 多线程环境下,多个定时器冲突问题
- 基于Spring Boot 2.0的IoT应用集成和使用CSE实践
- spring boot 启动警告 WARN 15684 --- [ restartedMain] c.n.c.sources.URLConfigurationSource : No URLs will be polled as dynamic configuration sources. 解决
- React.js 集成 Kotlin Spring Boot 开发 Web 应用实例详解
- Spring Boot 项目打包问题集锦: jar依赖多出boot-inf 文件夹问题/多环境动态打包/缺少BOOT-INF目录问题等...
- 019-Spring Boot 日志
- 008-Spring Boot @EnableAutoConfiguration深入分析、内部如何使用EnableAutoConfiguration
- Java SpringBoot 应用使用命令行 mvn spring-boot run 启动的原理
- Spring Boot启动报错,log4j2日志依赖冲突,报错提示:log4j-slf4j-impl cannot be present with log4j-to-slf4j
- spring-boot-starter-actuator与应用监控
- 【java】Spring Boot启动流程