首頁>技術>

Spring 的豐富生態備受開發者青睞,尤其是自從 SpringBoot 出現之後去掉了原來的複雜配置,因為 SpringBoot 的理念就是 約定大於配置 ,這讓我們省去了很多需要手動配置的過程,就拿 SpringMVC 來說吧各種 XML 配置直接勸退初學者,但是 SpringBoot 的易用性簡直是成為了推廣 Spring 生態的利器。本篇文章主要是結合 SpringBoot 的原始碼,來探究 SpringBoot 應用程式的啟動流程!

新建一個 SpringBoot 專案,首先映入眼簾的恐怕就是下面的這個關鍵的 Main 函式與 @SpringBootApplication 註解吧,我們將從這個註解開始,逐步探究 SpringBoot 應用的啟動流程:

@SpringBootApplicationpublic class SpringBootStartApplication {    public static void main(String[] args) {        SpringApplication.run(SpringBootStartApplication.class, args);    }}複製程式碼

@SpringBootApplication 註解實際上是 SpringBoot 提供的一個複合註解,我們來看一看其原始碼:

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })public @interface SpringBootApplication {    ...}複製程式碼

關於這裡面某些元註解的功能,可以參考我之前的寫的一篇部落格 《 註解的原理與實現 》 。在這裡我們只需要看 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 這三個註解。在 SpringBoot 應用的啟動類上用這個三個註解代替 @SpringBootApplication 註解其實也是沒問題的:

@SpringBootConfiguration@EnableAutoConfiguration@ComponentScanpublic class SpringBootStartApplication {    public static void main(String[] args) {        SpringApplication.run(SpringBootStartApplication.class, args);    }}複製程式碼

那我們接下來就需要分貝探究這三個註解的功能。

@SpringBootConfiguration
@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Configurationpublic @interface SpringBootConfiguration {    @AliasFor(        annotation = Configuration.class    )    boolean proxyBeanMethods() default true;}複製程式碼

@SpringBootConfiguration 也是來源於 @Configuration,二者功能都是將當前類標註為配置類,@Configuration 用於定義配置類,可替換 xml 配置檔案,被註解的類內部包含有一個或多個被 @Bean 註解的方法,這些方法將會被 AnnotationConfigApplicationContext 或 AnnotationConfigWebApplicationContext 類進行掃描,並用於構建 Bean 定義,初始化 Spring 容器,這個貌似一點都不新奇。

@EnableAutoConfiguration
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)public @interface EnableAutoConfiguration {    ...}複製程式碼

@EnableAutoConfiguration 註解啟用自動配置,其中最關鍵的要屬 @Import(AutoConfigurationImportSelector.class),借 AutoConfigurationImportSelector,@EnableAutoConfiguration 可以幫助 SpringBoot 應用將所有符合條件的 @Configuration 配置都載入到當前 SpringBoot 建立並使用的 IoC 容器。

藉助於 Spring 框架原有的一個工具類:SpringFactoriesLoader 的支援, @EnableAutoConfiguration 可以智慧的自動配置功效才得以大功告成!

關於這個註解可以參考我的另一篇文章 《SpringBoot 自動配置原理》 ,裡面有詳細介紹並且有例子。

@ComponentScan

@ComponentScan 對應於 XML 配置形式中的 context:component-scan,用於將一些標註了特定註解的 bean 定義批次採集註冊到 Spring 的 IoC 容器之中,這些特定的註解大致包括:

@Controller@Entity@Component@Service@Repository

對於該註解可以透過 basePackages 屬性來更細粒度的控制該註解的自動掃描範圍,比如:

@ComponentScan(basePackages = {"xpu.tim.controller","xpu.tim.entity"})複製程式碼

@SpringBootApplication 這個註解看完了, 那麼接下來就來看看這個 SpringApplication 以及 run() 方法究竟幹了些啥。原始的 SpringCore 中並沒有這個類,SpringApplication 裡面封裝了一套 Spring 應用的啟動流程,然而這對使用者完全透明,因此我們上手 SpringBoot 時感覺簡潔、輕量。

