1. 程式人生 > >Spring Boot(05)——SpringApplication介紹

Spring Boot(05)——SpringApplication介紹

SpringApplication介紹

通常啟動Spring Boot應用時呼叫SpringApplication類的static run()進行啟動。

@SpringBootApplication
public class Application {

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

}

其內部最終會轉換為new一個SpringApplication物件,然後呼叫該物件的run方法,然後整個核心啟動邏輯就由SpringApplication物件的run方法完成。

public static ConfigurableApplicationContext run(Class<?>[] primarySources,
        String[] args) {
    return new SpringApplication(primarySources).run(args);
}

呼叫SpringApplication的靜態run方法時,由於SpringApplication物件是在內部建立的,其會在啟動Spring Boot時使用一些預設的配置。如果我們需要進行一些自定義配置,則可以自己手動的new一個SpringApplication物件,進行一些特殊配置後再呼叫SpringApplication物件的例項run方法。比如Spring Boot預設在啟動的時候會輸出Spring Boot的banner,其中包含了Spring Boot的版本資訊,如果我們不希望輸出該banner資訊,則可以進行如下定製。

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(Application.class);
        app.setBannerMode(Banner.Mode.OFF);
        app.run(args);
    }

}

ApplicationEvent及其監聽

SpringApplication在啟動Spring Boot的過程中會發布以下ApplicationEvent,也可以參考SpringApplication的例項run方法的實現。

  • ApplicationStartingEvent :會在進行其它操作之前釋出
  • ApplicationEnvironmentPreparedEvent : 接著是準備Environment,準備好了會發布該事件
  • ApplicationPreparedEvent :接著會構造ApplicationContext,在構造好ApplicationContext之後,呼叫其refresh()方法之前會發布該事件
  • ApplicationStartedEvent :在ApplicationContext進行refresh之後,呼叫ApplicationRunner和CommandLineRunner之前會發布該事件
  • ApplicationReadyEvent :在Spring Boot應用啟動完成之後,也就是在SpringApplication的run()呼叫馬上結束之前會發布該事件
  • ApplicationFailedEvent :在啟動過程中出現異常時會發布該事件

從上述的事件釋出過程可以看出,有些事件的釋出是在ApplicationContext還沒有準備好的情況下發布的,所以它們不能通過傳統的定義ApplicationEvent實現類為bean容器中的一個bean的方式進行監聽。SpringApplication介面為我們提供了專門的註冊這些監聽器的方法addListeners()。事件監聽器需要實現org.springframework.context.ApplicationListener介面。以下定義了兩個事件監聽器,都只是簡單的進行日誌輸出,然後在啟動應用的時候通過addListeners()添加了監聽器,程式啟動後會看到這兩個監聽器輸出的日誌資訊。

@Slf4j
public class ApplicationStartingEventListener implements ApplicationListener<ApplicationStartingEvent> {

    @Override
    public void onApplicationEvent(ApplicationStartingEvent event) {
        log.info("收到Spring Boot應用準備啟動的事件[{}]", event);
    }

}

@Slf4j
public class ApplicationReadyEventListener implements ApplicationListener<ApplicationReadyEvent> {

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        log.info("收到Spring Boot應用啟動完成的事件[{}]", event);
    }

}

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(Application.class);
        app.addListeners(new ApplicationStartingEventListener(), new ApplicationReadyEventListener());
        app.run(args);
    }

}

特別需要注意的是在新增監聽器的時候不要呼叫SpringApplication的setListeners(),而要呼叫其addListeners()。因為在構造SpringApplication物件的時候構造方法中已經通過Spring Boot的spring factory機制獲取並註冊了一些ApplicationListener(可以通過呼叫SpringApplication的getListeners()獲取到已經註冊的ApplicationListener),使用setListeners()會覆蓋掉已經註冊過的ApplicationListener。Spring Boot的spring factory機制是指可以建立一個META-INF/spring.factories檔案,然後以介面類的全路徑名稱作為Key,以實現類的全路徑名稱作為Value,當有多個Value時以英文逗號分隔,當有多個Key時每個Key一行。它們會被SpringFactoriesLoader進行處理,可以通過它獲取到定義的介面對應的實現類。Spring Boot中有很多擴充套件都是基於這個機制進行的。上面的定義的ApplicationListener實現類,如果需要使用spring factory機制,則可以在spring.factories檔案中新增如下內容:

org.springframework.context.ApplicationListener=com.elim.springboot.listener.ApplicationStartingEventListener,com.elim.springboot.listener.ApplicationReadyEventListener

當你覺得一行展示的內容太長了,期望折行展示時,可以在行末加上\,這語法跟定義properties檔案是一樣的。實際上其內部也是按照properties檔案進行解析的。

org.springframework.context.ApplicationListener=com.elim.springboot.listener.ApplicationStartingEventListener,\
com.elim.springboot.listener.ApplicationReadyEventListener

