SpringBoot源碼:啓動過程分析(二) 原

    接着上篇繼續分析 SpringBoot 的啓動過程。

    SpringBoot的版本爲:2.1.0 release,最新版本。

一.時序圖

    一樣的,我們先把時序圖貼上來,方便理解:

 

二.源碼分析

    回顧一下,前面我們分析到了下面這步:

    public static ConfigurableApplicationContext run(Class<?>[] primarySources,
			String[] args) {
        // 前面分析了前一部分,SpringApplication的構造方法,本文分析後一部分的run()方法
		return new SpringApplication(primarySources).run(args);
	}

    SpringApplication#run方法的內容較多,準備刷屏了:

    public ConfigurableApplicationContext run(String... args) {
        // StopWatch是Spring中一個任務執行時間控制的類,記錄了任務的執行時間
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();// 開始時間
		ConfigurableApplicationContext context = null;
        // 創建一個SpringBootExceptionReporter的List,記錄啓動過程中的異常信息
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        // 設置系統屬性 "java.awt.headless" 爲 true
		configureHeadlessProperty();
        // 獲取所有啓動監聽器
		SpringApplicationRunListeners listeners = getRunListeners(args);
        // 調用所有監聽器的starting()
		listeners.starting();
		try {
            // 根據入參創建 ApplicationArguments 對象
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(
					args);
            // 準備應用上下文 environment 對象
			ConfigurableEnvironment environment = prepareEnvironment(listeners,
					applicationArguments);
            // 設置系統屬性 "spring.beaninfo.ignore",是否忽略bean信息
			configureIgnoreBeanInfo(environment);
            // 獲取打印 Banner 對象,也就是應用啓動時候看到控制檯輸出的那個很大的 "SpringBoot"
			Banner printedBanner = printBanner(environment);
            // 創建IOC容器
			context = createApplicationContext();
            // 獲取異常報告,在異常時報告異常信息
			exceptionReporters = getSpringFactoriesInstances(
					SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
            // 準備上下文
			prepareContext(context, environment, listeners, applicationArguments,
					printedBanner);
            // 刷新上下文
			refreshContext(context);
            // 刷新完後
			afterRefresh(context, applicationArguments);
			stopWatch.stop();// 啓動完成,記錄啓動結束時間
			if (this.logStartupInfo) {// 打印詳細啓動時間信息
				new StartupInfoLogger(this.mainApplicationClass)
						.logStarted(getApplicationLog(), stopWatch);
			}
            // 調用所有監聽器的started(ctx)
			listeners.started(context);
            // 調用實現了 ApplicationRunner、CommandLineRunner 的對象的 run 方法
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);// 調用所有監聽器的running()
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;// 返回上下文
	}

    上面從大體上介紹了SpringApplication在run()中做的事情,下面詳細分析每步具體的操作。

    SpringApplication#getRunListeners,獲取所有啓動監聽器,一樣的套路,通過SPI機制,通過工廠創建SpringApplicationRunListener的實例:

    private SpringApplicationRunListeners getRunListeners(String[] args) {
		Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
        // 注意這個地方,我們第二次看到了getSpringFactoriesInstances這個方法,作用就是從類路徑下 META-INF/spring.factories 下加載 SpringFactory 實例,最後傳入 SpringApplicationRunListener.class,創建實例
		return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
				SpringApplicationRunListener.class, types, this, args));
	}

    從 spring.factories 裏面找到如下內容:也就是最後拿到SpringApplicationRunListener的實例是EventPublishingRunListener的對象。

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

    SpringApplication#configureHeadlessProperty,設置系統屬性 "java.awt.headless" 。headless是系統的一種配置模式,在系統可能缺少顯示設備、鍵盤或鼠標這些外設的情況下可以使用該模式,也就是告訴服務器,沒有這些硬件設施,當需要這是設備信息的時候,別慌,我可以使用awt組件通過計算模擬出這些外設的特性。

    private void configureHeadlessProperty() {
        // this.headless在初始化的時候值爲 "true"
		System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, System.getProperty(
				SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
	}

    SpringApplication#prepareEnvironment,準備應用上下文 environment 對象,像設置當前使用的配置文件profile,就在該方法內完成:

    private ConfigurableEnvironment prepareEnvironment(
			SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// Create and configure the environment
		ConfigurableEnvironment environment = getOrCreateEnvironment();
        // 添加 ConversionService 轉換器,保存啓動參數
		configureEnvironment(environment, applicationArguments.getSourceArgs());
        // 調用所有監聽器的environmentPrepared(env)
		listeners.environmentPrepared(environment);
        // 將 environment 綁定到當前Spring上下文
		bindToSpringApplication(environment);
        // 判斷是否用戶自定義Environment
		if (!this.isCustomEnvironment) {
			environment = new EnvironmentConverter(getClassLoader())
					.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
		}
		ConfigurationPropertySources.attach(environment);
		return environment;
	}

    SpringApplication#configureEnvironment,添加 ConversionService 轉換器,保存啓動參數,即最外面main的入參args數組:

    protected void configureEnvironment(ConfigurableEnvironment environment,
			String[] args) {
		if (this.addConversionService) {
			ConversionService conversionService = ApplicationConversionService
					.getSharedInstance();
			environment.setConversionService(
					(ConfigurableConversionService) conversionService);
		}
        // 合併命令行啓動參數到當前 enviroment,這個args就是最外層App啓動傳入的main方法的參數
		configurePropertySources(environment, args);
        // 設置當前運行環境的配置文件(dev/uat/prod)
		configureProfiles(environment, args);
	}

    SpringApplication#configureProfiles ,設置當前運行環境的配置文件,會去查看"spring.profiles.active"中指定的是什麼:

    protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
        // 去找當前激活的Profile是哪個 "spring.profiles.active"
		environment.getActiveProfiles(); // ensure they are initialized
		// But these ones should go first (last wins in a property key clash)
		Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
		profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
		environment.setActiveProfiles(StringUtils.toStringArray(profiles));
	}

    不小心有陷入進去了,再次回到run()。

    接着看後面是獲取Banner。根據環境找到Banner,默認找classpath下面的 banner.txt,Gitee上面很多管理系統打印出各種自定義名稱。實現很簡單,只要你提前製作好banner.txt,放到resources下面,啓動的時候,就會加載你的,覆蓋原始的Banner信息。這部分代碼比較簡單,只要跟進去看下就能看明白,考慮篇幅問題,省略過了。分享一個在線設計Banner.txt的網站

    如果需要關閉Banner輸出,在App裏面調用:

