精盡Spring Boot原始碼分析 - SpringApplication 啟動類的啟動過程

月圓吖發表於2021-07-01

該系列文章是筆者在學習 Spring Boot 過程中總結下來的,裡面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼註釋 Spring Boot 原始碼分析 GitHub 地址 進行閱讀

Spring Boot 版本:2.2.x

最好對 Spring 原始碼有一定的瞭解,可以先檢視我的 《死磕 Spring 之 IoC 篇 - 文章導讀》 系列文章

如果該篇內容對您有幫助,麻煩點選一下“推薦”,也可以關注博主,感激不盡~

該系列其他文章請檢視:《精盡 Spring Boot 原始碼分析 - 文章導讀》

概述

我們 Spring Boot 專案的啟動類通常有下面三種方式

// 方式一
@SpringBootApplication
public class Application {

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

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).run(args);
    }
}
// 方式三
@SpringBootApplication
public class Application extends SpringBootServletInitializer {

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

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(Application.class);
    }
}

方式一方式二本質上都是通過呼叫 SpringApplication#run(..) 方法來啟動應用,不同的是方式二通過構建器模式,先構建一個 SpringApplication 例項物件,然後呼叫其 run(..) 方法啟動應用,這種方式可以對 SpringApplication 進行配置,更加的靈活。

我們再來看到方式三,它和方式一差不多,不同的是它繼承了 SpringBootServletInitializer 這個類,作用就是當你的 Spring Boot 專案打成 war 包時能夠放入外部的 Tomcat 容器中執行,如果是 war 包,那上面的 main(...) 方法自然是不需要的,當然,configure(..) 方法也可以不要。

在上篇 《精盡 Spring Boot 原始碼分析 - Jar 包的啟動實現》 文章中講到,通過 java -jar 啟動 Spring Boot 應用時,最終會呼叫我們啟動類的 main(..) 方法,那麼本文主要就是對 SpringApplication 這個類進行分析。至於上面 @SpringBootApplication 註解和方式三的相關內容在後續的文章會講到。

SpringApplicationBuilder

org.springframework.boot.builder.SpringApplicationBuilder,SpringApplication 的構建器,如下:

public class SpringApplicationBuilder {

	private final SpringApplication application;

	private ConfigurableApplicationContext context;

	private final AtomicBoolean running = new AtomicBoolean(false);

	private final Set<Class<?>> sources = new LinkedHashSet<>();

	public SpringApplicationBuilder(Class<?>... sources) {
		this.application = createSpringApplication(sources);
	}
    
    protected SpringApplication createSpringApplication(Class<?>... sources) {
		return new SpringApplication(sources);
	}
    
    public ConfigurableApplicationContext run(String... args) {
		if (this.running.get()) {
			// If already created we just return the existing context
			return this.context;
		}
		configureAsChildIfNecessary(args);
		if (this.running.compareAndSet(false, true)) {
			synchronized (this.running) {
				// If not already running copy the sources over and then run.
				this.context = build().run(args);
			}
		}
		return this.context;
	}
    
    public SpringApplication build() {
		return build(new String[0]);
	}
    
    public SpringApplication build(String... args) {
		configureAsChildIfNecessary(args);
		this.application.addPrimarySources(this.sources);
		return this.application;
	}
}

上面僅列出了 SpringApplicationBuilder 的部分程式碼,它支援對 SpringApplication 進行配置,底層還是通過 SpringApplication 這個類來啟動應用的,不過多的講述,感興趣的可以去看看。

SpringApplication

org.springframework.boot.SpringApplication,Spring 應用啟動器。正如其程式碼上所新增的註釋,它來提供啟動 Spring 應用的功能。

Class that can be used to bootstrap and launch a Spring application from a Java main method.

相關屬性

public class SpringApplication {

	/**
	 * Spring 應用上下文(非 Web 場景)
	 * The class name of application context that will be used by default for non-web environments.
	 */
	public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."
			+ "annotation.AnnotationConfigApplicationContext";

	/**
	 * Spring 應用上下文(Web 場景)
	 * The class name of application context that will be used by default for web environments.
	 */
	public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot."
			+ "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";

	/**
	 * Spring 應用上下文(Reactive 場景)
	 * The class name of application context that will be used by default for reactive web environments.
	 */
	public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework."
			+ "boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";

