springboot系列文章之啟動時初始化資料

pjmike_pj發表於2018-08-16

前言

在我們用springboot搭建專案的時候,有時候會碰到在專案啟動時初始化一些操作的需求,針對這種需求springboot(spring)為我們提供了以下幾種方案供我們選擇:

  • ApplicationRunnerCommandLineRunner介面
  • Spring Bean初始化的InitializingBean,init-method和PostConstruct
  • Spring的事件機制

ApplicationRunner與CommandLineRunner

如果需要在SpringApplication啟動時執行一些特殊的程式碼,你可以實現ApplicationRunnerCommandLineRunner介面,這兩個介面工作方式相同,都只提供單一的run方法,而且該方法僅在SpringApplication.run(...)完成之前呼叫,更準確的說是在構造SpringApplication例項完成之後呼叫run()的時候,具體分析見後文,所以這裡將他們分為一類。

ApplicationRunner

構造一個類實現ApplicationRunner介面

@Component
public class ApplicationRunnerTest implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("ApplicationRunner");
    }
}
複製程式碼

專案啟動測試結果如圖:

application

CommandLineRunner

對於這兩個介面而言,我們可以通過Order註解或者使用Ordered介面來指定呼叫順序,@Order()中的值越小,優先順序越高

@Component
@Order(1)
public class CommandLineRunnerTest implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        System.out.println("CommandLineRunner...");
    }
}

複製程式碼

當然我們也可以同時使用ApplicationRunnerCommandLineRunner,預設情況下前者比後者先執行,但是這沒有必要,使用一個就好了

兩者的聯絡與區別

前面就提到過,兩個介面都有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);
	}

複製程式碼

一路點選run()來到這裡

context
發現對ApplicationRunner的呼叫實際上在callRunners方法中
runer
對於CommandLineRunner或者ApplicationRunner來說,需要注意的兩點:

  • 所有CommandLineRunner/ApplicationRunner的執行時點是在SpringBoot應用的ApplicationContext完全初始化開始工作之後,callRunners()可以看出是run方法內部最後一個呼叫的方法(可以認為是main方法執行完成之前最後一步)
  • 只要存在於當前SpringBoot應用的ApplicationContext中的任何CommandLineRunner/ApplicationRunner,都會被載入執行(不管你是手動註冊還是自動掃描去Ioc容器)

Spring Bean初始化的InitializingBean,init-method和PostConstruct

InitializingBean介面

InitializingBean介面為bean提供了初始化方法的方式,它只包括afterPropertiesSet()方法。

在spring初始化bean的時候,如果bean實現了InitializingBean介面,在物件的所有屬性被初始化後之後才會呼叫afterPropertiesSet()方法

@Component
public class InitialingzingBeanTest implements InitializingBean {

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean..");
    }
}
複製程式碼

in

當然,我們可以看出spring初始化bean肯定會在 ApplicationRunner和CommandLineRunner介面呼叫之前。

當然有一點我們要注意的是,儘管使用initialingBean介面可以實現初始化動作,但是官方並不建議我們使用InitializingBean介面,因為它將你的程式碼耦合在Spring程式碼中,官方的建議是在bean的配置檔案指定init-method方法,或者在@Bean中設定init-method屬性

init

init-method和@PostConstruct

前面就說過官方文件上不建議使用InitializingBean介面,但是我們可以在<bean>元素的init-method屬性指定bean初始化之後的操作方法,或者在指定方法上加上@PostConstruct註解來制定該方法在初始化之後呼叫

@SpringBootApplication
public class Application {


    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @PostConstruct
    public void init() {
        System.out.println("init...");
    }
}

複製程式碼

更多關於Spring Bean的生命週期的內容,請參閱Spring相關書籍或部落格Spring Bean的生命週期

Spring的事件機制

Spring的事件機制實際上是設計模式中觀察者模式的典型應用,在Head First 設計模式中是這樣定義觀察者模式的:

觀察者模式定義了一個一對多的依賴關係,讓一個或多個觀察者物件監聽一個主題物件。這樣一來,當被觀察者狀態改變時,需要通知相應的觀察者,使這些觀察者能夠自動更新

watch

基礎概念

Spring的事件驅動模型由三部分組成

  • 事件: ApplicationEvent,繼承自JDK的EventObject,所有事件都要繼承它,也就是被觀察者
  • 事件釋出者: ApplicationEventPublisherApplicationEventMulticaster介面,使用這個介面,就可以釋出事件了
  • 事件監聽者: ApplicationListener,繼承JDK的EventListener,所有監聽者都繼承它,也就是我們所說的觀察者,當然我們也可以使用註解 @EventListener,效果是一樣的

事件

在Spring框架中,預設對ApplicationEvent事件提供瞭如下支援:

  • ContextStartedEvent:ApplicationContext啟動後觸發的事件
  • ContextStoppedEvent:ApplicationContext停止後觸發的事件
  • ContextRefreshedEvent:ApplicationContext初始化或重新整理完成後觸發的事件;(容器初始化完成後呼叫,所以我們可以利用這個事件做一些初始化操作)
  • ContextClosedEvent:ApplicationContext關閉後觸發的事件;(如web容器關閉時自動會觸發spring容器的關閉,如果是普通java應用,需要呼叫ctx.registerShutdownHook();註冊虛擬機器關閉時的鉤子才行)
    application

構造一個類繼承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();
    }
}
複製程式碼

事件釋出

對於事件釋出,代表者是ApplicationEventPublisherApplicationEventMulticaster,系統提供的實現如下

a
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("我被呼叫了..");
    }
}
複製程式碼

listener

注意: 在傳統的基於XML配置的Spring專案中會存在二次呼叫的問題,即呼叫兩次該方法,原因是在傳統的Spring MVC專案中,系統存在兩個容器,一個root容器,一個project-servlet.xml對應的子容器,在初始化這兩個容器的時候都會呼叫該方法一次,所以有二次呼叫的問題,而對於基於Springboot的專案不存在這個問題

小結

以上簡要總結了在springboot啟動時進行初始化操作的幾個方案,這幾種方式都可以滿足我們的需求,針對具體場景使用對應的方案。但是,CommandLineRunner或者ApplicationRunner不是Spring框架原有的東西,它倆屬於SpringBoot應用特定的回撥擴充套件介面,所以很容易進行擴充套件,在一些微服務應用中使用也較廣泛。

參考資料

相關文章