通過spring.factories檔案定義了ApplicationListener後,我們的啟動應用程式碼就可以改寫為如下這種最簡單的方式了。

@SpringBootApplication
public class Application {

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

}

ApplicationContext的選擇

預設情況下,當ClassPath下存在SpringMVC相關的Class時將使用org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext,當不存在SpringMVC相關的Class,而是存在SpringWebFlux相關的Class時將使用org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext,當兩者都不存在時可以使用預設的org.springframework.context.annotation.AnnotationConfigApplicationContext。可以通過其setWebApplicationType(WebApplicationType webApplicationType)手動指定WebApplicationType,從而影響使用的ApplicationContext的選擇,也可以直接通過setApplicationContextClass(Class<? extends ConfigurableApplicationContext> applicationContextClass)指定需要使用的ApplicationContext對應的Class。

訪問命令列引數

呼叫SpringApplication的run()時傳遞的引數通常來自於命令列的引數,SpringApplication內部在呼叫run()時會把它們封裝為一個ApplicationArguments物件,並且會把它定義為bean容器中的一個bean。如果在應用中需要訪問命令列傳遞的引數,則可以通過注入ApplicationArguments物件,進行獲取到對應的引數。命令列指定引數時有兩種引數,一種是可選型引數、一種是非可選型引數,可選型引數以--開頭,需要賦值時可以加上=,比如指定命令列引數為--debug --foo=bar abc,則可選型引數為debug和foo,而非可選型引數為abc。如下程式碼就是基於該命令列引數的一個簡單示例。

@Controller
public class SampleController {

    @Autowired
    private ApplicationArguments arguments;
    
    /**
     * 傳遞的命令列引數是--debug --foo=bar abc
     * @param writer
     * @throws Exception
     */
    @GetMapping("sample/args")
    public void arguments(PrintWriter writer) throws Exception {
        writer.println("包含debug引數:" + arguments.containsOption("debug"));//true
        writer.println("引數foo的值是:" + arguments.getOptionValues("foo"));//[bar]
        writer.println("其它非選項性引數:" + arguments.getNonOptionArgs());//[abc]
        writer.println("原始引數是:" + Arrays.toString(arguments.getSourceArgs()));//--debug, --foo=bar, abc
    }

}

這種引數有別於在執行程式時通過-Dkey=value指定的虛擬機器引數,通過-Dkey=value指定的虛擬機器引數可以通過System.getProperty("key")獲取到。命令列引數是對應程式執行主命令之後新增的引數,比如上面新增的那些引數的完整指令是java -jar app.jar --debug --foo=bar abc

ApplicationRunner和CommandLineRunner

前面在介紹事件監聽器的時候已經介紹了,在Spring Boot應用啟動成功後會在bean容器中尋找ApplicationRunner和CommandLineRunner型別的bean,呼叫它們的run()。所以如果想在Spring Boot應用啟動成功或做一些事情,則可以實現自己的ApplicationRunner或CommandLineRunner。它們的區別在於ApplicationRunner的run()的入參是ApplicationArguments物件,而CommandLineRunner的run()的入參是原始的引數陣列。

@Component
@Slf4j
public class MyApplicationRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("Spring Boot應用啟動成功,攜帶的命令列引數是:{}", Arrays.toString(args.getSourceArgs()));
    }

}

@Component
@Slf4j
public class MyCommandLineRunner implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        log.info("Spring Boot應用已經啟動成功了,攜帶的命令列引數是:{}", Arrays.toString(args));
    }

}

其實前面介紹事件監聽器的時候也提到了,通過實現ApplicationListener,監聽ApplicationStartedEvent或ApplicationReadyEvent也可以在Spring Boot應用啟動成功後做一些事情。它們的區別主要就在於ApplicationRunner和CommandLineRunner實現類是bean容器中的一個bean,可以注入其它bean,而且它們可以很方便的訪問到命令列引數。

SpringApplicationBuilder

在構建SpringApplication物件時也可以通過SpringApplicationBuilder進行構建,通過它可以流式的進行配置,還可以指定子ApplicationContext。

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        // SpringApplication.run(SpringBootApplication.class, args);
        SpringApplication app = new SpringApplicationBuilder(Application.class)
            .child(ChildConfig.class)
            .bannerMode(Banner.Mode.OFF)
            .build();
        app.run(args);
    }

}

關於SpringApplication的更多可定製的資訊可以參考對應的API文件。

啟用JMX管理

在application.properties檔案中新增spring.application.admin.enabled=true可以啟用JMX管理,這會發佈一個SpringApplicationAdminMXBean型別的MBean。通過它的getProperty()可以獲取當前應用對應的啟動JVM的一些系統屬性或者是定義在application.properties中的一些屬性的值,因為其底層對應的是當前Environment物件。通過其shutdown()可以進行遠端的關閉操作。

參考文件

https://docs.spring.io/spring-boot/docs/2.0.3.RELEASE/reference/html/boot-features-spring-application.html

(注:本文是基於Spring Boot 2.0.3所寫)