Spring Boot源碼分析-啓動原理

1.簡介

Spring Boot是由Pivotal團隊提供的快速開發框架,基於SpringMVC經過註解+內置Http服務器如:tomcat-embed-core,簡化了XML配置,快速將一些經常使用的第三方依賴整合(經過Maven繼承依賴關係),最終實現以Java應用程序的方式進行執行。java

1.1 SpringBoot起源

  • Spring框架:Spring框架從早期的IOC與AOP衍生出了不少產品例如Spring (boot、security、jpa)等等
  • SpringMVC框架:Spring MVC提供了一種輕度耦合的方式來開發web應用,它是Spring的一個web框架。經過Dispatcher Servlet, ModelAndView 和 View Resolver,開發web應用變得很容易。解決的問題領域是網站應用程序或者服務開發——URL路由、Session、模板引擎、靜態Web資源等等,是基於Spring的一個 MVC 框架。
  • SpringBoot框架:Spring Boot實現了自動配置,下降了項目搭建的複雜度。它主要是爲了解決使用Spring框架須要進行大量的配置太麻煩的問題,因此它並非用來替代Spring的解決方案,而是和Spring框架緊密結合用於提高Spring開發者體驗的工具。同時它集成了大量經常使用的第三方庫配置(例如Jackson, JDBC, Mongo, Redis, Mail等等),是基於Spring4的條件註冊的一套快速開發整合包。

圖片描述

1.2 便捷的starter poms (啓動器)

starter包含了搭建項目快速運行所需的依賴。它是一個依賴關係描述符的集合。當應用須要一種spring的其它服務時,不須要粘貼拷貝大量的依賴關係描述符。例如想在spring中使用redis,只須要在項目中包含 spring-boot-starter-redis 依賴就可使用了,全部的starters遵循一個類似的命名模式:spring-boot-starter-,在這裏是一種特殊類型的應用程序。該命名結構能夠幫你找到須要的starter。不少IDEs集成的Maven容許你經過名稱搜索依賴。react

1.3 demo

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

這是一個簡單的SpringBoot的啓動類實現,下文章將圍繞這個demo進行擴展分析。web

1.4 SpringBoot啓動流程

clipboard.png

2 @SpringBootApplication註解分析

如下是註解@SpringBootApplication的源碼實現redis