	/**
	 * 主 Bean(通常為我們的啟動類,優先註冊)
	 */
	private Set<Class<?>> primarySources;
	/**
	 * 來源 Bean(優先註冊)
	 */
	private Set<String> sources = new LinkedHashSet<>();
	/**
	 * 啟動類
	 */
	private Class<?> mainApplicationClass;
	/**
	 * Banner 列印模式
	 */
	private Banner.Mode bannerMode = Banner.Mode.CONSOLE;
	/**
	 * 是否列印應用啟動耗時日誌
	 */
	private boolean logStartupInfo = true;
	/**
	 * 是否接收命令列中的引數
	 */
	private boolean addCommandLineProperties = true;
	/**
	 * 是否設定 ConversionService 型別轉換器
	 */
	private boolean addConversionService = true;
	/**
	 * Banner 物件(用於輸出橫幅)
	 */
	private Banner banner;
	/**
	 * 資源載入物件
	 */
	private ResourceLoader resourceLoader;
	/**
	 * Bean 名稱生成器
	 */
	private BeanNameGenerator beanNameGenerator;
	/**
	 * Spring 應用的環境物件
	 */
	private ConfigurableEnvironment environment;
	/**
	 * Spring 應用上下文的 Class 物件
	 */
	private Class<? extends ConfigurableApplicationContext> applicationContextClass;
	/**
	 * Web 應用的型別(Servlet、Reactive)
	 */
	private WebApplicationType webApplicationType;
	/**
	 * 是否註冊鉤子函式,用於 JVM 關閉時關閉 Spring 應用上下文
	 */
	private boolean registerShutdownHook = true;
	/**
	 * 儲存 ApplicationContextInitializer 物件(主要是對 Spring 應用上下文做一些初始化工作)
	 */
	private List<ApplicationContextInitializer<?>> initializers;
	/**
	 * 儲存 ApplicationListener 監聽器(支援在整個 Spring Boot 的多個時間點進行擴充套件)
	 */
	private List<ApplicationListener<?>> listeners;
	/**
	 * 預設的配置項
	 */
	private Map<String, Object> defaultProperties;
	/**
	 * 額外的 profile
	 */
	private Set<String> additionalProfiles = new HashSet<>();
	/**
	 * 是否允許覆蓋 BeanDefinition
	 */
	private boolean allowBeanDefinitionOverriding;
	/**
	 * 是否為自定義的 Environment 物件
	 */
	private boolean isCustomEnvironment = false;
	/**
	 * 是否支援延遲初始化(需要通過 {@link LazyInitializationExcludeFilter} 過濾)
	 */
	private boolean lazyInitialization = false;
}

上面基本上列出了 SpringApplication 的所有屬性,每個屬性都比較關鍵,大家先有一個印象,後面也可以回過頭來看

構造器

public SpringApplication(Class<?>... primarySources) {
    this(null, primarySources);
}

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    // <1> 設定資源載入器
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    // <2> 設定主要的 Class 類物件,通常是我們的啟動類
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    // <3> 通過 `classpath` 判斷是否存在相應的 Class 類物件,來決定當前 Web 應用的型別(REACTIVE、SERVLET、NONE),預設為 **SERVLET**
    // 不同的型別後續建立的 Environment 型別不同
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    /**
     * <4> 初始化所有 `ApplicationContextInitializer` 型別的物件,並儲存至 `initializers` 集合中
     * 通過類載入器從 `META-INF/spring.factories` 檔案中獲取 ApplicationContextInitializer 型別的類名稱,並進行例項化
     */
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    /**
     * <5> 初始化所有 `ApplicationListener` 型別的物件,並儲存至 `listeners` 集合中
     * 通過類載入器從 `META-INF/spring.factories` 檔案中獲取 ApplicationListener 型別的類名稱,並進行例項化
     * 例如有一個 {@link ConfigFileApplicationListener}
     */
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    // <6> 獲取當前被呼叫的 `main` 方法所屬的 Class 類物件,並設定(主要用於列印日誌)
    this.mainApplicationClass = deduceMainApplicationClass();
}

在我們自己的啟動類中不管是通過哪種方式都是會先建立一個 SpringApplication 例項物件的,可以先看下它的 run(Class<?>, String...) 方法:

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
    return run(new Class<?>[] { primarySource }, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    // 同樣是先建立一個 SpringApplication 物件
    return new SpringApplication(primarySources).run(args);
}

例項化的過程中做了不少事情,如下:

  1. 設定資源載入器,預設為 null,可以通過 SpringApplicationBuilder 設定

  2. 設定 primarySources 為主要的 Class 類物件,通常是我們的啟動類

  3. 通過 classpath 判斷是否存在相應的 Class 類物件,來決定當前 Web 應用的型別(REACTIVE、SERVLET、NONE),預設為 SERVLET,不同的型別後續建立的 Environment 型別不同

    public enum WebApplicationType {
    
    	NONE, SERVLET, REACTIVE;
    
    	private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
    			"org.springframework.web.context.ConfigurableWebApplicationContext" };
    
    	private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";
    
    	private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";
    
    	private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
    
    	private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";
    
    	private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";
    
    	static WebApplicationType deduceFromClasspath() {
    		if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
    				&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
    			return WebApplicationType.REACTIVE;
    		}
    		for (String className : SERVLET_INDICATOR_CLASSES) {
    			if (!ClassUtils.isPresent(className, null)) {
    				return WebApplicationType.NONE;
    			}
    		}
    		return WebApplicationType.SERVLET;
    	}
    }
    

    很簡單,就是依次判斷當前 JVM 中是否存在相關的 Class 類物件,來決定使用哪種 Web 型別,預設是 SERVLET 型別

  4. 初始化所有 ApplicationContextInitializer 型別的物件,並儲存至 initializers 集合中

  5. 初始化所有 ApplicationListener 型別的物件,並儲存至 listeners 集合中,例如 ConfigFileApplicationListenerLoggingApplicationListener

  6. 獲取當前被呼叫的 main 方法所屬的 Class 類物件,並設定(主要用於列印日誌)

    private Class<?> deduceMainApplicationClass() {
        try {
            // 獲取當前的呼叫棧
            StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
            // 對呼叫棧進行遍歷,找到 `main` 方法所在的 Class 類物件並返回
            for (StackTraceElement stackTraceElement : stackTrace) {
                if ("main".equals(stackTraceElement.getMethodName())) {
                    return Class.forName(stackTraceElement.getClassName());
                }
            }
        } catch (ClassNotFoundException ex) {
            // Swallow and continue
        }
        return null;
    }
    

