[TOC]
我们经常需要在容器启动的时候做一些钩子动作,比如注册消息消费者,监听配置等,今天就总结下`SpringBoot`留给开发者的7个启动扩展点。
## 容器刷新完成扩展点
### 1、监听容器刷新完成扩展点`ApplicationListener<ContextRefreshedEvent>`
#### 基本用法
熟悉`Spring`的同学一定知道,容器刷新成功意味着所有的`Bean`初始化已经完成,当容器刷新之后`Spring`将会调用容器内所有实现了`ApplicationListener<ContextRefreshedEvent>`的`Bean`的`onApplicationEvent`方法,应用程序可以以此达到监听容器初始化完成事件的目的。
```java
@Component
public class StartupApplicationListenerExample implements
ApplicationListener<ContextRefreshedEvent> {
private static final Logger LOG
= Logger.getLogger(StartupApplicationListenerExample.class);
public static int counter;
@Override public void onApplicationEvent(ContextRefreshedEvent event) {
LOG.info("Increment counter");
counter++;
}
}
```
#### 易错的点
这个扩展点用在`web`容器中的时候需要额外注意,在web 项目中(例如`spring mvc`),系统会存在两个容器,一个是`root application context`,另一个就是我们自己的`context`(作为`root application context`的子容器)。如果按照上面这种写法,就会造成`onApplicationEvent`方法被执行两次。解决此问题的方法如下:
~~~go
@Component
public class StartupApplicationListenerExample implements
ApplicationListener<ContextRefreshedEvent> {
private static final Logger LOG
= Logger.getLogger(StartupApplicationListenerExample.class);
public static int counter;
@Override public void onApplicationEvent(ContextRefreshedEvent event) {
if (event.getApplicationContext().getParent() == null) {
// root application context 没有parent
LOG.info("Increment counter");
counter++;
}
}
}
~~~
#### 高阶玩法
当然这个扩展还可以有更高阶的玩法:**自定义事件**,可以借助`Spring`以最小成本实现一个观察者模式:
* 先自定义一个事件:
~~~go
public class NotifyEvent extends ApplicationEvent {
private String email;
private String content;
public NotifyEvent(Object source) {
super(source);
}
public NotifyEvent(Object source, String email, String content) {
super(source);
this.email = email;
this.content = content;
}
// 省略getter/setter方法
}
~~~
* 注册一个事件监听器
~~~go
@Component
public class NotifyListener implements ApplicationListener<NotifyEvent> {
@Override
public void onApplicationEvent(NotifyEvent event) {
System.out.println("邮件地址:" + event.getEmail());
System.out.println("邮件内容:" + event.getContent());
}
}
~~~
* 发布事件
~~~go
@RunWith(SpringRunner.class)
@SpringBootTest
public class ListenerTest {
@Autowired
private WebApplicationContext webApplicationContext;
@Test
public void testListener() {
NotifyEvent event = new NotifyEvent("object", "abc@qq.com", "This is the content");
webApplicationContext.publishEvent(event);
}
}
~~~
* 执行单元测试可以看到邮件的地址和内容都被打印出来了
### 2、`SpringBoot`的`CommandLineRunner`接口
当容器上下文初始化完成之后,`SpringBoot`也会调用所有实现了`CommandLineRunner`接口的`run`方法,下面这段代码可起到和上文同样的作用:
~~~go
@Component
public class CommandLineAppStartupRunner implements CommandLineRunner {
private static final Logger LOG =
LoggerFactory.getLogger(CommandLineAppStartupRunner.class);
public static int counter;
@Override
public void run(String...args) throws Exception {
LOG.info("Increment counter");
counter++;
}
}
~~~
对于这个扩展点的使用有额外两点需要注意:
* 多个实现了`CommandLineRunner`的`Bean`的执行顺序可以根据`Bean`上的`@Order`注解调整
* 其`run`方法可以接受从控制台输入的参数,跟`ApplicationListener<ContextRefreshedEvent>`这种扩展相比,更加灵活
~~~go
// 从控制台输入参数示例
java -jar CommandLineAppStartupRunner.jar abc abcd
~~~
### 3、`SpringBoot`的`ApplicationRunner`接口
这个扩展和`SpringBoot`的`CommandLineRunner`接口的扩展类似,只不过接受的参数是一个`ApplicationArguments`类,对控制台输入的参数提供了更好的封装,以`--`开头的被视为带选项的参数,否则是普通的参数
~~~go
@Component
public class AppStartupRunner implements ApplicationRunner {
private static final Logger LOG =
LoggerFactory.getLogger(AppStartupRunner.class);
public static int counter;
@Override
public void run(ApplicationArguments args) throws Exception {
LOG.info("Application started with option names : {}",
args.getOptionNames());
LOG.info("Increment counter");
counter++;
}
}
~~~
比如:
~~~go
java -jar CommandLineAppStartupRunner.jar abc abcd --autho=mark verbose
~~~
## 二、Bean初始化完成扩展点
前面的内容总结了针对容器初始化的扩展点,在有些场景,比如监听消息的时候,我们希望`Bean`初始化完成之后立刻注册监听器,而不是等到整个容器刷新完成,`Spring`针对这种场景同样留足了扩展点:
### 1、`@PostConstruct`注解
`@PostConstruct`注解一般放在`Bean`的方法上,被`@PostConstruct`修饰的方法会在`Bean`初始化后马上调用:
~~~go
@Component
public class PostConstructExampleBean {
private static final Logger LOG
= Logger.getLogger(PostConstructExampleBean.class);
@Autowired
private Environment environment;
@PostConstruct
public void init() {
LOG.info(Arrays.asList(environment.getDefaultProfiles()));
}
}
~~~
### 2、`InitializingBean`接口
`InitializingBean`的用法基本上与`@PostConstruct`一致,只不过相应的`Bean`需要实现`afterPropertiesSet`方法
~~~go
@Component
public class InitializingBeanExampleBean implements InitializingBean {
private static final Logger LOG
= Logger.getLogger(InitializingBeanExampleBean.class);
@Autowired
private Environment environment;
@Override
public void afterPropertiesSet() throws Exception {
LOG.info(Arrays.asList(environment.getDefaultProfiles()));
}
}
~~~
### 3、`@Bean`注解的初始化方法
通过`@Bean`注入`Bean`的时候可以指定初始化方法:
**`Bean`的定义**
~~~go
public class InitMethodExampleBean {
private static final Logger LOG = Logger.getLogger(InitMethodExampleBean.class);
@Autowired
private Environment environment;
public void init() {
LOG.info(Arrays.asList(environment.getDefaultProfiles()));
}
}
~~~
**`Bean`注入**
~~~go
@Bean(initMethod="init")
public InitMethodExampleBean initMethodExampleBean() {
return new InitMethodExampleBean();
}
~~~
### 4、通过构造函数注入
`Spring`也支持通过构造函数注入,我们可以把搞事情的代码写在构造函数中,同样能达到目的
~~~go
@Component
public class LogicInConstructorExampleBean {
private static final Logger LOG
= Logger.getLogger(LogicInConstructorExampleBean.class);
private final Environment environment;
@Autowired
public LogicInConstructorExampleBean(Environment environment) {
this.environment = environment;
LOG.info(Arrays.asList(environment.getDefaultProfiles()));
}
}
~~~
### Bean初始化完成扩展点执行顺序?
可以用一个简单的测试:
~~~go
@Component
@Scope(value = "prototype")
public class AllStrategiesExampleBean implements InitializingBean {
private static final Logger LOG
= Logger.getLogger(AllStrategiesExampleBean.class);
public AllStrategiesExampleBean() {
LOG.info("Constructor");
}
@Override
public void afterPropertiesSet() throws Exception {
LOG.info("InitializingBean");
}
@PostConstruct
public void postConstruct() {
LOG.info("PostConstruct");
}
public void init() {
LOG.info("init-method");
}
}
~~~
实例化这个`Bean`后输出:
~~~go
[main] INFO o.b.startup.AllStrategiesExampleBean - Constructor
[main] INFO o.b.startup.AllStrategiesExampleBean - PostConstruct
[main] INFO o.b.startup.AllStrategiesExampleBean - InitializingBean
[main] INFO o.b.startup.AllStrategiesExampleBean - init-method
~~~
> 文章转载自:https://blog.csdn.net/weixin_36380516/article/details/115388363
- 简介
- 更新说明
- 其他作品
- 第一部分 Java框架基础
- 第一章 Java基础
- 多线程实战
- 尝试一下Guava带返回值的多线程处理类ListenableFuture
- LocalDate和Date有什么区别
- JAVA8接口增强实践
- 第二章 Spring框架基础
- MVC究竟是个啥?
- @ApiImplicitParam
- 七种方式,教你在SpringBoot初始化时搞点事情!
- Spring事务状态
- maven
- Mybatis小总结
- mybatis-plus的使用
- 第三章 SpringSecurity实战
- 基于SpringSecurity+jwt的用户认证
- spring-security-oauth2
- 第四章 数据库
- mysql
- mysql授权
- mysql数据库三个关键性能指标--TPS\QPS\IOPS
- 梳理一下那些年Mysql的弱语法可能会踩的坑
- 关于Mysql的“字符串”数值的转换和使用
- 凭这一文咱把事务讲透
- Mysql性能优化
- 查询性能优化
- 不常用的一些语法
- elasticsearch
- elasticsearch文档操作
- 索引的基本操作
- java操作ElaticSearch
- elasticsearch中的各种查询
- DB与ES混合应用可能存在的问题及解决方案探索
- 使用es必须要知道的一些知识点:索引篇
- Es中的日期操作
- MongoDB
- 入门篇(了解非关系型数据库 NoSQL - MongoDB)
- 集群分片 (高级篇)
- 互联网大厂的建表规范
- 第五章 中间件
- nginx
- nginx动静分离配置,这个雷你踩过吗?
- Canal
- Sharding-jdbc
- 水平分库实践
- kafka
- 第六章 版本管理
- git
- Not currently on any branch 情况提交版本
- 第七章 IO编程
- 第八章 JVM实战调优
- jvisualvm
- jstat
- 第二部分 高级项目实战篇
- 第一章 微信开发实战
- 第二章 文件处理
- 使用EasyExcel处理导入导出
- 第三章 踩坑指南
- 邮件发送功能
- 第三部分 架构实战篇
- 第一章 架构实战原则
- 接口防止重复调用的一种方案
- 第二章 高并发缓存一致性管理办法
- 第三章 异地多活场景下的数据同步之道
- 第四章 用户体系
- 集成登录
- auth-sso的管理
- 第五章 分库分表场景
- 第六章 秒杀与高并发
- 秒杀场景
- 第七章 业务中台
- 中台的使用效果是怎样的?
- 通用黑白名单方案
- 第八章 领域驱动设计
- 第十一章 微服务实战
- Nacos多环境管理之道
- logback日志双写问题及Springboot项目正确的启动方式
- 第四部分 优雅的代码
- java中的链式编程
- 面向对象
- 开发原则
- Stream操作案例分享
- 注重性能的代码
- 第五部分 谈谈成长
- 新手入门指北
- 不可不知的调试技巧
- 构建自己的知识体系
- 我是如何做笔记的
- 有效的提问
- 谨防思维定势
- 学会与上级沟通
- 想清楚再去做
- 碎片化学习
- 第六部分 思维导图(付费)
- 技术基础篇
- 技术框架篇
- 数据存储篇
- 项目实战篇
- 第七部分 吾爱开源
- 7-1 麻雀聊天
- 项目启动
- 前端登录无请求问题解决
- websocket测试
- 7-2 ocp微服务框架
- evm框架集成
- 项目构建与集成
- zentao-center
- 二次开发:初始框架的搭建
- 二次开发:增加细分菜单、权限到应用
- 7-3 书栈网
- 项目启动
- 源码分析
- 我的书架
- 文章发布机制
- IM
- 第八章 团队管理篇
- 大厂是怎么运作的
- 第九章 码山有道
- 简历内推
- 联系我内推
- 第十章 学点前端
- Vue