七種方式,教你在 SpringBoot 初始化時搞點事情!
我們經常需要在容器啓動的時候做一些鉤子動作,比如註冊消息消費者,監聽配置等,今天就總結下SpringBoot
留給開發者的 7 個啓動擴展點。
容器刷新完成擴展點
1、監聽容器刷新完成擴展點ApplicationListener<ContextRefreshedEvent>
基本用法
熟悉Spring
的同學一定知道,容器刷新成功意味着所有的Bean
初始化已經完成,當容器刷新之後Spring
將會調用容器內所有實現了ApplicationListener<ContextRefreshedEvent>
的Bean
的onApplicationEvent
方法,應用程序可以以此達到監聽容器初始化完成事件的目的。
1@Component
2public class StartupApplicationListenerExample implements
3 ApplicationListener<ContextRefreshedEvent> {
4
5 private static final Logger LOG
6 = Logger.getLogger(StartupApplicationListenerExample.class);
7
8 public static int counter;
9
10 @Override public void onApplicationEvent(ContextRefreshedEvent event) {
11 LOG.info("Increment counter");
12 counter++;
13 }
14}
15
16
易錯的點
這個擴展點用在web
容器中的時候需要額外注意,在 web 項目中(例如spring mvc
),系統會存在兩個容器,一個是root application context
, 另一個就是我們自己的context
(作爲root application context
的子容器)。如果按照上面這種寫法,就會造成onApplicationEvent
方法被執行兩次。解決此問題的方法如下:
1@Component
2public class StartupApplicationListenerExample implements
3 ApplicationListener<ContextRefreshedEvent> {
4
5 private static final Logger LOG
6 = Logger.getLogger(StartupApplicationListenerExample.class);
7
8 public static int counter;
9
10 @Override public void onApplicationEvent(ContextRefreshedEvent event) {
11 if (event.getApplicationContext().getParent() == null) {
12 // root application context 沒有parent
13 LOG.info("Increment counter");
14 counter++;
15 }
16 }
17}
18
19
高階玩法
當然這個擴展還可以有更高階的玩法:自定義事件,可以藉助Spring
以最小成本實現一個觀察者模式:
- 先自定義一個事件:
1public class NotifyEvent extends ApplicationEvent {
2 private String email;
3 private String content;
4 public NotifyEvent(Object source) {
5 super(source);
6 }
7 public NotifyEvent(Object source, String email, String content) {
8 super(source);
9 this.email = email;
10 this.content = content;
11 }
12 // 省略getter/setter方法
13}
14
15
- 註冊一個事件監聽器
1@Component
2public class NotifyListener implements ApplicationListener<NotifyEvent> {
3
4 @Override
5 public void onApplicationEvent(NotifyEvent event) {
6 System.out.println("郵件地址:" + event.getEmail());
7 System.out.println("郵件內容:" + event.getContent());
8 }
9}
10
11
- 發佈事件
1@RunWith(SpringRunner.class)
2@SpringBootTest
3public class ListenerTest {
4 @Autowired
5 private WebApplicationContext webApplicationContext;
6
7 @Test
8 public void testListener() {
9 NotifyEvent event = new NotifyEvent("object", "abc@qq.com", "This is the content");
10 webApplicationContext.publishEvent(event);
11 }
12}
13
14
- 執行單元測試可以看到郵件的地址和內容都被打印出來了
2、SpringBoot
的CommandLineRunner
接口
當容器上下文初始化完成之後,SpringBoot
也會調用所有實現了CommandLineRunner
接口的run
方法,下面這段代碼可起到和上文同樣的作用:
1@Component
2public class CommandLineAppStartupRunner implements CommandLineRunner {
3 private static final Logger LOG =
4 LoggerFactory.getLogger(CommandLineAppStartupRunner.class);
5
6 public static int counter;
7
8 @Override
9 public void run(String...args) throws Exception {
10 LOG.info("Increment counter");
11 counter++;
12 }
13}
14
15
對於這個擴展點的使用有額外兩點需要注意:
-
多個實現了
CommandLineRunner
的Bean
的執行順序可以根據Bean
上的@Order
註解調整 -
其
run
方法可以接受從控制檯輸入的參數,跟ApplicationListener<ContextRefreshedEvent>
這種擴展相比,更加靈活
1// 從控制檯輸入參數示例
2java -jar CommandLineAppStartupRunner.jar abc abcd
3
4
3、SpringBoot
的ApplicationRunner
接口
這個擴展和SpringBoot
的CommandLineRunner
接口的擴展類似,只不過接受的參數是一個ApplicationArguments
類,對控制檯輸入的參數提供了更好的封裝,以--
開頭的被視爲帶選項的參數,否則是普通的參數
1@Component
2public class AppStartupRunner implements ApplicationRunner {
3 private static final Logger LOG =
4 LoggerFactory.getLogger(AppStartupRunner.class);
5
6 public static int counter;
7
8 @Override
9 public void run(ApplicationArguments args) throws Exception {
10 LOG.info("Application started with option names : {}",
11 args.getOptionNames());
12 LOG.info("Increment counter");
13 counter++;
14 }
15}
16
17
比如:
1java -jar CommandLineAppStartupRunner.jar abc abcd --autho=mark verbose
2
3
Bean
初始化完成擴展點
前面的內容總結了針對容器初始化的擴展點,在有些場景,比如監聽消息的時候,我們希望Bean
初始化完成之後立刻註冊監聽器,而不是等到整個容器刷新完成,Spring
針對這種場景同樣留足了擴展點:
1、@PostConstruct
註解
@PostConstruct
註解一般放在Bean
的方法上,被@PostConstruct
修飾的方法會在Bean
初始化後馬上調用:
1@Component
2public class PostConstructExampleBean {
3
4 private static final Logger LOG
5 = Logger.getLogger(PostConstructExampleBean.class);
6
7 @Autowired
8 private Environment environment;
9
10 @PostConstruct
11 public void init() {
12 LOG.info(Arrays.asList(environment.getDefaultProfiles()));
13 }
14}
15
16
2、 InitializingBean
接口
InitializingBean
的用法基本上與@PostConstruct
一致,只不過相應的Bean
需要實現afterPropertiesSet
方法
1@Component
2public class InitializingBeanExampleBean implements InitializingBean {
3
4 private static final Logger LOG
5 = Logger.getLogger(InitializingBeanExampleBean.class);
6
7 @Autowired
8 private Environment environment;
9
10 @Override
11 public void afterPropertiesSet() throws Exception {
12 LOG.info(Arrays.asList(environment.getDefaultProfiles()));
13 }
14}
15
16
3、@Bean
註解的初始化方法
通過@Bean
注入Bean
的時候可以指定初始化方法:
Bean
的定義
1public class InitMethodExampleBean {
2
3 private static final Logger LOG = Logger.getLogger(InitMethodExampleBean.class);
4
5 @Autowired
6 private Environment environment;
7
8 public void init() {
9 LOG.info(Arrays.asList(environment.getDefaultProfiles()));
10 }
11}
12
13
14
Bean
注入
1@Bean(initMethod="init")
2public InitMethodExampleBean initMethodExampleBean() {
3 return new InitMethodExampleBean();
4}
5
6
4、通過構造函數注入
Spring
也支持通過構造函數注入,我們可以把搞事情的代碼寫在構造函數中,同樣能達到目的
1@Component
2public class LogicInConstructorExampleBean {
3
4 private static final Logger LOG
5 = Logger.getLogger(LogicInConstructorExampleBean.class);
6
7 private final Environment environment;
8
9 @Autowired
10 public LogicInConstructorExampleBean(Environment environment) {
11 this.environment = environment;
12 LOG.info(Arrays.asList(environment.getDefaultProfiles()));
13 }
14}
15
16
Bean
初始化完成擴展點執行順序?
可以用一個簡單的測試:
1@Component
2@Scope(value = "prototype")
3public class AllStrategiesExampleBean implements InitializingBean {
4
5 private static final Logger LOG
6 = Logger.getLogger(AllStrategiesExampleBean.class);
7
8 public AllStrategiesExampleBean() {
9 LOG.info("Constructor");
10 }
11
12 @Override
13 public void afterPropertiesSet() throws Exception {
14 LOG.info("InitializingBean");
15 }
16
17 @PostConstruct
18 public void postConstruct() {
19 LOG.info("PostConstruct");
20 }
21
22 public void init() {
23 LOG.info("init-method");
24 }
25}
26
27
實例化這個Bean
後輸出:
1[main] INFO o.b.startup.AllStrategiesExampleBean - Constructor
2[main] INFO o.b.startup.AllStrategiesExampleBean - PostConstruct
3[main] INFO o.b.startup.AllStrategiesExampleBean - InitializingBean
4[main] INFO o.b.startup.AllStrategiesExampleBean - init-method
5
6
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/68Wq3-7xJrB99yIAzhI6WA