透過閱讀 run 方法的原始碼我們不難發現,其實是需要構造一個 SpringApplication 物件:

/** * Static helper that can be used to run a {@link SpringApplication} from the * specified sources using default settings and user supplied arguments. * @param primarySources the primary sources to load * @param args the application arguments (usually passed from a Java main method) * @return the running {@link ApplicationContext} */public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {    return new SpringApplication(primarySources).run(args);}複製程式碼

預設的 SpringApplication 執行流程已經可以滿足大部分需求,但是若使用者想幹預這個過程,則可以透過 SpringApplication 在流程某些地方開啟的擴充套件點來完成對流程的擴充套件,典型的擴充套件方案那就是使用 set 方法。

@SpringBootApplicationpublic class SpringBootStartApplication {    public static void main(String[] args) {        //SpringApplication.run(SpringBootStartApplication.class, args);        SpringApplication application = new SpringApplication(SpringBootStartApplication.class);        application.set...(); // 使用者自定義擴充套件點        application.set...(); // 使用者自定義擴充套件點        application.run(args);    }}複製程式碼

這樣一拆解後我們發現,我們也需要先構造 SpringApplication 類物件,然後呼叫該物件的 run() 方法。那麼接下來就講講 SpringApplication 的構造過程以及其 run() 方法的流程,搞清楚了這個,那麼也就搞清楚了 SpringBoot 應用是如何執行起來的! 主要需要看以下四個方法:

1、deduceFromClasspath:用來推斷應用的型別:建立的是 REACTIVE 應用、SERVLET 應用、NONE 三種中的一種

NONE 表示當前的應用即不是一個 web 應用也不是一個 REACTIVE 應用,是一個純後臺的應用。SERVLET 表示當前應用是一個標準的 web 應用。REACTIVE 是 spring5 當中的新特性,表示是一個響應式的 web 應用。而判斷的依據就是根據 Classloader 中載入的類。如果是 servlet,則表示是 web,如果是 DispatcherHandler,則表示是一個 REACTIVE 應用,如果兩者都不存在,則表示是一個非 web 環境的應用。

2、setInitializers:使用 SpringFactoriesLoader 查詢並載入 classpath 下 META-INF/spring.factories 檔案中所有可用的 ApplicationContextInitializer

使用 SpringFactoriesLoader 查詢並載入 classpath 下 META-INF/spring.factories 檔案中的所有可用的 ApplicationListener

3、setListeners:使用 SpringFactoriesLoader 查詢並載入 classpath 下 META-INF/spring.factories 檔案中的所有可用的 ApplicationListener

4、deduceMainApplicationClass:推斷並設定 main 方法的定義類

透過這個幾個關鍵步驟,SpringApplication 完成了例項化。

之前我們弄清楚了 SpringApplication 的例項化過程,現在看看它的 run 方法究竟幹了什麼:

1、透過 SpringFactoriesLoader 載入 META-INF/spring.factories 檔案,獲取並建立 SpringApplicationRunListener 物件;

2、然後由 SpringApplicationRunListener 來發出 starting 訊息;

3、把引數 args 封裝成 DefaultApplicationArguments,並配置當前 SpringBoot 應用將要使用的 Environment;

4、完成之後,依然有 SpringApplicationRunListener 來發出 environmentPrepared(環境已準備)訊息;

5、建立上下文,根據專案型別建立上下文;

6、初始化 ApplicationContext,並設定 Environment,載入相關配置等;

7、由 SpringApplicationRunListener 來發出 contextPrepared 訊息,告知 SpringBoot 應用使用的 ApplicationContext 已準備 OK;

8、將各種 beans 裝載入 ApplicationContext,繼續由 SpringApplicationRunListener 來發出 contextLoaded 訊息,告知 SpringBoot 應用使用的 ApplicationContext 已裝填 OK;

9、refresh ApplicationContext,完成 IoC 容器可用的最後一步;

10、由 SpringApplicationRunListener 來發出 started 訊息 ;

11、完成最終的程式的啟動;

12、SpringApplicationRunListener 來發出 running 訊息,告知程式已執行起來了;

步驟 4 和 5 之間還有個 PrintBanner,用來列印 Banner

createApplicationContext()

下面這段程式碼主要是根據專案型別建立上下文,並且會注入幾個核心元件類:

protected ConfigurableApplicationContext createApplicationContext() {    Class<?> contextClass = this.applicationContextClass;    if (contextClass == null) {        try {            switch (this.webApplicationType) {                case SERVLET:                    contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);                    break;                case REACTIVE:                    contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);                    break;                default:                    contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);            }        }        catch (ClassNotFoundException ex) {            throw new IllegalStateException(                "Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);        }    }    return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);}複製程式碼

Web 型別專案建立上下文物件 AnnotationConfigServletWebServerApplicationContext 。這裡會把 ConfigurationClassPostProcessor 、AutowiredAnnotationBeanPostProcessor 等一些核心元件加入到 Spring 容器。

refreshContext()

下面一起來看下 refreshContext(context) 這個方法,這個方法啟動 spring 的程式碼載入了 bean,還啟動了內建 web 容器:

private void refreshContext(ConfigurableApplicationContext context) {    refresh(context);    if (this.registerShutdownHook) {        try {            context.registerShutdownHook();        }        catch (AccessControlException ex) {            // Not allowed in some environments.        }    }}複製程式碼

點選跟進後發現方法裡面是 spring 容器啟動程式碼:

我們可以看到一個 onRefresh 方法,點進去需要看的是子類實現,我們只看其中一個子類實現:

@Overrideprotected void onRefresh() {    super.onRefresh();    try {        createWebServer();    }    catch (Throwable ex) {        throw new ApplicationContextException("Unable to start web server", ex);    }}private void createWebServer() {    WebServer webServer = this.webServer;    ServletContext servletContext = getServletContext();    if (webServer == null && servletContext == null) {        // 這個獲取webServerFactory還是要進去看看        ServletWebServerFactory factory = getWebServerFactory();        this.webServer = factory.getWebServer(getSelfInitializer());    }    else if (servletContext != null) {        try {            getSelfInitializer().onStartup(servletContext);        }        catch (ServletException ex) {            throw new ApplicationContextException("Cannot initialize servlet context", ex);        }    }    initPropertySources();}複製程式碼

我們繼續看下 getWebServletFactory() 這個方法,這個裡面其實就是選擇出哪種型別的 web 容器了:

protected ServletWebServerFactory getWebServerFactory() {    // Use bean names so that we don't consider the hierarchy    String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);    if (beanNames.length == 0) {        throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing "                                              + "ServletWebServerFactory bean.");    }    if (beanNames.length > 1) {        throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "                                              + "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));    }    return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);}複製程式碼

我們再去看 factory.getWebServer(getSelfInitializer()) ,轉到定義就會看到很熟悉的名字 tomcat:

內建的 Servlet 容器就是在 onRefresh() 方法裡面啟動的,至此一個 Servlet 容器就啟動 OK 了。

1、new 了一個 SpringApplication 物件,使用 SPI 技術載入載入 ApplicationContextInitializer、ApplicationListener 介面例項;

2、呼叫 SpringApplication.run() 方法;

3、呼叫 createApplicationContext() 方法建立上下文物件,建立上下文物件同時會註冊 spring 的核心元件類(ConfigurationClassPostProcessor 、AutowiredAnnotationBeanPostProcessor 等);

4、呼叫 refreshContext() 方法啟動 Spring 容器和內建的 Servlet 容器;

原文連結:https://juejin.cn/post/6921226309792169991

9
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • apache 2.2升級到2.4的詳細步驟