看標題,你可能並太可能知道我想要說什麼,但是肯定和這3個關鍵字有關係。
這一切都要從線上一個服務的報錯開始:
背景
線上報錯表現:服務啓動時候一個定時任務卡住了,進而導致服務起不來。
當時這個足足卡了有5分鐘多,可見並不是任務執行時間久(平時也就最多30s)
分析
連忙相辦法找運維同學把線上該服務的堆棧信息拿到手,發現有線程blocked,這個線程就是加載定時任務,去redis裏面get數據的。
如上圖可見,是最後獲取spring bean的時候死鎖了。
spring 初始化相關點
ApplicationRunner與CommandLineRunner接口
如果需要在SpringApplication啓動時執行一些特殊的代碼,你可以實現ApplicationRunner或CommandLineRunner接口,這兩個接口工作方式相同,都只提供單一的run方法,而且該方法僅在SpringApplication.run(…)完成之前調用,更準確的說是在構造SpringApplication實例完成之後調用run()的時候,具體分析見後文,所以這裏將他們分爲一類。
構造一個類實現ApplicationRunner接口
@Component
public class ApplicationRunnerTest implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("ApplicationRunner");
}
}
CommandLineRunner
對於這兩個接口而言,我們可以通過Order註解或者使用Ordered接口來指定調用順序,@Order()中的值越小,優先級越高
@Component
@Order(1)
public class CommandLineRunnerTest implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("CommandLineRunner...");
}
}
兩者的聯繫與區別
前面就提到過,兩個接口都有run()方法,只不過它們的參數不一樣,CommandLineRunner的參數是最原始的參數,沒有進行任何處理,ApplicationRunner的參數是ApplicationArguments,是對原始參數的進一步封裝
接下來我們簡要跟蹤一下源碼看ApplicationRunner(CommandLineRunner)是如何被調用的。
Springboot在啓動的時候,都會構造一個SpringApplication實例,至於這個實例怎麼構造的,這裏不去探究了,有感興趣的可以去看下源碼。這裏主要看ApplicationRunner是如何被調用的,而它的調用就是在SpringApplication這個實例調用run方法中。
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
進入run方法
public static ConfigurableApplicationContext run(Class<?> primarySource,
String... args) {
return run(new Class<?>[] { primarySource }, args);
}
執行SpringApplication的run方法
public static ConfigurableApplicationContext run(Class<?>[] primarySources,
String[] args) {
return new SpringApplication(primarySources).run(args);
}
發現對ApplicationRunner的調用實際上在callRunners方法中
對於CommandLineRunner或者ApplicationRunner來說,需要注意的兩點:
所有CommandLineRunner/ApplicationRunner的執行時點是在SpringBoot應用的ApplicationContext完全初始化開始工作之後,callRunners()可以看出是run方法內部最後一個調用的方法(可以認爲是main方法執行完成之前最後一步)
只要存在於當前SpringBoot應用的ApplicationContext中的任何CommandLineRunner/ApplicationRunner,都會被加載執行(不管你是手動註冊還是自動掃描去Ioc容器)
Spring Bean初始化的InitializingBean,init-method和PostConstruct
InitializingBean接口爲bean提供了初始化方法的方式,它只包括afterPropertiesSet()方法。
在spring初始化bean的時候,如果bean實現了InitializingBean接口,在對象的所有屬性被初始化後之後纔會調用afterPropertiesSet()方法
@Component
public class InitialingzingBeanTest implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("InitializingBean..");
}
}
當然,我們可以看出spring初始化bean肯定會在
ApplicationRunner和CommandLineRunner接口調用之前。
當然有一點我們要注意的是,儘管使用initialingBean接口可以實現初始化動作,但是官方並不建議我們使用InitializingBean接口,因爲它將你的代碼耦合在Spring代碼中,官方的建議是在bean的配置文件指定init-method方法,或者在@Bean中設置init-method屬性
init-method和@PostConstruct
前面就說過官方文檔上不建議使用InitializingBean接口,但是我們可以在元素的init-method屬性指定bean初始化之後的操作方法,或者在指定方法上加上@PostConstruct註解來制定該方法在初始化之後調用
Spring的事件機制
Spring的事件機制實際上是設計模式中觀察者模式的典型應用,在Head First 設計模式中是這樣定義觀察者模式的:
觀察者模式定義了一個一對多的依賴關係,讓一個或多個觀察者對象監聽一個主題對象。這樣一來,當被觀察者狀態改變時,需要通知相應的觀察者,使這些觀察者能夠自動更新
基礎概念
Spring的事件驅動模型由三部分組成
事件: ApplicationEvent,繼承自JDK的EventObject,所有事件都要繼承它,也就是被觀察者
事件發佈者: ApplicationEventPublisher及ApplicationEventMulticaster接口,使用這個接口,就可以發佈事件了
事件監聽者: ApplicationListener,繼承JDK的EventListener,所有監聽者都繼承它,也就是我們所說的觀察者,當然我們也可以使用註解 @EventListener,效果是一樣的
事件
在Spring框架中,默認對ApplicationEvent事件提供瞭如下支持:
ContextStartedEvent:ApplicationContext啓動後觸發的事件
ContextStoppedEvent:ApplicationContext停止後觸發的事件
ContextRefreshedEvent:ApplicationContext初始化或刷新完成後觸發的事件;(容器初始化完成後調用,所以我們可以利用這個事件做一些初始化操作)
ContextClosedEvent:ApplicationContext關閉後觸發的事件;(如web容器關閉時自動會觸發spring容器的關閉,如果是普通java應用,需要調用ctx.registerShutdownHook();註冊虛擬機關閉時的鉤子纔行)
構造一個類繼承ApplicationEvent
public class TestEvent extends ApplicationEvent {
private static final long serialVersionUID = -376299954511699499L;
private String message;
/**
* Create a new ApplicationEvent.
*
* @param source the object on which the event initially occurred (never {@code null})
*/
public TestEvent(Object source) {
super(source);
}
public void getMessage() {
System.out.println(message);
}
public void setMessage(String message) {
this.message = message;
}
}
創建事件監聽者
有兩種方法可以創建監聽者,一種是直接實現ApplicationListener的接口,一種是使用註解 @EventListener,註解是添加在監聽方法上的,下面的例子是直接實現的接口
@Component
public class ApplicationListenerTest implements ApplicationListener<TestEvent> {
@Override
public void onApplicationEvent(TestEvent testEvent)
{
testEvent.getMessage();
}
}
事件發佈
對於事件發佈,代表者是ApplicationEventPublisher和ApplicationEventMulticaster,系統提供的實現如下
ApplicationContext接口繼承了ApplicationEventPublisher,並在AbstractApplicationContext實現了具體代碼,實際執行是委託給ApplicationEventMulticaster(可以認爲是多播)
下面是一個事件發佈者的測試實例:
@RunWith(SpringRunner.class)
@SpringBootTest
public class EventTest {
@Autowired
private ApplicationContext applicationContext;
@Test
public void publishTest() {
TestEvent testEvent = new TestEvent("");
testEvent.setMessage("hello world");
applicationContext.publishEvent(testEvent);
}
}
//output:
hello world
利用ContextRefreshedEvent事件進行初始化操作
利用Spring的事件機制進行初始化一些操作,實際上就是前面提到了,利用ContextRefreshedEvent事件進行初始化,該事件是ApplicationContext初始化完成後調用的事件,所以我們可以利用這個事件,對應實現一個監聽器,在其onApplicationEvent()方法裏初始化操作.
@Component
public class ApplicationListenerTest implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("我被調用了..");
}
}
以上簡要總結了在springboot啓動時進行初始化操作的幾個方案,這幾種方式都可以滿足我們的需求,針對具體場景使用對應的方案。但是,CommandLineRunner或者ApplicationRunner不是Spring框架原有的東西,它倆屬於SpringBoot應用特定的回調擴展接口,所以很容易進行擴展,在一些微服務應用中使用也較廣泛。