@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 {

能夠發現它是由衆多註解組合而成的,下面具體分析下這裏每一個註解所起到的做用。spring

  • @Target Target經過ElementType來指定註解可以使用範圍的枚舉集合(FIELD/METHOD/PARAMETER...)
  • @Retention Retention(保留)註解說明,這種類型的註解會被保留到那個階段. 有三個值:tomcat

    • RetentionPolicy.SOURCE —— 這種類型的Annotations只在源代碼級別保留,編譯時就會被忽略
    • RetentionPolicy.CLASS —— 這種類型的Annotations編譯時被保留,在class文件中存在,但JVM將會忽略
    • RetentionPolicy.RUNTIME —— 這種類型的Annotations將被JVM保留,因此他們能在運行時被JVM或其餘使
  • @Documented 註解代表這個註解應該被 javadoc工具記錄. 默認狀況下,javadoc是不包括註解的. 但若是聲明註解時指定了 @Documented,則它會被 javadoc 之類的工具處理, 因此註解類型信息也會被包括在生成的文檔中
  • @Inherited 容許子類繼承父類的註解,僅限於類註解有用,對於方法和屬性無效。
  • @SpringBootConfiguration 註解實際上和@Configuration有相同的做用,配備了該註解的類就可以以JavaConfig的方式完成一些配置,能夠再也不使用XML配置。
  • @ComponentScan 這個註解完成的是自動掃描的功能,至關於Spring XML配置文件中的:<context:component-scan>,可以使用basePackages屬性指定要掃描的包,及掃描的條件。若是不設置則默認掃描@ComponentScan註解所在類的同級類和同級目錄下的全部類,因此咱們的Spring Boot項目,通常會把入口類放在頂層目錄中,這樣就可以保證源碼目錄下的全部類都可以被掃描到。
  • @EnableAutoConfiguration 這個註解是讓Spring Boot的配置可以如此簡化的關鍵性註解。我把EnableAutoConfiguration的實現端上來了,你們來鑑賞一下!服務器

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @AutoConfigurationPackage
    @Import(AutoConfigurationImportSelector.class)
    public @interface EnableAutoConfiguration {
    • @AutoConfigurationPackage 註解用於保存自動配置類以供以後的使用,好比給JPA entity掃描器,用來掃描開發人員經過註解@Entity定義的entity類。通俗的講就是,註冊bean定義到容器中。
    • @Import(AutoConfigurationImportSelector.class)是EnableAutoConfiguration註解中最關鍵的來,它藉助AutoConfigurationImportSelector,能夠幫助SpringBoot應用將全部符合條件的@Configuration配置都加載到當前SpringBoot建立並使用的IoC容器中。關於@Import註解要說的內容還比較多,改天再聊。

關於註解的話題就先談到這裏,下面開啓擼代碼環節。併發

3 剖析代碼

查看SpringApplication的源代碼能夠發現SpringApplication的啓動由兩部分組成:app

  • new SpringApplication(primarySources):建立SpringApplication對象
  • run(args):調用run方法

3.1 實例化SpringApplication對象

源碼以下:框架

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        this.resourceLoader = resourceLoader;//一、初始化資源加載器
        Assert.notNull(primarySources, "PrimarySources must not be null");//二、斷言資源加載類不能爲 null
        this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));//三、初始化加載資源類集合並去重
        this.webApplicationType = deduceWebApplicationType();//四、 推斷應用類型是Standard仍是Web
        setInitializers((Collection) getSpringFactoriesInstances(
                ApplicationContextInitializer.class));//五、設置應用上下文初始化器
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));//六、設置監聽器 
        this.mainApplicationClass = deduceMainApplicationClass();//七、推斷應用入口類
    }

下面將針對源碼中的重要實現進行詳細的分析。

3.1.1 初始化資源加載器

ResourceLoader接口,在 Spring 中用於加載資源,經過它能夠獲取一個Resouce 對象。使用spring的朋友都知道它加載資源的方式由多種,下面就挑兩個經常使用的繼承ResourceLoader的接口與實現類提及。

  • DefaultResourceLoader : 做爲 ResourceLoader 接口的直接實現類,該類實現了基本的資源加載功能,能夠實現對單個資源的加載。
  • ResourcePatternResolver :該接口繼承了 ResourceLoader,定義了加載多個資源的方法, 能夠實現對多個資源的加載。
一、DefaultResourceLoader

上面介紹過該類經過實現 ResourceLoader 接口實現了加載單個資源的功能。它的子類經過繼承它來實現具體的資源訪問策略。下面來探究下該類如何加載單個資源:

public Resource getResource(String location) {//這裏是三種識別location加載出Resource的方式。
        Assert.notNull(location, "Location must not be null");
        //1.先看有沒有自定義的ProtocolResolver,若是有則先根據自定義的ProtocolResolver解析location獲得Resource
        for (ProtocolResolver protocolResolver : this.protocolResolvers) {
            Resource resource = protocolResolver.resolve(location, this);
            if (resource != null) {
                return resource;
            }
        }
        //2.根據路徑是否匹配"/"或"classpath:"來解析獲得ClassPathResource
        if (location.startsWith("/")) {
            return getResourceByPath(location);
        }else if (location.startsWith(CLASSPATH_URL_PREFIX)) {//classpath
            return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
        }else {
            try {
                //默認傳入的location是一個URL路徑,加載獲得一個UrlResource
                URL url = new URL(location);
                return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
            }
            catch (MalformedURLException ex) {
                // 若是以上三種狀況都不知足,則按照「/」來處理
                return getResourceByPath(location);
            }
        }
    }