SpringApplication.setBannerMode(Banner.Mode.OFF);// 關掉Banner

    接下來重點看一下 SpringApplication#prepareContext,分析SpringBoot如何準備上下文的:

    private void prepareContext(ConfigurableApplicationContext context,
			ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments, Banner printedBanner) {
        // 給當前 context 設置 environment
		context.setEnvironment(environment);
        // 設置 beanNameGenerator、resourceLoader、ConversionService
		postProcessApplicationContext(context);
        // 獲取所有 ApplicationContextInitializer,調用其 initialize(ctx) 方法
		applyInitializers(context);
        // 調用所有監聽器的contextPrepared(ctx)
		listeners.contextPrepared(context);
		if (this.logStartupInfo) {
            // 打印啓動進程信息
			logStartupInfo(context.getParent() == null);
			logStartupProfileInfo(context);// 打印 profile 信息
		}
		// Add boot specific singleton beans 註冊 SpringBoot 特有的單實例
		ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
		beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
		if (printedBanner != null) {
			beanFactory.registerSingleton("springBootBanner", printedBanner);
		}
		if (beanFactory instanceof DefaultListableBeanFactory) {
            // 設置是否允許重寫 BeanDefinition
			((DefaultListableBeanFactory) beanFactory)
					.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
		}
		// Load the sources
		Set<Object> sources = getAllSources();
		Assert.notEmpty(sources, "Sources must not be empty");
        // 創建 BeanDefinitionLoader,用於加載 BeanDefinition,此處加載應用啓動類有@SpringBootApplication 註解的主類
		load(context, sources.toArray(new Object[0]));
        // 調用所有監聽器的contextLoaded(ctx)
		listeners.contextLoaded(context);
	}

    穩住,快啓動完了。接着看SpringApplication#refreshContext,完成刷新上下文的操作:

    private void refreshContext(ConfigurableApplicationContext context) {
		refresh(context);// 刷新上下文,最後使用了AbstractApplicationContext#refresh方法,進入了Spring的啓動流程
		if (this.registerShutdownHook) {
			try {
                // 註冊停止鉤子,爲了在應用程序shutdown的時候銷燬所有 bean 實例
				context.registerShutdownHook();
			}
			catch (AccessControlException ex) {
				// Not allowed in some environments.
			}
		}
	}

    然後是到了SpringApplication#afterRefresh,這是一個模板方法,父類不提供實現,留給子類發揮想象實現,在context刷新好之後需要做的事情可以在此方法實現中完成。

    最後就是 stopWatch.stop() 停止計時,打印啓動耗時信息,回調監聽器的started(ctx)方法,返回上下文context。

 

    至此,SpringBoot啓動過程分析完成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章