上面的第 45 步都是通過類載入器從 META-INF/spring.factories 檔案中分別獲取 ApplicationContextInitializerApplicationListener 型別的類名稱,然後進行例項化,這個兩種型別的物件都是對 Spring 的一種擴充,像很多框架整合 Spring Boot 都可以通過自定義的 ApplicationContextInitializer 對 ApplicationContext 進行一些初始化,通過 ApplicationListener 在 Spring 應用啟動的不同階段來織入一些功能

getSpringFactoriesInstances 方法

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
    return getSpringFactoriesInstances(type, new Class<?>[] {});
}

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
    // <1> 獲取類載入器
    ClassLoader classLoader = getClassLoader();
    // Use names and ensure unique to protect against duplicates
    // <2> 通過類載入器從 `META-INF/spring.factories` 檔案中獲取型別為 `type` 的類名稱
    Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    // <3> 為上一步獲取到的所有類名稱建立對應的例項物件
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    // <4> 通過 `@Order` 註解進行排序
    AnnotationAwareOrderComparator.sort(instances);
    // <5> 返回排序後的 `type` 型別的例項物件
    return instances;
}

過程比較簡單,如下:

  1. 獲取類載入器

  2. 通過類載入器從所有 META-INF/spring.factories 檔案中獲取型別為 type 的類名稱,這裡的 SpringFactoriesLoader 是 Spring 中的一個類

  3. 為上一步獲取到的所有類名稱建立對應的例項物件

    private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes,
            ClassLoader classLoader, Object[] args, Set<String> names) {
        List<T> instances = new ArrayList<>(names.size());
        // 遍歷所有的類名稱,依次建立例項物件,一併返回
        for (String name : names) {
            try {
                // 獲取對應的 Class 類物件
                Class<?> instanceClass = ClassUtils.forName(name, classLoader);
                // 對 Class 類物件進行校驗,判斷型別是否匹配
                Assert.isAssignable(type, instanceClass);
                // 獲取指定入參型別(ConfigurableApplicationContext)的構造器
                Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
                // 通過構造器建立一個例項物件
                T instance = (T) BeanUtils.instantiateClass(constructor, args);
                instances.add(instance);
            }
            catch (Throwable ex) {
                throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex);
            }
        }
        return instances;
    }
    
  4. 通過 @Order 註解進行排序

  5. 返回排序後的 type 型別的例項物件

SpringApplication#run 方法

上面已經講述了 SpringApplication 的例項化過程,那麼接下來就是呼叫它的 run(String... args) 方法來啟動 Spring 應用,該過程如下:

