⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 苦味代码 「苦味代码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

我们经常需要在容器启动的时候做一些钩子动作,比如注册消息消费者,监听配置等,今天就总结下SpringBoot留给开发者的7个启动扩展点。

容器刷新完成扩展点

1、监听容器刷新完成扩展点ApplicationListener<ContextRefreshedEvent>

基本用法

熟悉Spring的同学一定知道,容器刷新成功意味着所有的Bean初始化已经完成,当容器刷新之后Spring将会调用容器内所有实现了ApplicationListener<ContextRefreshedEvent>BeanonApplicationEvent方法,应用程序可以以此达到监听容器初始化完成事件的目的。

@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方法被执行两次。解决此问题的方法如下:

@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以最小成本实现一个观察者模式:

  • 先自定义一个事件:

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方法
}

  • 注册一个事件监听器

@Component
public class NotifyListener implements ApplicationListener<NotifyEvent> {

@Override
public void onApplicationEvent(NotifyEvent event) {
System.out.println("邮件地址:" + event.getEmail());
System.out.println("邮件内容:" + event.getContent());
}
}

  • 发布事件

@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、SpringBootCommandLineRunner接口

当容器上下文初始化完成之后,SpringBoot也会调用所有实现了CommandLineRunner接口的run方法,下面这段代码可起到和上文同样的作用:

@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++;
}
}

对于这个扩展点的使用有额外两点需要注意:

  • 多个实现了CommandLineRunnerBean的执行顺序可以根据Bean上的@Order注解调整
  • run方法可以接受从控制台输入的参数,跟ApplicationListener<ContextRefreshedEvent>这种扩展相比,更加灵活

// 从控制台输入参数示例
java -jar CommandLineAppStartupRunner.jar abc abcd

3、SpringBootApplicationRunner接口

这个扩展和SpringBootCommandLineRunner接口的扩展类似,只不过接受的参数是一个ApplicationArguments类,对控制台输入的参数提供了更好的封装,以--开头的被视为带选项的参数,否则是普通的参数

@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++;
}
}

比如:

java -jar CommandLineAppStartupRunner.jar abc abcd --autho=mark verbose

Bean初始化完成扩展点

前面的内容总结了针对容器初始化的扩展点,在有些场景,比如监听消息的时候,我们希望Bean初始化完成之后立刻注册监听器,而不是等到整个容器刷新完成,Spring针对这种场景同样留足了扩展点:

1、@PostConstruct注解

@PostConstruct注解一般放在Bean的方法上,被@PostConstruct修饰的方法会在Bean初始化后马上调用:

@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方法

@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的定义

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注入

@Bean(initMethod="init")
public InitMethodExampleBean initMethodExampleBean() {
return new InitMethodExampleBean();
}

4、通过构造函数注入

Spring也支持通过构造函数注入,我们可以把搞事情的代码写在构造函数中,同样能达到目的

@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初始化完成扩展点执行顺序?

可以用一个简单的测试:

@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后输出:

[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

文章目录
  1. 1. 容器刷新完成扩展点
    1. 1.1. 1、监听容器刷新完成扩展点ApplicationListener<ContextRefreshedEvent>
      1. 1.1.1. 基本用法
      2. 1.1.2. 易错的点
      3. 1.1.3. 高阶玩法
    2. 1.2. 2、SpringBoot的CommandLineRunner接口
    3. 1.3. 3、SpringBoot的ApplicationRunner接口
  2. 2. Bean初始化完成扩展点
    1. 2.1. 1、@PostConstruct注解
    2. 2.2. 2、 InitializingBean接口
    3. 2.3. 3、@Bean注解的初始化方法
    4. 2.4. 4、通过构造函数注入
    5. 2.5. Bean初始化完成扩展点执行顺序?