七種方式教你在SpringBoot初始化時搞點事情

大家好,我是躍哥。清明假期已經悄然過去,相比往常就是多了周一這一天的休息,所以大家適應起來應該還蠻快的吧。
為了讓大家更好的適應工作,今天直接來主題,和大家聊聊 SpringBoot 初始化的那些事,沖鴨。
我們經常需要在容器啟動的時候做一些鉤子動作,比如注冊消息消費者,監(jiān)聽配置等,今天就總結下SpringBoot留給開發(fā)者的7個啟動擴展點。
容器刷新完成擴展點
1、監(jiān)聽容器刷新完成擴展點ApplicationListener<ContextRefreshedEvent>
基本用法
熟悉Spring的同學一定知道,容器刷新成功意味著所有的Bean初始化已經完成,當容器刷新之后Spring將會調用容器內所有實現(xiàn)了ApplicationListener<ContextRefreshedEvent>的Bean的onApplicationEvent方法,應用程序可以以此達到監(jiān)聽容器初始化完成事件的目的。
@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),系統(tǒng)會存在兩個容器,一個是root application context,另一個就是我們自己的context(作為root application context的子容器)。如果按照上面這種寫法,就會造成onApplicationEvent方法被執(zhí)行兩次。解決此問題的方法如下:
@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以最小成本實現(xiàn)一個觀察者模式:
先自定義一個事件:
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方法
}
注冊一個事件監(jiān)聽器
@Component
public class NotifyListener implements ApplicationListener<NotifyEvent> {
@Override
public void onApplicationEvent(NotifyEvent event) {
System.out.println("郵件地址:" + event.getEmail());
System.out.println("郵件內容:" + event.getContent());
}
}
發(fā)布事件
@RunWith(SpringRunner.class)
@SpringBootTest
public class ListenerTest {
@Autowired
private WebApplicationContext webApplicationContext;
@Test
public void testListener() {
NotifyEvent event = new NotifyEvent("object", "[email protected]", "This is the content");
webApplicationContext.publishEvent(event);
}
}
執(zhí)行單元測試可以看到郵件的地址和內容都被打印出來了
2、SpringBoot的CommandLineRunner接口
當容器上下文初始化完成之后,SpringBoot也會調用所有實現(xiàn)了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++;
}
}
對于這個擴展點的使用有額外兩點需要注意:
多個實現(xiàn)了 CommandLineRunner的Bean的執(zhí)行順序可以根據Bean上的@Order注解調整其 run方法可以接受從控制臺輸入的參數(shù),跟ApplicationListener<ContextRefreshedEvent>這種擴展相比,更加靈活
// 從控制臺輸入參數(shù)示例
java -jar CommandLineAppStartupRunner.jar abc abcd
3、SpringBoot的ApplicationRunner接口
這個擴展和SpringBoot的CommandLineRunner接口的擴展類似,只不過接受的參數(shù)是一個ApplicationArguments類,對控制臺輸入的參數(shù)提供了更好的封裝,以--開頭的被視為帶選項的參數(shù),否則是普通的參數(shù)
@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初始化完成擴展點
前面的內容總結了針對容器初始化的擴展點,在有些場景,比如監(jiān)聽消息的時候,我們希望Bean初始化完成之后立刻注冊監(jiān)聽器,而不是等到整個容器刷新完成,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需要實現(xiàn)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、通過構造函數(shù)注入
Spring也支持通過構造函數(shù)注入,我們可以把搞事情的代碼寫在構造函數(shù)中,同樣能達到目的
@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初始化完成擴展點執(zhí)行順序?
可以用一個簡單的測試:
@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