public ConfigurableApplicationContext run(String... args) {
    // <1> 建立 StopWatch 物件並啟動,主要用於統計當前方法執行過程的耗時
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    // <2> 設定 `java.awt.headless` 屬性,和 AWT 相關,暫時忽略
    configureHeadlessProperty();
    // <3> 初始化所有 `SpringApplicationRunListener` 型別的物件,並全部封裝到 `SpringApplicationRunListeners` 物件中
    SpringApplicationRunListeners listeners = getRunListeners(args);
    // <4> 啟動所有的 `SpringApplicationRunListener` 監聽器
    // 例如 `EventPublishingRunListener` 會廣播 ApplicationEvent 應用正在啟動的事件,
    // 它裡面封裝了所有的 `ApplicationListener` 物件,那麼此時就可以通過它們做一些初始化工作,進行擴充
    listeners.starting();
    try {
        // <5> 建立一個應用引數物件,將 `main(String[] args)` 方法的 `args` 引數封裝起來,便於後續使用
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // <6> 準備好當前應用 Environment 環境,這裡會載入出所有的配置資訊,包括 `application.yaml` 和外部的屬性配置
        ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);

        configureIgnoreBeanInfo(environment);
        // <7> 列印 banner 內容
        Banner printedBanner = printBanner(environment);
        // <8> 對 `context` (Spring 上下文)進行例項化
        // 例如 Servlet(預設)會建立一個 AnnotationConfigServletWebServerApplicationContext 例項物件
        context = createApplicationContext();
        // <9> 獲取異常報告器,通過類載入器從 `META-INF/spring.factories` 檔案中獲取 SpringBootExceptionReporter 型別的類名稱,並進行例項化
        exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context);
        // <10> 對 Spring 應用上下文做一些初始化工作,例如執行 ApplicationContextInitializer#initialize(..) 方法
        prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        /**
         * <11> 重新整理 Spring 應用上下文,在這裡會完成所有 Spring Bean 的初始化,同時會初始化好 Servlet 容器,例如 Tomcat
         * 這一步涉及到 Spring IoC 的所有內容,參考[死磕Spring之IoC篇 - Spring 應用上下文 ApplicationContext](https://www.cnblogs.com/lifullmoon/p/14453083.html)
         * 在 {@link ServletWebServerApplicationContext#onRefresh()} 方法中會建立一個 Servlet 容器(預設為 Tomcat),也就是當前 Spring Boot 應用所執行的 Web 環境
         */
        refreshContext(context);
        // <12> 完成重新整理 Spring 應用上下文的後置操作,空實現,擴充套件點
        afterRefresh(context, applicationArguments);
        // <13> 停止 StopWatch,統計整個 Spring Boot 應用的啟動耗時,並列印
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
        }
        // <14> 對所有的 SpringApplicationRunListener 監聽器進行廣播,釋出 ApplicationStartedEvent 應用已啟動事件
        listeners.started(context);
        // <15> 回撥 IoC 容器中所有 ApplicationRunner 和 CommandLineRunner 型別的啟動器
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        // 處理異常,同時會將異常傳送至上面第 `9` 步獲取到的異常報告器
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        // <16> 對所有的 SpringApplicationRunListener 監聽器進行廣播,釋出 ApplicationReadyEvent 應用已就緒事件
        listeners.running(context);
    }
    catch (Throwable ex) {
        // 處理異常,同時會將異常傳送至上面第 `9` 步獲取到的異常報告器
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

整個啟動過程做了很多事情,主要過程如下:

  1. 建立 StopWatch 物件並啟動,主要用於統計當前方法執行過程的耗時

  2. 設定 java.awt.headless 屬性,和 AWT 相關,暫時忽略

  3. 呼叫 getRunListeners(..) 方法,初始化所有 SpringApplicationRunListener 型別的物件,並全部封裝到 SpringApplicationRunListeners 物件中

  4. 啟動所有的 SpringApplicationRunListener 監聽器,例如 EventPublishingRunListener 會廣播 ApplicationEvent 應用正在啟動的事件,它裡面封裝了所有的 ApplicationListener 物件,那麼此時就可以通過它們做一些初始化工作,進行擴充

  5. 建立一個 ApplicationArguments 應用引數物件,將 main(String[] args) 方法的 args 引數封裝起來,便於後續使用

  6. 呼叫 prepareEnvironment(..) 方法,準備好當前應用 Environment 環境,這裡會載入出所有的配置資訊,包括 application.yaml 和外部的屬性配置

  7. 呼叫 printBanner(..) 方法,列印 banner 內容

  8. 呼叫 createApplicationContext() 方法, 對 context(Spring 上下文)進行例項化,例如 Servlet(預設)會建立一個 AnnotationConfigServletWebServerApplicationContext 例項物件

  9. 獲取異常報告器,通過類載入器從 META-INF/spring.factories 檔案中獲取 SpringBootExceptionReporter 型別的類名稱,並進行例項化

  10. 呼叫 prepareContext(..) 方法,對 Spring 應用上下文做一些初始化工作,例如執行 ApplicationContextInitializer#initialize(..) 方法

  11. 呼叫 refreshContext(..) 方法,重新整理 Spring 應用上下文,在這裡會完成所有 Spring Bean 的初始化,同時會初始化好 Servlet 容器,例如 Tomcat

    這一步涉及到 Spring IoC 的所有內容,參考 《死磕Spring之IoC篇 - Spring 應用上下文 ApplicationContext》

    ServletWebServerApplicationContext#onRefresh() 方法中會建立一個 Servlet 容器(預設為 Tomcat),也就是當前 Spring Boot 應用所執行的 Web 環境

  12. 呼叫 afterRefresh(..) 方法,完成重新整理 Spring 應用上下文的後置操作,空實現,擴充套件點

  13. 停止 StopWatch,統計整個 Spring Boot 應用的啟動耗時,並列印

  14. 對所有的 SpringApplicationRunListener 監聽器進行廣播,釋出 ApplicationStartedEvent 應用已啟動事件,通常只有一個 EventPublishingRunListener 物件

  15. 回撥 IoC 容器中所有 ApplicationRunner 和 CommandLineRunner 型別的啟動器,預設情況下沒有,先暫時忽略

  16. 對所有的 SpringApplicationRunListener 監聽器進行廣播,釋出 ApplicationReadyEvent 應用已就緒事件,通常只有一個 EventPublishingRunListener 物件

啟動 Spring 應用的整個主流程清晰明瞭,先準備好當前應用的 Environment 環境,然後建立 Spring ApplicationContext 應用上下文。

該方法的整個過程更多的細節在於上面每一步呼叫的方法,抽絲剝繭,對於非常複雜的地方會另起文章進行分析

3. getRunListeners 方法

getRunListeners(String[] args) 方法,初始化所有 SpringApplicationRunListener 型別的物件,並全部封裝到 SpringApplicationRunListeners 物件中,如下:

private SpringApplicationRunListeners getRunListeners(String[] args) {
    // 指定例項化物件所使用的構造器的入參型別
    Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
    // 通過類載入器從 `META-INF/spring.factories` 檔案中獲取 SpringApplicationRunListener 型別的類名稱,並進行例項化
    return new SpringApplicationRunListeners(logger,
            getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

這裡同樣呼叫上面講過的 getSpringFactoriesInstances(..) 方法,通過類載入器從 META-INF/spring.factories 檔案中獲取 SpringApplicationRunListener 型別的類名稱,並進行例項化

最後會將它們全部封裝到 SpringApplicationRunListeners 物件中,就是把一個 List 封裝到一個物件中,不過預設情況只有一個 EventPublishingRunListener 物件,其內部又封裝了 SpringApplication 中的所有 ApplicationListener 應用監聽器們,例如 ConfigFileApplicationListenerLoggingApplicationListener

6. prepareEnvironment 方法

prepareEnvironment(SpringApplicationRunListeners, ApplicationArguments) 方法,準備好當前應用 Environment 環境,載入出所有的配置資訊,包括 application.yaml 和外部的屬性配置,如下:

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments) {
    // Create and configure the environment
    // <1> 根據 Web 應用的型別建立一個 StandardEnvironment 物件 `environment`
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    // <2> 為 `environment` 配置預設屬性(如果有)並設定需要啟用的 `profile` 們
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    // <3> 將當前 `environment` 的 MutablePropertySources 封裝成 SpringConfigurationPropertySources 新增到 MutablePropertySources 首部
    ConfigurationPropertySources.attach(environment);
    /**
     * <4> 對所有的 SpringApplicationRunListener 廣播 ApplicationEvent 應用環境已準備好的事件,這一步比較複雜
     * 例如 Spring Cloud 的 BootstrapApplicationListener 監聽到該事件會建立一個 ApplicationContext 作為當前 Spring 應用上下文的父容器,同時會讀取 `bootstrap.yml` 檔案的資訊
     * {@link ConfigFileApplicationListener} 監聽到該事件然後去解析 `application.yml` 等應用配置檔案的配置資訊
     */
    listeners.environmentPrepared(environment);
    // <5> 將 `environment` 繫結到當前 SpringApplication 上
    bindToSpringApplication(environment);
    // <6> 如果不是自定義的 Environment 則需要根據 Web 應用型別轉換成對應 Environment 型別
    if (!this.isCustomEnvironment) {
        environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
                deduceEnvironmentClass());
    }
    // <7> 再次進行上面第 `3` 步的處理過程,防止上面幾步對上面的 PropertySources 有修改
    ConfigurationPropertySources.attach(environment);
    // <8> 返回準備好的 `environment`
    return environment;
}

該過程如下:

  1. 根據 Web 應用的型別建立一個 StandardEnvironment 物件 environment

    private ConfigurableEnvironment getOrCreateEnvironment() {
        if (this.environment != null) {
            return this.environment;
        }
        switch (this.webApplicationType) {
        case SERVLET:
            return new StandardServletEnvironment();
        case REACTIVE:
            return new StandardReactiveWebEnvironment();
        default:
            return new StandardEnvironment();
        }
    }
    
  2. environment 配置預設屬性(如果有)並設定需要啟用的 profile

    protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
        if (this.addConversionService) {
            // <1> 獲取 ConversionService 型別轉換器並設定到 Environment 物件中
            ConversionService conversionService = ApplicationConversionService.getSharedInstance();
            environment.setConversionService((ConfigurableConversionService) conversionService);
        }
        // <2> 配置屬性源裡面的屬性
        // 例如有預設屬性則將他們新增至最後,命令列中的引數則新增至最前面
        configurePropertySources(environment, args);
        // <3> 設定需要啟用的 `profile` 們
        // 例如通過 `-Dspring.profiles.active=dev` 配置需要啟用的環境
        configureProfiles(environment, args);
    }
    
    protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
        MutablePropertySources sources = environment.getPropertySources();
        // 如果預設配置屬性不為空則新增至最後
        if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
            sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties));
        }
        // 如果 `main` 方法有入參
        if (this.addCommandLineProperties && args.length > 0) {
            String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
            if (sources.contains(name)) {
                PropertySource<?> source = sources.get(name);
                CompositePropertySource composite = new CompositePropertySource(name);
                composite.addPropertySource(
                        new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
                composite.addPropertySource(source);
                sources.replace(name, composite);
            }
            else {
                // 將命令列中的入參新增至最前面
                // 例如 'java -jar xxx.jar --spring.profiles.active=dev',那麼這裡就可以獲取到
                sources.addFirst(new SimpleCommandLinePropertySource(args));
            }
        }
    }
    

    可以看到會將 main(String[] args) 方法入參中的 -- 開頭的引數設定到 Environment 中

  3. 將當前 environment 的 MutablePropertySources 封裝成 SpringConfigurationPropertySources 新增到 MutablePropertySources 首部

  4. 對所有的 SpringApplicationRunListener 廣播 ApplicationEvent 應用環境已準備好的事件,這一步比較複雜,例如 Spring Cloud 的 BootstrapApplicationListener 監聽到該事件會建立一個 ApplicationContext 作為當前 Spring 應用上下文的父容器,同時會讀取 bootstrap.yml 檔案的資訊

    這裡會有一個 ConfigFileApplicationListener 監聽到該事件然後去解析 application.yml 等應用配置檔案的配置資訊

  5. environment 繫結到當前 SpringApplication 上

  6. 如果不是自定義的 Environment 則需要根據 Web 應用型別轉換成對應 Environment 型別

  7. 再次進行上面第 3 步的處理過程,防止上面幾步對上面的 PropertySources 有修改

  8. 返回準備好的 environment