掃盲:ProtocolResolver是解析location的自定義拓展類,有了它咱們才能隨意傳入不一樣格式的location,而後根據對應的格式去解析並得到咱們的Resource便可。
擴展:在Spring容器初始化過程當中,咱們能夠自定義一個類實現ProtocolResolver接口,而後實現該resolve方法,就能夠解析特定的location獲得Resoure。

二、ResourcePatternResolver

該接口繼承了ResourceLoader接口,在其基礎上增長了同時對多個資源的訪問功能。

public interface ResourcePatternResolver extends ResourceLoader {
    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

    // 例如使用 ant 風格的路徑,匹配路徑下的多個資源
    Resource[] getResources(String locationPattern) throws IOException;
}

PathMatchingResourcePatternResolver是 ResourcePatternResolver 接口的直接實現類,它是基於模式匹配的,默認使用AntPathMatcher 進行路徑匹配,它除了支持 ResourceLoader 支持的前綴外,還額外支持 「classpath*」 ,下面查看源碼看看它如何實現對多個資源的訪問。

public Resource[] getResources(String locationPattern) throws IOException {
        Assert.notNull(locationPattern, "Location pattern must not be null");
         //首先判斷資源路徑是不是類路徑下的資源(以 「classpath*:」 開頭)
        if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
            // 經過 getPathMatcher 方法取得 PathMatcher ,默認只有 AntPathMatcher 一個實現類
           // 經過 isPattern 方法判斷路徑是否容許存在多個匹配的資源(路徑中包含 「*」 或 「?」)
            if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
                //找到全部匹配路徑(ant 風格)的資源
                return findPathMatchingResources(locationPattern);
            }
            else {
                //經過類加載器查找
                return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
            }
        }
        else {
            int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
                    locationPattern.indexOf(':') + 1);
            // 判斷資源路徑 ":" 以後的部分是否包含 "*" 或 "?"
            if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
                // a file pattern
                return findPathMatchingResources(locationPattern);
            }
            else {
                // 若不存在表示是單個資源,則經過從構造函數傳入的 ResourceLoader 取得
                return new Resource[] {getResourceLoader().getResource(locationPattern)};
            }
        }
    }

擴展:查看源碼發現ApplicationContext接口也繼承了 ResourcePatternResolver 接口,說明它也集成了對對單個或多個資源的訪問功能。

  • 當 Spring 須要進行資源訪問時,實際上並不須要直接使用 Resource 實現類,而是調用 getResource 方法來獲取資源。
  • 當經過 ApplicationContext 實例獲取 Resource 實例時,它將會負責選擇具體的 Resource 的實現類。代碼以下:
    Resource res = ctx.getResource("some/resource/path/myTemplate.txt);

Spring採用和 ApplicationContext 相同的策略來訪問資源。也就是說:

  • 若是 ApplicationContext 是 FileSystemXmlApplicationContext,res 就是 FileSystemResource 實例;
  • 若是 ApplicationContext 是 ClassPathXmlApplicationContext,res 就是 ClassPathResource 實例;
  • 若是 ApplicationContext 是 XmlWebApplicationContext,res 是 ServletContextResource 實例。

也就是說 ApplicationContext 將會肯定具體的資源訪問策略,從而將應用程序和具體的資源訪問策略分離開來,這就體現了策略模式的優點。

3.1.2 設置應用上下文初始化器setInitializers

  • 介紹:initializers是SpringApplication中的一個實例屬性:List<ApplicationContextInitializer<?>> initializers,每個initailizer都是一個實現了ApplicationContextInitializer接口的實例。ApplicationContextInitializer是Spring IOC容器中提供的一個接口,源碼以下:
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext>{
        void initialize(C applicationContext);
    }
  • 做用:ApplicationContextInitializer接口的做用就是在spring prepareContext的時候作一些初始化工做,在spring 初始化的過程當中 執行prepareContext方法的時候裏面會經過applyInitializers方法回調全部ApplicationContextInitializer接口的實現。因此在SpringApplication的構造方法中執行了setInitializers方法,該方法是把初始化的ApplicationContextInitializer實現類所有加載到SpringApplication內部的集合中。
  • 實現:這些初始化器(initializers)是Spring Boot從本地的META-INF/spring.factories文件和jar文件中的META-INF/spring.factories 文件獲取配置的初始化類(注意這邊是獲取了配置文件的所有配置類),而後經過loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());

}方法得到ApplicationContextInitializer接口所有實現類的完整名稱,最後經過反射的機制得到ApplicationContextInitializer實現類。

  • 使用:經過getSpringFactoriesInstances(

ApplicationContextInitializer.class)方法得到實現類