該方法準備好了當前應用 Environment 環境,主要在於上面第 4 步,是 ApplicationListener 監聽器的一個擴充套件點,在這裡會載入出所有的配置資訊,包括 application.yml 和外部配置,解析配置的過程比較複雜,在後面的文章中單獨分析

8. createApplicationContext 方法

createApplicationContext() 方法,對 context(Spring 上下文)進行例項化,如下:

protected ConfigurableApplicationContext createApplicationContext() {
    Class<?> contextClass = this.applicationContextClass;
    if (contextClass == null) {
        try {
            switch (this.webApplicationType) {
            case SERVLET:
                // org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext
                contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
                break;
            case REACTIVE:
                // org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext
                contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
                break;
            default:
                // org.springframework.context.annotation.AnnotationConfigApplicationContext
                contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
            }
        }
        catch (ClassNotFoundException ex) {
            throw new IllegalStateException(
                    "Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
        }
    }
    // 例項化這個 Class 物件
    return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

不同的應用型別建立不同的 Spring 應用上下文物件:

  • SERVLET(預設是這個):AnnotationConfigServletWebServerApplicationContext
  • REACTIVEAnnotationConfigReactiveWebServerApplicationContext
  • DEFAULTAnnotationConfigApplicationContext

10. prepareContext 方法

prepareContext(ConfigurableApplicationContext, ConfigurableEnvironment, SpringApplicationRunListeners, ApplicationArguments, Banner) 方法,對 Spring 應用上下文做一些初始化工作,如下:

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
        SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
    // <1> 為 Spring 應用上下文設定 Environment 環境
    context.setEnvironment(environment);
    // <2> 將一些工具 Bean 設定到 Spring 應用上下文中,供使用
    postProcessApplicationContext(context);
    // <3> 通知 ApplicationContextInitializer 對 Spring 應用上下文進行初始化工作
    // 參考 SpringApplication 構造方法
    applyInitializers(context);
    // <4> 對所有 SpringApplicationRunListener 進行廣播,釋出 ApplicationContextInitializedEvent 初始化事件
    listeners.contextPrepared(context);
    if (this.logStartupInfo) {
        logStartupInfo(context.getParent() == null);
        logStartupProfileInfo(context);
    }
    // Add boot specific singleton beans
    // <5> 嚮應用上下文註冊 `main(String[])` 方法的引數 Bean 和 Banner 物件
    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
    beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
    if (printedBanner != null) {
        beanFactory.registerSingleton("springBootBanner", printedBanner);
    }
    if (beanFactory instanceof DefaultListableBeanFactory) {
        ((DefaultListableBeanFactory) beanFactory)
                .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
    }
    if (this.lazyInitialization) {
        context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
    }
    // Load the sources
    // <6> 獲取 `primarySources`(例如你的啟動類)和 `sources`(例如 Spring Cloud 中的 @BootstrapConfiguration)源物件
    Set<Object> sources = getAllSources();
    // 應用上下文的源物件不能為空
    Assert.notEmpty(sources, "Sources must not be empty");
    // <7> 將上面的源物件載入成 BeanDefinition 並註冊
    load(context, sources.toArray(new Object[0]));
    // <8> 對所有的 SpringApplicationRunListener 廣播 ApplicationPreparedEvent 應用已準備事件
    // 會把 ApplicationListener 新增至 Spring 應用上下文中
    listeners.contextLoaded(context);
}

該過程如下:

  1. 為 Spring 應用上下文設定 Environment 環境

  2. 將一些工具 Bean 設定到 Spring 應用上下文中,供使用

    protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
        if (this.beanNameGenerator != null) {
            // 註冊 Bean 名稱生成器
            context.getBeanFactory().registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR,
                    this.beanNameGenerator);
        }
        if (this.resourceLoader != null) {
            if (context instanceof GenericApplicationContext) {
                // 設定資源載入器
                ((GenericApplicationContext) context).setResourceLoader(this.resourceLoader);
            }
            if (context instanceof DefaultResourceLoader) {
                ((DefaultResourceLoader) context).setClassLoader(this.resourceLoader.getClassLoader());
            }
        }
        if (this.addConversionService) {
            // 設定型別轉換器
            context.getBeanFactory().setConversionService(ApplicationConversionService.getSharedInstance());
        }
    }
    
  3. 通知 ApplicationContextInitializer 對 Spring 應用上下文進行初始化工作

    protected void applyInitializers(ConfigurableApplicationContext context) {
        for (ApplicationContextInitializer initializer : getInitializers()) {
            Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
                    ApplicationContextInitializer.class);
            Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
            // 執行 Spring 應用上下文初始器
            // 例如 ContextIdApplicationContextInitializer 會向 Spring 應用上下文註冊一個 ContextId 物件
            initializer.initialize(context);
        }
    }
    
  4. 對所有 SpringApplicationRunListener 進行廣播,釋出 ApplicationContextInitializedEvent 初始化事件

  5. 向 Spring 應用上下文註冊 main(String[]) 方法的引數 Bean 和 Banner 物件

  6. 獲取 primarySources(例如你的啟動類)和 sources(例如 Spring Cloud 中的 @BootstrapConfiguration)源物件,沒有的話會丟擲異常

  7. 將上面的源物件載入成 BeanDefinition 並註冊

  8. 對所有的 SpringApplicationRunListener 廣播 ApplicationPreparedEvent 應用已準備事件,會把 ApplicationListener 新增至 Spring 應用上下文中

通過上面的第 6 步你就知道為什麼我們的啟動類裡面一定得有一個入參為啟動類的 Class 物件了

11. refreshContext 方法

refreshContext(ConfigurableApplicationContext) 方法,重新整理 Spring 應用上下文,在這裡會完成所有 Spring Bean 的初始化,同時會初始化好 Servlet 容器,例如 Tomcat,該方法涉及到 Spring IoC 的所有內容,參考 《死磕Spring之IoC篇 - Spring 應用上下文 ApplicationContext》

private void refreshContext(ConfigurableApplicationContext context) {
    if (this.registerShutdownHook) {
        try {
            // 為當前 Spring 應用上下文註冊一個鉤子函式
            // 在 JVM 關閉時先關閉 Spring 應用上下文
            context.registerShutdownHook();
        }
        catch (AccessControlException ex) {
            // Not allowed in some environments.
        }
    }
    // 呼叫 AbstractApplicationContext#refresh() 方法,重新整理 Spring 上下文
    refresh(context);
}

protected void refresh(ApplicationContext applicationContext) {
    Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
    ((AbstractApplicationContext) applicationContext).refresh();
}

該方法主要是呼叫 AbstractApplicationContext#refresh() 方法,重新整理 Spring 應用上下文,整個過程牽涉到 Spring 的所有內容,之前的一系列文章已經分析過,關於更多的細節這裡不展開談論,當然,這個過程會有對 @SpingBootApplication 註解的解析