3.1.3 設置監聽器(setListeners)

listeners成員變量,是一個ApplicationListener<?>類型對象的集合。能夠看到獲取該成員變量內容使用的是跟成員變量initializers同樣的方法,只不過傳入的類型從ApplicationContextInitializer.class變成了ApplicationListener.class。

public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
        void onApplicationEvent(E event);
    }

這個接口基於JDK中的EventListener接口,實踐了觀察者模式。對於Spring框架的觀察者模式實現,它限定感興趣的事件類型須要是ApplicationEvent類型的子類,而這個類一樣是繼承自JDK中的EventObject類。

3.1.4 推斷應用入口類

該方法經過構造一個運行時異常,經過異常棧中方法名爲main的棧幀來獲得main()所在類的名字。

private Class<?> deduceMainApplicationClass() {
        try {
            StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
            for (StackTraceElement stackTraceElement : stackTrace) {
                if ("main".equals(stackTraceElement.getMethodName())) {
                    return Class.forName(stackTraceElement.getClassName());
                }
            }
        }
        catch (ClassNotFoundException ex) {
            // Swallow and continue
        }
        return null;
    }

3.2 run方法

源碼以下:

public ConfigurableApplicationContext run(String... args) {
    //一、計時監控類
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    
    // 二、初始化應用上下文和異常報告集合
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    
    // 三、設置系統屬性java.awt.headless的值,默認值爲:true(沒有圖形化界面)
    configureHeadlessProperty();
    
    // 四、建立全部 Spring 運行監聽器併發出開始執行的事件
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
        // 五、初始化默認應用參數類
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                args);
                
        // 六、根據SpringApplicationRunListeners和應用參數來準備 Spring 環境
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                applicationArguments);
        configureIgnoreBeanInfo(environment);
        
        // 七、準備Banner打印器 - 就是啓動Spring Boot的時候打印在console上的ASCII藝術字體
        Banner printedBanner = printBanner(environment);
        
        // 八、建立Spring上下文
        context = createApplicationContext();
        
        // 九、準備異常報告器
        exceptionReporters = getSpringFactoriesInstances(
                SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context);
                
        // 十、Spring上下文前置處理
        prepareContext(context, environment, listeners, applicationArguments,
                printedBanner);
                
        // 十一、刷新Spring上下文
        refreshContext(context);
        
        // 十二、Spring上下文後置處理
        afterRefresh(context, applicationArguments);
        
        // 1三、中止計時監控類
        stopWatch.stop();
        
        // 1四、輸出日誌記錄執行主類名、時間信息
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass)
                    .logStarted(getApplicationLog(), stopWatch);
        }
        
        // 1五、發佈應用上下文啓動完成事件
        listeners.started(context);
        
        // 1六、執行全部 Runner 運行器
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }
 
    try {
        // 1七、發佈應用上下文就緒事件
        listeners.running(context);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    // 1八、返回應用上下文
    return context;
}

下面針對run方法中提到的一些重要步驟進行闡述:

3.2.1 計時監控類

啓動方法以下:

public void start() throws IllegalStateException {
        start("");
    }
    
    public void start(String taskName) throws IllegalStateException {
        if (this.currentTaskName != null) {
            throw new IllegalStateException("Can't start StopWatch: it's already running");
        }
        this.currentTaskName = taskName;
        this.startTimeMillis = System.currentTimeMillis();
    }

你能夠看到它傳入了一個空字符串給當前任務做爲任務名稱,而後記錄當前Spring Boot應用啓動的開始時間。而且它會判斷當前任務名是否存在,用於保證Spring Boot應用不重複啓動。

3.2.2 初始化應用上下文和異常報告集合

這裏僅初始化一個應用上下文對象context和一個空的異常報告集合,具體用途看下文。

3.2.3 設置系統屬性

configureHeadlessProperty的方法實現以下:

private void configureHeadlessProperty() {
        System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, System.getProperty(
                SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
    }
    private static final String SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = "java.awt.headless";

該方法設置了一個名爲java.awt.headless的系統屬性,觀其源碼能夠發現它給屬性設值System.setProperty(),而它的值來源於System.getProperty(),這並不奇怪,由於System中getProperty()有2個重載方法,其中getProperty()有單參和雙參兩個方法,這裏調用的是雙參數方法,該方法在沒有的時候會返回一個調用者指定的默認值,因此這裏先獲取後設置。這個設置的目的是保證即便沒有檢測到顯示器(服務器不須要顯示器),也容許程序啓動。

3.2.4 建立全部 Spring 運行監聽器併發出開始執行的事件

實現以下:

//根據args獲取全部SpringApplicationRunListeners監聽器
SpringApplicationRunListeners listeners = getRunListeners(args);

//這段代碼你們應該已經熟悉了,獲取SpringApplicationRunListeners擴展
private SpringApplicationRunListeners getRunListeners(String[] args) {
    Class<?>[] types = new Class<?>[]{SpringApplication.class, String[].class};
    return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

經過Class<?>[] types = new Class<?>[]{SpringApplication.class, String[].class};類型加載對應的監聽器,並建立SpringApplicationRunListener實例

下面的內容和以前實例化初始化器的流程是同樣,經過getSpringFactoriesInstances方法從spring.factories中獲取與SpringApplicationRunListener.class相關的實例類名列表。

3.2.5 初始化默認應用參數類

ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

args是啓動Spring應用的命令行參數,該參數能夠在Spring應用中被訪問。
如:--server.port=9000

3.2.6 根據運行監聽器和應用參數來準備 Spring 環境

建立並配置當前SpringBoot應用將要使用的Environment(包括配置要使用的PropertySource以及Profile(其做用就是指定激活的配置文件,能夠區分環境來加載不一樣的配置)),並遍歷調用全部的SpringApplicationRunListener的environmentPrepared()方法,廣播Environment準備完畢。
源碼以下:

private ConfigurableEnvironment prepareEnvironment(
            SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments) {
        //獲取或建立環境(存在就直接返回,不存在建立一個再返回)
        ConfigurableEnvironment environment = getOrCreateEnvironment();
        //配置環境:配置PropertySources和activeProfiles
        configureEnvironment(environment, applicationArguments.getSourceArgs());
        //listeners環境準備(就是廣播ApplicationEnvironmentPreparedEvent事件)。
        listeners.environmentPrepared(environment);
        //將環境綁定到SpringApplication
        bindToSpringApplication(environment);
        //若是非web環境,將環境轉換成StandardEnvironment
        if (this.webApplicationType == WebApplicationType.NONE) {
            environment = new EnvironmentConverter(getClassLoader())
                    .convertToStandardEnvironmentIfNecessary(environment);
        }
        //配置PropertySources對它本身的遞歸依賴
        ConfigurationPropertySources.attach(environment);
        return environment;
    }

prepareEnvironment的做用:加載外部化配置資源到environment,包括命令行參數、servletConfigInitParams、servletContextInitParams、systemProperties、sytemEnvironment、random、application.yml(.yaml/.xml/.properties)等;初始化日誌系統。

3.2.7 準備Banner打印器

該功能僅供自娛自樂,不作過多解讀,看源碼即是。

private Banner printBanner(ConfigurableEnvironment environment) {
        if (this.bannerMode == Banner.Mode.OFF) {
            return null;
        }
        ResourceLoader resourceLoader = (this.resourceLoader != null ? this.resourceLoader
                : new DefaultResourceLoader(getClassLoader()));
        SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(
                resourceLoader, this.banner);
        if (this.bannerMode == Mode.LOG) {
            return bannerPrinter.print(environment, this.mainApplicationClass, logger);
        }
        return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
    }

3.2.8 建立Spring上下文

源碼以下:

public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext";

public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";

public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context.annotation.AnnotationConfigApplicationContext";

protected ConfigurableApplicationContext createApplicationContext() {
        // 先判斷有沒有指定的實現類
        Class<?> contextClass = this.applicationContextClass;
        // 若是沒有,則根據應用類型選擇
        if (contextClass == null) {
            try {
                //根據webApplicationType的類型去反射建立ConfigurableApplicationContext的具體實例。
                switch (this.webApplicationType) {
                case SERVLET:
                    contextClass = Class.forName(DEFAULT_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);
    }

createApplicationContext()該方法的邏輯較爲簡單,共兩個分支:

  • 自定義ApplicationContext的實現類
  • 根據當前應用的類型webApplicationType來匹配對應的ApplicationContext,是servlet、reactive或者非web應用

3.2.9 準備異常報告器

這一步的邏輯和實例化初始化器和監聽器的同樣,都是經過調用 getSpringFactoriesInstances 方法來獲取配置的異常類名稱並實例化全部的異常處理類。

3.2.10 Spring上下文前置處理

源碼以下:

private void prepareContext(ConfigurableApplicationContext context,
            ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments, Banner printedBanner) {
        //設置容器環境,包括各類變量
        context.setEnvironment(environment);
 
        //設置上下文的 bean 生成器和資源加載器
        postProcessApplicationContext(context);
 
        //執行容器中的ApplicationContextInitializer(包括 spring.factories和自定義的實例)
        applyInitializers(context);
 
        //觸發全部 SpringApplicationRunListener 監聽器的 contextPrepared 事件方法
        listeners.contextPrepared(context);
 
        //記錄啓動日誌
        if (this.logStartupInfo) {
            logStartupInfo(context.getParent() == null);
            logStartupProfileInfo(context);
        }
 
        // 添加特定於引導的單例bean
context.getBeanFactory().registerSingleton("springApplicationArguments",
                applicationArguments);
        if (printedBanner != null) {
            context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
        }
        // 加載全部資源
        Set<Object> sources = getAllSources();
        Assert.notEmpty(sources, "Sources must not be empty");
        //加載咱們的啓動類,將啓動類注入容器
        load(context, sources.toArray(new Object[0]));
 
        //觸發全部 SpringApplicationRunListener 監聽器的 contextLoaded 事件方法
        listeners.contextLoaded(context);
    }

這塊會對整個上下文進行一個預處理,好比觸發監聽器的響應事件、加載資源、設置上下文環境等等。

3.2.11 刷新Spring上下文

源碼以下:

private void refreshContext(ConfigurableApplicationContext context) {
        refresh(context);
        if (this.registerShutdownHook) {
            try {
                //向JVM運行時註冊一個關機鉤子,在JVM關機時關閉這個上下文
                context.registerShutdownHook();
            }
            catch (AccessControlException ex) {
                // Not allowed in some environments.
            }
        }
    }

這塊主要作了兩件事:

  1. 經過refresh方法對整個IoC容器的初始化(包括Bean資源的定位、解析、註冊等等)
  2. 經過context.registerShutdownHook()(向JVM運行時註冊一個關機鉤子,在JVM關機時關閉這個上下文,除非它當時已經關閉。)

3.2.12 Spring上下文後置處理

protected void afterRefresh(ConfigurableApplicationContext context,
        ApplicationArguments args) {
}

該方法沒有實現,能夠根據須要作一些定製化的操做。

3.2.13 中止計時監控類

源碼以下:

public void stop() throws IllegalStateException {
        if (this.currentTaskName == null) {
            throw new IllegalStateException("Can't stop StopWatch: it's not running");
        }
        long lastTime = System.currentTimeMillis() - this.startTimeMillis;
        this.totalTimeMillis += lastTime;
        this.lastTaskInfo = new TaskInfo(this.currentTaskName, lastTime);
        if (this.keepTaskList) {
            this.taskList.add(this.lastTaskInfo);
        }
        ++this.taskCount;
        this.currentTaskName = null;
    }

該方法主要是作計時監聽器中止操做,並統計一些任務執行信息。

3.2.14 輸出日誌記錄執行主類名、時間信息

源碼以下:

if (this.logStartupInfo) {
    new StartupInfoLogger(this.mainApplicationClass)
                    .logStarted(getApplicationLog(), stopWatch);
}

用於打印主類信息和時間信息。

3.2.15 發佈應用上下文啓動完成事件

源碼以下:

public void started(ConfigurableApplicationContext context) {
    for (SpringApplicationRunListener listener : this.listeners) {
        listener.started(context);
    }
}

執行全部SpringApplicationRunListener實現的started方法。

3.2.16 執行全部 Runner 運行器

Runner 運行器用於在服務啓動時進行一些業務初始化操做,這些操做只在服務啓動後執行一次。
Spring Boot提供了ApplicationRunner和CommandLineRunner兩種服務接口CommandLineRunner、ApplicationRunner
對比

  • 相同點

    • 二者均在服務啓動完成後執行,而且只執行一次。
    • 二者都能獲取到應用的命令行參數。
    • 二者在執行時機上是一致的(能夠經過Ordered相關的接口或註解來實現自定義執行優先級。)。
  • 不一樣點

    • 雖然二者都是獲取到應用的命令行參數,可是ApplicationRunner獲取到的是封裝後的ApplicationArguments對象,而CommandLine獲取到的是ApplicationArguments中的sourceArgs屬性(List<String>),即原始參數字符串列表(命令行參數列表)。

源碼以下:

private void callRunners(ApplicationContext context, ApplicationArguments args) {
        List<Object> runners = new ArrayList<>();
// 從Spring容器中獲取ApplicationRunner實現類        runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
// 從Spring容器中獲取CommandLineRunner實現類        runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
        //排序
        AnnotationAwareOrderComparator.sort(runners);
        //回調
        for (Object runner : new LinkedHashSet<>(runners)) {
            if (runner instanceof ApplicationRunner) {
                callRunner((ApplicationRunner) runner, args);
            }
            if (runner instanceof CommandLineRunner) {
                callRunner((CommandLineRunner) runner, args);
            }
        }
    }

ApplicationRunner仍是CommandLineRunner,都是在應用啓動完成後執行一次業務初始化代碼,達到的效果也相似,因爲ApplicationRunner的方法參數是ApplicationArguments對象,使用起來更加方便,因此更推薦使用。

3.2.17 發佈應用上下文就緒事件

源碼以下

public void running(ConfigurableApplicationContext context) {
        for (SpringApplicationRunListener listener : this.listeners) {
            listener.running(context);
        }
    }

觸發全部 SpringApplicationRunListener 監聽器的 running 事件方法。

總結

SpirngBoot啓動流程到這裏就分析完了、接下來的文章會針對SpringBoot的特定場景進行分析,但願每一個看到這篇文章的朋友都能有所收穫,同時也歡迎你們多提寶貴意見共同成長。
相關文章
相關標籤/搜索