根據 8. createApplicationContext 方法 方法中講到,我們預設情況下是 SERVLET 應用型別,也就是建立一個 AnnotationConfigServletWebServerApplicationContext 物件,在其父類 ServletWebServerApplicationContext 中重寫了 onRefresh() 方法,會建立一個 Servlet 容器(預設為 Tomcat),也就是當前 Spring Boot 應用所執行的 Web 環境,這部分內容放在後面的文章單獨分析。

SpringApplicationRunListeners

org.springframework.boot.SpringApplicationRunListeners,對 SpringApplicationRunListener 陣列的封裝

class SpringApplicationRunListeners {

	private final Log log;

	/**
	 * 封裝的所有 SpringApplicationRunListener
	 * Spring Boot 在 META-INF/spring.factories 只配置 {@link EventPublishingRunListener} 監聽器
	 */
	private final List<SpringApplicationRunListener> listeners;

	SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners) {
		this.log = log;
		this.listeners = new ArrayList<>(listeners);
	}

	void starting() {
		for (SpringApplicationRunListener listener : this.listeners) {
			listener.starting();
		}
	}

	void environmentPrepared(ConfigurableEnvironment environment) {
		for (SpringApplicationRunListener listener : this.listeners) {
			listener.environmentPrepared(environment);
		}
	}

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

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

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

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

	void failed(ConfigurableApplicationContext context, Throwable exception) {
		for (SpringApplicationRunListener listener : this.listeners) {
			callFailedListener(listener, context, exception);
		}
	}

	private void callFailedListener(SpringApplicationRunListener listener, ConfigurableApplicationContext context,
			Throwable exception) {
		try {
			listener.failed(context, exception);
		}
		catch (Throwable ex) {
			if (exception == null) {
				ReflectionUtils.rethrowRuntimeException(ex);
			}
			if (this.log.isDebugEnabled()) {
				this.log.error("Error handling failed", ex);
			}
			else {
				String message = ex.getMessage();
				message = (message != null) ? message : "no error message";
				this.log.warn("Error handling failed (" + message + ")");
			}
		}
	}
}

比較簡單,就是封裝了多個 SpringApplicationRunListener 物件,對於不同型別的事件,呼叫其不同的方法

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

可以在 META-INF/spring.factories 檔案中看到,只有一個 EventPublishingRunListener 物件

EventPublishingRunListener

org.springframework.boot.context.event.EventPublishingRunListener,實現了 SpringApplicationRunListener 介面,事件廣播器,釋出不同型別的事件

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {

	/**
	 * Spring Boot 應用的啟動類
	 */
	private final SpringApplication application;

	/**
	 * 啟動類 `main(String[])` 方法的入參
	 */
	private final String[] args;

	/**
	 * 事件廣播器,包含了所有的 `META-INF/spring.factories` 檔案中配置的 {@link ApplicationListener} 監聽器
	 */
	private final SimpleApplicationEventMulticaster initialMulticaster;

	public EventPublishingRunListener(SpringApplication application, String[] args) {
		this.application = application;
		this.args = args;
		this.initialMulticaster = new SimpleApplicationEventMulticaster();
		// 在例項化 SpringApplication 的過程中會從 `META-INF/spring.factories` 檔案中獲取 ApplicationListener 型別的類名稱,並進行例項化
		// 這裡會將他們新增至廣播器中
		for (ApplicationListener<?> listener : application.getListeners()) {
			this.initialMulticaster.addApplicationListener(listener);
		}
	}

	@Override
	public int getOrder() {
		return 0;
	}

	@Override
	public void starting() {
		// 廣播 Spring Boot 應用正在啟動事件
		this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
	}

	@Override
	public void environmentPrepared(ConfigurableEnvironment environment) {
		// 廣播 Spring Boot 應用的 Environment 環境已準備事件
		this.initialMulticaster
				.multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
	}

	@Override
	public void contextPrepared(ConfigurableApplicationContext context) {
		// 廣播 Spring Boot 應用的 Spring 上下文已初始化事件
		this.initialMulticaster
				.multicastEvent(new ApplicationContextInitializedEvent(this.application, this.args, context));
	}

	@Override
	public void contextLoaded(ConfigurableApplicationContext context) {
		// 將所有的 ApplicationListener 新增至 Spring 應用上下文
		for (ApplicationListener<?> listener : this.application.getListeners()) {
			if (listener instanceof ApplicationContextAware) {
				((ApplicationContextAware) listener).setApplicationContext(context);
			}
			context.addApplicationListener(listener);
		}
		// 廣播 Spring Boot 應用的 Spring 上下文已準備事件
		this.initialMulticaster.multicastEvent(new ApplicationPreparedEvent(this.application, this.args, context));
	}

	@Override
	public void started(ConfigurableApplicationContext context) {
		// 廣播 Spring Boot 應用的 Spring 上下文已啟動事件
		context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context));
	}

	@Override
	public void running(ConfigurableApplicationContext context) {
		// 廣播 Spring Boot 應用的 Spring 上下文準備就緒事件
		context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context));
	}

	@Override
	public void failed(ConfigurableApplicationContext context, Throwable exception) {
		ApplicationFailedEvent event = new ApplicationFailedEvent(this.application, this.args, context, exception);
		if (context != null && context.isActive()) {
			// Listeners have been registered to the application context so we should
			// use it at this point if we can
			context.publishEvent(event);
		}
		else {
			// An inactive context may not have a multicaster so we use our multicaster to
			// call all of the context's listeners instead
			if (context instanceof AbstractApplicationContext) {
				for (ApplicationListener<?> listener : ((AbstractApplicationContext) context)
						.getApplicationListeners()) {
					this.initialMulticaster.addApplicationListener(listener);
				}
			}
			this.initialMulticaster.setErrorHandler(new LoggingErrorHandler());
			this.initialMulticaster.multicastEvent(event);
		}
	}
}

比較簡單,關鍵在於內部的 SimpleApplicationEventMulticaster 事件廣播器,裡面包含了所有的 META-INF/spring.factories 檔案中配置的 ApplicationListener 監聽器,不同的方法釋出不同的事件,進行廣播

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

可以看到 Spring Boot 配置了許多個 ApplicationListener,後續文章會對 ConfigFileApplicationListener 和 LoggingApplicationListener 進行簡單的分析

總結

Spring Boot 應用打成 jar 包後的啟動都是通過 SpringApplication#run(String... args) 這個方法來啟動整個 Spring 應用的,流程大致如下:

  1. META-INF/spring.factories 檔案中載入出相關 Class 物件,並進行例項化,例如 ApplicationContextInitializerSpringApplicationRunListenerApplicationListener 物件
  2. 準備好當前 Spring 應用的 Environment 環境,這裡會解析 application.yml 以及外部配置
  3. 建立一個 ApplicationContext 應用上下文物件,預設 SERVLET 型別下建立 AnnotationConfigServletWebServerApplicationContext 物件
  4. 呼叫 AbstractApplication#refresh() 方法,重新整理 Spring 應用上下文,也就是之前一系列 Spring 相關的文章所講述的內容

整個過程有許多個擴充套件點是通過監聽器機制實現的,在不同階段廣播不同型別的事件,此時 ApplicationListener 就可進行相關的操作

在上面第 4 步中,SERVLET 應用型別下的 Spring 應用上下文會建立一個 Servlet 容器(預設為 Tomcat)

更多的細節在後續文章依次進行分析

相關文章