Spring Boot -- 啟動流程分析之SpringApplication


我們在開發Spring Boot程序的時候,我們只需要在啟動類上加入@SpringBootApplication注解,然后運行SpringApplication.run(),這樣Spring容器就運行起來了。

@SpringBootApplication(scanBasePackages={"com.jnu.example"}) @CoreMapperScan @EnableAspectAutoProxy public class App { public static void main(String[] args) { SpringApplication.run(BlogApplication.class, args); } }

那么問題來了,相比最初Spring MVC繁瑣的xml的配置方式,現在只需要簡單幾行代碼,Spring容器就可以啟動起來,我們就可以從容器中獲取到bean,Spring Boot內部是如何做到的呢?

下圖是我繪制的Spring啟動的流程圖,這個圖是個精簡版,還有許多不完善的地方,后續幾篇博客我將會通過源碼分析來解讀Spring的啟動流程:

其中源碼版本以2.2.2.RELEASE為例:

    <!--  spring-boot-starter-parent  整合第三方常用框架依賴信息(各種依賴信息)-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
    </parent>

一、SpringApplication准備階段(構造函數)

SpringApplication 在運行前做了一系列的准備工作,如:推斷 Web 應用類型、加載 Spring 上下文初始器、事件監聽器等。接下來,就通過源碼的方式進行學習。

我們首先進入SpringApplication的靜態幫助方法run里面,該方法接受兩個參數:

    /** * Static helper that can be used to run a {@link SpringApplication} from the * specified source using default settings. * @param primarySource the primary source to load * @param args the application arguments (usually passed from a Java main method) * @return the running {@link ApplicationContext} */
    public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) { return run(new Class<?>[] { primarySource }, args); }
  • primarySource:要加載的主要配置類,也就是被@SpringBootApplication注解的App類;
  • args:一個可變數組,保存的是應用程序的運行參數,具體可以指定哪些屬性,可以查看官網說明:Common Application properties

接下來,調用了另一個靜態幫助方法run,並將primarySource轉換為數組參數傳入;

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

這里通過入參primarySources創建了一個SpringApplication對象,其中,准備階段的工作皆在 SpringApplication 的構造器中處理:

    /** * Create a new {@link SpringApplication} instance. The application context will load * beans from the specified primary sources (see {@link SpringApplication class-level} * documentation for details. The instance can be customized before calling * {@link #run(String...)}. * @param primarySources the primary bean sources * @see #run(Class, String[]) * @see #SpringApplication(ResourceLoader, Class...) * @see #setSources(Set) */
    public SpringApplication(Class<?>... primarySources) { this(null, primarySources); }

這里調用了一個構造方法,然后初始化primarySource,webApplicationType、initializers、listeners、mainApplicationClass等字段:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { // resourceLoader 主要用來獲取 Resource 及 ClassLoader。這里值為 null
        this.resourceLoader = resourceLoader; // 斷言primarySources不能為null,否則報錯
        Assert.notNull(primarySources, "PrimarySources must not be null"); // primarySources是SpringApplication.run的參數,存放的是主配置類
        this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); // 進行Web應用的類型推斷
        this.webApplicationType = WebApplicationType.deduceFromClasspath(); // 加載應用上下文初始化器 initializer
        setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); // 加載應用事件監聽器 listener
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); // 推斷引導類,也就是找到入口類
        this.mainApplicationClass = deduceMainApplicationClass(); }

1.1、推斷Web應用類型

// 進行Web應用的類型推斷
this.webApplicationType = WebApplicationType.deduceFromClasspath();

SpringApplication允許指定應用的類型,大體上分為Web應用和非Web應用。從Spring Boot2.0開始,Web應用又可以分為Servlet Web和Reactive Web。而在准備階段,是通過檢查當前ClassPath下某些Class是否存在,從而推導Web應用的類型:

/** * An enumeration of possible types of web application. * * @author Andy Wilkinson * @author Brian Clozel * @since 2.0.0 */
public enum WebApplicationType { /** * The application should not run as a web application and should not start an * embedded web server. */ NONE, /** * The application should run as a servlet-based web application and should start an * embedded servlet web server. */ SERVLET, /** * The application should run as a reactive web application and should start an * embedded reactive web server. */ 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; } static WebApplicationType deduceFromApplicationContext(Class<?> applicationContextClass) { if (isAssignable(SERVLET_APPLICATION_CONTEXT_CLASS, applicationContextClass)) { return WebApplicationType.SERVLET; } if (isAssignable(REACTIVE_APPLICATION_CONTEXT_CLASS, applicationContextClass)) { return WebApplicationType.REACTIVE; } return WebApplicationType.NONE; } private static boolean isAssignable(String target, Class<?> type) { try { return ClassUtils.resolveClassName(target, null).isAssignableFrom(type); } catch (Throwable ex) { return false; } } }

可以看到,在方法中利用 ClassUtils.isPresent 進行判斷, 當DispatcherHandler存在,而DispatcherServlet和ServletContainer不存在時,則當前應用推導為 Reactive web 類型;當 Servlet 和 ConfigurableWebApplicationContext 不存在時,當前應用為非 Web 類型;其他的則為 Servlet Web 類型。

注:Reactive:Reactive響應式編程是一種新的編程風格,其特點是異步或並發、事件驅動、推送PUSH機制以及觀察者模式的衍生。

該函數執行完后,結果如下:

1.2、加載應用上下文初始器ApplicationContextInitializer

setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class))

接着進入加載Spring應用上下文初始器的過程;

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) { ClassLoader classLoader = getClassLoader(); // Use names and ensure unique to protect against duplicates
        Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader)); List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names); AnnotationAwareOrderComparator.sort(instances); return instances; }

可以看到,這里是通過 Spring 工廠加載機制 SpringFactoriesLoader.loadFactoryNames(type, classLoader) 方法獲取,采用這種方式可以將非本項目的外部包的bean加載到Spring容器中,比如一些第三方模塊采用這種方式實現@Enable模塊的功能,具體可以看考博客:Spring Boot自定義Starter

該方法是從項目引用的所有的jar的 META-INF/spring.factories 資源中獲取key為 ApplicationContextInitializer 的實現類集合,如下是 spring-boot-autoconfigure 包下的 spring.factories 文件:

# Initializers org.springframework.context.ApplicationContextInitializer=\ org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\ org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

這里獲取的就是 SharedMetadataReaderFactoryContextInitializer 和 ConditionEvaluationReportLoggingListener 上下文初始化器,接下來通過 createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names) 方法初始化這些實現類:

 @SuppressWarnings("unchecked") 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<?> instanceClass = ClassUtils.forName(name, classLoader); Assert.isAssignable(type, instanceClass); 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; }

這里先通過 BeanUtils.instantiate 初始化這些類,然后將初始化的類保存至List進行返回。

並進行排序操作,最后添加到SpringApplication的initializers集合變量中。至此,該流程結束。 

    /** * Sets the {@link ApplicationContextInitializer} that will be applied to the Spring * {@link ApplicationContext}. * @param initializers the initializers to set */
    public void setInitializers(Collection<? extends ApplicationContextInitializer<?>> initializers) { this.initializers = new ArrayList<>(initializers); }

我們舉例來看看初始器中的內容,如SharedMetadataReaderFactoryContextInitializer:

/* * Copyright 2012-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */

package org.springframework.boot.autoconfigure; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.boot.type.classreading.ConcurrentReferenceCachingMetadataReaderFactory; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.annotation.ConfigurationClassPostProcessor; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.core.Ordered; import org.springframework.core.PriorityOrdered; import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReaderFactory; /** * {@link ApplicationContextInitializer} to create a shared * {@link CachingMetadataReaderFactory} between the * {@link ConfigurationClassPostProcessor} and Spring Boot. * * @author Phillip Webb */
class SharedMetadataReaderFactoryContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered { public static final String BEAN_NAME = "org.springframework.boot.autoconfigure."
            + "internalCachingMetadataReaderFactory"; @Override public void initialize(ConfigurableApplicationContext applicationContext) { applicationContext.addBeanFactoryPostProcessor(new CachingMetadataReaderFactoryPostProcessor()); } @Override public int getOrder() { return 0; } /** * {@link BeanDefinitionRegistryPostProcessor} to register the * {@link CachingMetadataReaderFactory} and configure the * {@link ConfigurationClassPostProcessor}. */
    private static class CachingMetadataReaderFactoryPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered { @Override public int getOrder() { // Must happen before the ConfigurationClassPostProcessor is created
            return Ordered.HIGHEST_PRECEDENCE; } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { } @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { register(registry); configureConfigurationClassPostProcessor(registry); } private void register(BeanDefinitionRegistry registry) { BeanDefinition definition = BeanDefinitionBuilder .genericBeanDefinition(SharedMetadataReaderFactoryBean.class, SharedMetadataReaderFactoryBean::new) .getBeanDefinition(); registry.registerBeanDefinition(BEAN_NAME, definition); } private void configureConfigurationClassPostProcessor(BeanDefinitionRegistry registry) { try { BeanDefinition definition = registry .getBeanDefinition(AnnotationConfigUtils.CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME); definition.getPropertyValues().add("metadataReaderFactory", new RuntimeBeanReference(BEAN_NAME)); } catch (NoSuchBeanDefinitionException ex) { } } } /** * {@link FactoryBean} to create the shared {@link MetadataReaderFactory}. */
    static class SharedMetadataReaderFactoryBean implements FactoryBean<ConcurrentReferenceCachingMetadataReaderFactory>, BeanClassLoaderAware, ApplicationListener<ContextRefreshedEvent> { private ConcurrentReferenceCachingMetadataReaderFactory metadataReaderFactory; @Override public void setBeanClassLoader(ClassLoader classLoader) { this.metadataReaderFactory = new ConcurrentReferenceCachingMetadataReaderFactory(classLoader); } @Override public ConcurrentReferenceCachingMetadataReaderFactory getObject() throws Exception { return this.metadataReaderFactory; } @Override public Class<?> getObjectType() { return CachingMetadataReaderFactory.class; } @Override public boolean isSingleton() { return true; } @Override public void onApplicationEvent(ContextRefreshedEvent event) { this.metadataReaderFactory.clearCache(); } } }
View Code

可以看到該類實現了 Spring 的 ApplicationContextInitializer 接口,並重寫了initialize()方法。同理,其他的 Initializer 接口也是類似實現。 而在這里則是在上下文中加入了 CachingMetadataReaderFactoryPostProcessor bean工廠后置處理器。

ApplicationContextInitializer接口的主要作用是在 ConfigurableApplicationContext#refresh()方法調用之前做一些初始化工作。

1.3、加載應用事件監聽器ApplicationListener

setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class))

接着加載應用事件監聽器 ,過程與“加載應用上下文初始器”基本一致,同樣是調用 getSpringFactoriesInstances 方法,不過這里獲取的是key為ApplicationListener 的對象集合:

如下是spring-boot-autoconfigure包下的spring.factories文件:

# Application Listeners org.springframework.context.ApplicationListener=\ org.springframework.boot.autoconfigure.BackgroundPreinitializer

最后,將獲取的 BackgroundPreinitializer對象通過setListeners方法放入listeners 屬性變量中:

    /** * Sets the {@link ApplicationListener}s that will be applied to the SpringApplication * and registered with the {@link ApplicationContext}. * @param listeners the listeners to set */
    public void setListeners(Collection<? extends ApplicationListener<?>> listeners) { this.listeners = new ArrayList<>(listeners); }

我們同樣舉例,來看看監聽器中的內容,如BackgroundPreinitializer:

/* * Copyright 2012-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */

package org.springframework.boot.autoconfigure; import java.nio.charset.StandardCharsets; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import javax.validation.Configuration; import javax.validation.Validation; import org.springframework.boot.context.event.ApplicationFailedEvent; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.boot.context.event.ApplicationStartingEvent; import org.springframework.boot.context.event.SpringApplicationEvent; import org.springframework.boot.context.logging.LoggingApplicationListener; import org.springframework.context.ApplicationListener; import org.springframework.core.annotation.Order; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; /** * {@link ApplicationListener} to trigger early initialization in a background thread of * time consuming tasks. * <p> * Set the {@link #IGNORE_BACKGROUNDPREINITIALIZER_PROPERTY_NAME} system property to * {@code true} to disable this mechanism and let such initialization happen in the * foreground. * * @author Phillip Webb * @author Andy Wilkinson * @author Artsiom Yudovin * @since 1.3.0 */ @Order(LoggingApplicationListener.DEFAULT_ORDER + 1) public class BackgroundPreinitializer implements ApplicationListener<SpringApplicationEvent> { /** * System property that instructs Spring Boot how to run pre initialization. When the * property is set to {@code true}, no pre-initialization happens and each item is * initialized in the foreground as it needs to. When the property is {@code false} * (default), pre initialization runs in a separate thread in the background. * @since 2.1.0 */
    public static final String IGNORE_BACKGROUNDPREINITIALIZER_PROPERTY_NAME = "spring.backgroundpreinitializer.ignore"; private static final AtomicBoolean preinitializationStarted = new AtomicBoolean(false); private static final CountDownLatch preinitializationComplete = new CountDownLatch(1); @Override public void onApplicationEvent(SpringApplicationEvent event) { if (!Boolean.getBoolean(IGNORE_BACKGROUNDPREINITIALIZER_PROPERTY_NAME) && event instanceof ApplicationStartingEvent && multipleProcessors() && preinitializationStarted.compareAndSet(false, true)) { performPreinitialization(); } if ((event instanceof ApplicationReadyEvent || event instanceof ApplicationFailedEvent) && preinitializationStarted.get()) { try { preinitializationComplete.await(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } private boolean multipleProcessors() { return Runtime.getRuntime().availableProcessors() > 1; } private void performPreinitialization() { try { Thread thread = new Thread(new Runnable() { @Override public void run() { runSafely(new ConversionServiceInitializer()); runSafely(new ValidationInitializer()); runSafely(new MessageConverterInitializer()); runSafely(new JacksonInitializer()); runSafely(new CharsetInitializer()); preinitializationComplete.countDown(); } public void runSafely(Runnable runnable) { try { runnable.run(); } catch (Throwable ex) { // Ignore
 } } }, "background-preinit"); thread.start(); } catch (Exception ex) { // This will fail on GAE where creating threads is prohibited. We can safely // continue but startup will be slightly slower as the initialization will now // happen on the main thread.
 preinitializationComplete.countDown(); } } /** * Early initializer for Spring MessageConverters. */
    private static class MessageConverterInitializer implements Runnable { @Override public void run() { new AllEncompassingFormHttpMessageConverter(); } } /** * Early initializer for javax.validation. */
    private static class ValidationInitializer implements Runnable { @Override public void run() { Configuration<?> configuration = Validation.byDefaultProvider().configure(); configuration.buildValidatorFactory().getValidator(); } } /** * Early initializer for Jackson. */
    private static class JacksonInitializer implements Runnable { @Override public void run() { Jackson2ObjectMapperBuilder.json().build(); } } /** * Early initializer for Spring's ConversionService. */
    private static class ConversionServiceInitializer implements Runnable { @Override public void run() { new DefaultFormattingConversionService(); } } private static class CharsetInitializer implements Runnable { @Override public void run() { StandardCharsets.UTF_8.name(); } } }
View Code

可以看到,該類實現了Spring的ApplicationListener 接口,在重寫的 onApplicationEvent 方法中觸發相應的事件進行操作。同理,其他Listener也是類似實現。而該接口的主要功能是另起一個后台線程觸發那些耗時的初始化,包括驗證器、消息轉換器等等。 

1.4、推斷應用引導類

// 推斷引導類,也就是找到入口類
this.mainApplicationClass = deduceMainApplicationClass();

准備階段的最后一步是推斷應用的引導類,也就是獲取啟動 main 方法的類,執行的是 deduceMainApplicationClass() 方法:

    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; }

可以看到,通過 getStackTrace()方法獲取當前線程的執行棧,再通過 getMethodName()獲取方法名,判斷是否是main 方法,最后返回main方法的所在類。

二、SpringApplication運行階段——事件監聽機制

前面我們已經講了 SpringApplication 的構造方法,這里我們就來講講 SpringApplication 的核心,也就是run方法:

public ConfigurableApplicationContext run(String... args) { // 這是 Spring 的一個計時器,計算代碼的執行時間(ms級別)
        StopWatch stopWatch = new StopWatch(); stopWatch.start(); // 這倆變量在后面賦值處進行說明
        ConfigurableApplicationContext context = null; Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); // 用來設置java.awt.headless屬性值
 configureHeadlessProperty(); // 該對象屬於組合模式的實現,核心是內部關聯的 SpringApplicationRunListener 集合,SpringApplicationRunListener 是 Spring Boot 的運行時監聽器
        SpringApplicationRunListeners listeners = getRunListeners(args); // 會在不同的階段調用對應的方法,這里表示SpringApplication的run方法剛開始執行
 listeners.starting(); try { // 用來獲取 SpringApplication.run(args)傳入的參數
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); // 獲取 properties 配置文件
            ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); // 設置 spring.beaninfo.ignore 的屬性值,判斷是否跳過搜索BeanInfo類
 configureIgnoreBeanInfo(environment); // 這里是項目啟動時,控制台打印的 Banner
            Banner printedBanner = printBanner(environment); // 這里就是創建 Spring 應用上下文
            context = createApplicationContext(); // 獲取 spring.factories 中key為 SpringBootExceptionReporter 的類名集合
            exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); // 這里是准備 Spring 應用上下文
 prepareContext(context, environment, listeners, applicationArguments, printedBanner); // 這里是啟動 Spring 應用上下文,底層調用的是 ApplicationContext 的 refresh() 方法,到這里就正式進入了 Spring 的生命周期,同時,SpringBoot的自動裝配特性也隨之啟動
 refreshContext(context); // 里面是空的,猜測應該是交由開發人員自行擴展
 afterRefresh(context, applicationArguments); stopWatch.stop(); // 這里打印啟動信息
            if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } // ApplicationContext 啟動時,調用該方法
 listeners.started(context); // 項目啟動后,做的一些操作,開發人員可自行擴展
 callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { // ApplicationContext 啟動完成時,調用該方法
 listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context; }

這段代碼在最開始使用了事件/監聽器模式,因此在需要先介紹一下監聽器模式。

2.1、監聽器模式

將一個監聽器(listener)與特定的控件(如按鈕等)綁定起來,當發生用戶點擊等事件(Event)時,調用監聽器的處理方法,從而響應用戶的動作,就叫做事件/監聽器模式。

下面列出spring boot中主要的應用事件類型:

  • ApplicationFailedEvent:該事件為spring boot啟動失敗時的操作;
  • ApplicationPreparedEvent:上下文context准備時觸發;
  • ApplicationReadyEvent:上下文已經准備完畢的時候觸發;
  • ApplicationStartingEvent:SpringApplication剛啟動時觸發;
  • ApplicationStartedEvent:上下文已經啟動時觸發;
  • SpringApplicationEvent:獲取SpringApplication;
  • ApplicationEnvironmentPreparedEvent:環境事先准備;

我們1.3節中介紹的ApplicationListener的實現類就是監聽器,它用於監聽某個事件的發生,當某個應用事件發生的時候,就會調用對應的ApplicationListener的onApplicationEvent 方法;

2.2、獲取SpringApplicationRunListeners

 // 該對象屬於組合模式的實現,核心是內部關聯的 SpringApplicationRunListener 集合,SpringApplicationRunListener 是 Spring Boot 的運行時監聽器
 SpringApplicationRunListeners listeners = getRunListeners(args);

SpringApplicationRunListeners我們單從字面意思來看,Spring應用運行監聽器列表,啥是運行監聽器列表?說白了,就是在SpringApplication啟動各個階段,會調用一次listeners.xxx()方法,然后廣播各個階段對應的事件(ApplicationEvent),最終會調用各個階段對應的監聽器(ApplicationListener)列表,執行監聽器的onApplicationEvent ()方法。SpringApplicationRunListeners這個名字我總感覺有點變扭,應該叫事件發布者更為貼切。

我們先來看看 SpringApplicationRunListeners 對象,從代碼可以看出該對象是由 getRunListeners 方法創建的:

    private SpringApplicationRunListeners getRunListeners(String[] args) { Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class }; return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args)); }

可以看到,通過傳入的 getSpringFactoriesInstances 方法的返回值,執行 SpringApplicationRunListeners 的構造方法,進行對象的創建。

同樣是調用 getSpringFactoriesInstances 方法,不過這里獲取的是 key 為 SpringApplicationRunListener 的對象集合.

 最后,就是將該集合傳入 SpringApplicationRunListeners 的構造方法:

/** * A collection of {@link SpringApplicationRunListener}. * * @author Phillip Webb */
class SpringApplicationRunListeners { private final Log log; 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(); } } ... }

里面是將集合賦值到 listeners 屬性,可以看到 SpringApplicationRunListeners 屬於組合模式的實現,核心其實是內部關聯的 SpringApplicationRunListener 對象集合,當外部調用該階段方法時,就會迭代執行集合中 SpringApplicationRunListener 對應的方法。所以接下來我們就來討論 SpringApplicationRunListener。

2.3、監聽器觸發機制

SpringApplicationRunListener 負責在 SpringBoot 的不同階段廣播相應的事件,然后調用實際的 ApplicationListener 類,在該類的 onApplicationEvent 方法中,根據不同的 Spring Boot 事件執行相應操作。整個過程大概如此:

接下來進行詳細討論,先來看看 SpringApplicationRunListener 定義:

public interface SpringApplicationRunListener { // 在run()方法開始執行時被調用,表示應用剛剛啟動,對應的 Spring Boot 事件為 ApplicationStartingEvent
    void starting(); // ConfigurableEnvironment 構建完成時調用,對應的 Spring Boot 事件為 ApplicationEnvironmentPreparedEvent
    void environmentPrepared(ConfigurableEnvironment environment); // ApplicationContext 構建完成時調用,對應的 Spring Boot 事件為 ApplicationContextInitializedEvent
    void contextPrepared(ConfigurableApplicationContext context); // ApplicationContext 完成加載但還未啟動時調用,對應的 Spring Boot 事件為 ApplicationPreparedEvent
    void contextLoaded(ConfigurableApplicationContext context); // ApplicationContext 已啟動,但 callRunners 還未執行時調用,對應的 Spring Boot 事件為 ApplicationStartedEvent
    void started(ConfigurableApplicationContext context); // ApplicationContext 啟動完畢被調用,對應的 Spring Boot 事件為 ApplicationReadyEvent
    void running(ConfigurableApplicationContext context); // 應用出錯時被調用,對應的 Spring Boot 事件為 ApplicationFailedEvent
    void failed(ConfigurableApplicationContext context, Throwable exception); }

我們來看看它的實現類,也就是上面加載的 spring.factories 文件中的 EventPublishingRunListener 類,該類也是 Spring Boot 內建的唯一實現類,具體廣播事件的操作在該類中進行,代碼如下: 

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered { private final SpringApplication application; private final String[] args; private final SimpleApplicationEventMulticaster initialMulticaster; public EventPublishingRunListener(SpringApplication application, String[] args) { this.application = application; this.args = args; this.initialMulticaster = new SimpleApplicationEventMulticaster(); for (ApplicationListener<?> listener : application.getListeners()) {
//添加ApplicationListener監聽器
this.initialMulticaster.addApplicationListener(listener); } } @Override public void starting() {
//調用廣播器來觸發事件
this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args)); } ... }

可以看到,通過構造方法創建 EventPublishingRunListener 實例的過程中,調用了 getListeners 方法,將 SpringApplication 中所有 ApplicationListener 監聽器關聯到了 initialMulticaster 屬性中。沒錯,這里的 ApplicationListener 監聽器就是在 SpringApplication 准備階段從 spring.factories 文件加載的 key 為 ApplicationListener 的實現類集合,該實現類集合全部重寫了 onApplicationEvent 方法。

2.4、SimpleApplicationEventMulticaster 廣播器觸發ApplicationStaringEvent事件

這里又引出了另一個類, 也就是 SimpleApplicationEventMulticaster ,該類是 Spring 的事件廣播器,也就是通過它來廣播各種事件。接着,當外部迭代的執行到 EventPublishingRunListener 的 starting 方法時,會通過 SimpleApplicationEventMulticaster 的 multicastEvent 方法進行事件的廣播,這里廣播的是 ApplicationStartingEvent 事件,我們進入 multicastEvent 方法:

public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster { ... @Override public void multicastEvent(ApplicationEvent event) { multicastEvent(event, resolveDefaultEventType(event)); } @Override public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) { ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
//獲取線程池 Executor executor
= getTaskExecutor();
//獲取對當前事件感興趣的監聽器列表
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) { if (executor != null) { executor.execute(() -> invokeListener(listener, event)); } else { invokeListener(listener, event); } } } }

通過 getApplicationListeners 方法,根據事件類型返回從上面關聯的 ApplicationListener 集合中篩選出匹配的 ApplicationListener 集合:

然后依次遍歷這些監聽器,同步或異步的調用 invokeListener 方法:

/** * Invoke the given listener with the given event. * @param listener the ApplicationListener to invoke * @param event the current event to propagate * @since 4.1 */
    protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) { ErrorHandler errorHandler = getErrorHandler(); if (errorHandler != null) { try { doInvokeListener(listener, event); } catch (Throwable err) { errorHandler.handleError(err); } } else { doInvokeListener(listener, event); } } @SuppressWarnings({"rawtypes", "unchecked"}) private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) { try { listener.onApplicationEvent(event); } catch (ClassCastException ex) { String msg = ex.getMessage(); if (msg == null || matchesClassCastMessage(msg, event.getClass())) { // Possibly a lambda-defined listener which we could not resolve the generic event type for // -> let's suppress the exception and just log a debug message.
                Log logger = LogFactory.getLog(getClass()); if (logger.isTraceEnabled()) { logger.trace("Non-matching event type for listener: " + listener, ex); } } else { throw ex; } } }

可以看到,最終調用的是 doInvokeListener 方法,在該方法中執行了 ApplicationListener 的 onApplicationEvent 方法,入參為廣播的事件對象。我們就拿其中一個的監聽器來看看 onApplicationEvent 中的實現,如 BackgroundPreinitializer 類:

public class BackgroundPreinitializer implements ApplicationListener<SpringApplicationEvent> { ... @Override public void onApplicationEvent(SpringApplicationEvent event) { if (!Boolean.getBoolean(IGNORE_BACKGROUNDPREINITIALIZER_PROPERTY_NAME) && event instanceof ApplicationStartingEvent && preinitializationStarted.compareAndSet(false, true)) { performPreinitialization(); } if ((event instanceof ApplicationReadyEvent || event instanceof ApplicationFailedEvent) && preinitializationStarted.get()) { try { preinitializationComplete.await(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } ... }

在該方法中,通過 instanceof 判斷事件的類型,從而進行相應的操作。該監聽器主要的操作是新建一個后台線程去執行那些耗時的初始化工作,包括驗證器、消息轉換器等。LoggingApplicationListener 監聽器則是對 Spring Boot 的日志系統做一些初始化的前置操作。另外兩個監聽器在該階段無任何操作。 至此,SpringBoot 事件機制的整體流程大概如此,我們簡要回顧一下幾個核心組件: 

  • SpringApplicationRunListeners:首先,在SpringApplication的run 方法的執行過程中,通過該類在 Spring Boot 不同的階段調用不同的階段方法,如在剛啟動階段調用的 listeners.starting 方法。
  • SpringApplicationRunListener:而 SpringApplicationRunListeners 屬於組合模式的實現,它里面關聯了 SpringApplicationRunListener 實現類集合,當外部調用階段方法如listeners.starting 時,會迭代執行該集合中的階段方法。實現類集合是 spring.factories 文件中定義好的類。
  • EventPublishingRunListener:該類是 Spring Boot 內置的 SpringApplicationRunListener 唯一實現類,所以,當外部調用各階段的方法時,真正執行的是該類中的方法。
  • SimpleApplicationEventMulticaster:在階段方法中,會通過 Spring 的 SimpleApplicationEventMulticaster 事件廣播器,廣播各個階段對應的事件,如這里的 starting 方法廣播的事件是 ApplicationStartingEvent。
  • ApplicationListener:最后 ApplicationListener 的實現類也就是 Spring Boot 監聽器會監聽到廣播的事件,根據不同的事件,進行相應的操作。

以發布ApplicationStartingEvent事件為例,時序圖如下:

到這里 Spring Boot 事件監聽機制差不多就結束了,值得注意的是 Spring Boot 監聽器實現的是 Spring 的 ApplicationListener 類,事件類最終繼承的也是 Spring 的 ApplicationEvent 類,所以,Spring Boot 的事件和監聽機制都基於 Spring 而實現的。 

三、SpringApplication運行階段——加載啟動參數

當執行完 listeners.starting 方法后,接着進入構造 ApplicationArguments 階段:

ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

該類是用於簡化 Spring Boot 應用啟動參數的封裝接口,我們啟動項目時輸入的命令參數會封裝在該類中。一種是通過 IDEA 輸入的參數,如下:

另一種是 springboot jar包運行時傳遞的參數: cmd中運行java -jar xxx.jar name=張三 pwa=123 。

然后,可以通過 @Autowired 注入 ApplicationArguments 的方式進行使用:

public class Test { @Autowired private ApplicationArguments applicationArguments; public void getArgs() { // 獲取 args 中的所有 non option 參數
 applicationArguments.getNonOptionArgs(); // 獲取 args 中所有的 option 參數的 name
 applicationArguments.getOptionNames(); // 獲取傳遞給應用程序的原始未處理參數
 applicationArguments.getSourceArgs(); // 獲取 args 中指定 name 的 option 參數的值
        applicationArguments.getOptionValues("name"); // 判斷從參數中解析的 option 參數是否包含指定名稱的選項
        applicationArguments.containsOption("name"); } }

四、SpringApplication運行階段——加載外部化配置的資源文件

在介紹加載外部化配置資源文件之前,我們先來介紹一下PropertySource以及外部化配置的資源類型有哪些。

4.1、外部化配置的資源類型

除了在項目中我們常常用到的properties、YAML文件外,還有環境變量、系統屬性、啟動參數等。所有的資源類型將近二十種,這里只介紹比較熟悉的:

  • properties:文件是以.properties為后綴,文件中的數據以key-value格式保存;
  • yaml:文件是以.yml為后綴,文件中的數據以key-value格式保存;
  • 環境變量:這是通過 System.getenv() 方式獲取的默認配置,也是key value格式,下面列出部分配置,其它的還請自行了解,如下:

名稱

Key
Java安裝目錄 JAVA_HOME
classpath環境變量 CLASSPATH
用戶臨時文件目錄 TEMP
計算機名 COMPUTERNAME
用戶名 USERNAME
  • 系統屬性:這是通過 System.getProperties() 方式獲取的默認配置,也是key value格式,下面列出部分配置,其它的還請自行了解,如下:
名稱 key
運行時環境版本 java.version Java
Java安裝目錄 java.home
要使用的 JIT編譯器的名稱 java.compiler
操作系統的架構 os.arch
操作系統的版本 os.version
  • 啟動參數:就是我們上面介紹到的,一種是在 jar 包運行時行時傳遞的參數,如:java -jar xxx.jar name=張三 pwa=123 ,還有一種是在 IDEA 的 Program arguments 中輸入數據。

可以看到,外部化配置中的數據都是key value 格式。這里還要注意它們的加載順序,當key相同時,會出現覆蓋的情況。

接下來,我們的重心來圍繞 properties 和 YAML 配置文件,這兩者也是我們日常工作中常用的。首先來看取值方式,在 Spring 時代有 Environment 、 @Value 、 XML 三種方式,在 Spring Boot 時代則是 @ConfigurationProperties 方式。其中,涉及到了一個核心類,它就是 Environment ,該對象不僅可以獲取所有的外部化配置數據,就連另外幾種取值方式的數據來源也是從該類中獲取。這里,主要對 Environment 和 @ConfigurationProperties 進行詳細討論。 

4.2 、PropertySource

PropertySource ,我們將其稱之為配置源,官方定義它是外部化配置的API描述方式,是外部化配置的一個媒介。 用我們的話來說,它是一個抽象類,提供了統一存儲外部化配置數據的功能,而每種外部化配置有具體的實現類 。我們來看看 PropertySource 對象的數據格式,一般包含: 

  • name : 外部化配置的名稱;
  • source : 存儲配置中的數據,底層一般數據格式都是key value;

 這個類結構簡化如下,主要提供不同的基礎操作,如 get、contains 等:

public abstract class PropertySource<T> {protected final String name;//屬性源名稱
    protected final T source;//屬性源(比如來自Map,那就是一個Map對象)
    public String getName();  //獲取屬性源的名字 
    public T getSource();        //獲取屬性源 
    public boolean containsProperty(String name);  //是否包含某個屬性 
    public abstract Object getProperty(String name);   //得到屬性名對應的屬性值 
} 

其類圖如下:

  • MapPropertySource:其source來自於一個Map,用法如下:
public static void main(String[] args) { Map<String,Object> map=new HashMap<>(); map.put("name","wang"); map.put("age",23); MapPropertySource source_1=new MapPropertySource("person",map); System.out.println(source_1.getProperty("name"));//wang
        System.out.println(source_1.getProperty("age"));//23
        System.out.println(source_1.getName());//person
        System.out.println(source_1.containsProperty("class"));//false
 }
  • PropertiesPropertySource:source是一個Properties對象,繼承自MapPropertySource。與MapPropertySource用法相同;

  • ResourcePropertySource:繼承自PropertiesPropertySource,它的source來自於一個Resource資源;

  • ServletConfigPropertySource:source為ServletConfig對象;

  • ServletContextPropertySource:source為ServletContext對象;

  • StubPropertySource:臨時作為一個PropertySource的占位,后期會被真實的PropertySource取代;

  • SystemEnvironmentPropertySource:繼承自MapPropertySource,它的source也是一個map,但來源於系統環境。 【重點】與MapPropertySource不同的是,取值時它將會忽略大小寫,”.”和”_”將會轉化。遇到轉化情況會打印出日志;

  • CompositePropertySource:內部可以保存多個PropertySource;

private final Set<PropertySource<?>> propertySources = new LinkedHashSet<PropertySource<?>>();

4.3、PropertySources

包含多個PropertySource,繼承了Iterable接口,所以它的子類還具有迭代的能力。 接口定義:

public interface PropertySources extends Iterable<PropertySource<?>> { boolean contains(String name);//是否包含某個PropertySource
 PropertySource<?> get(String name);//獲取某個PropertySource
}

PropertySource的實現類MutablePropertySources,它包含了一個CopyOnWriteArrayList集合,用來包含多個PropertySource:

private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<PropertySource<?>>();

這個類還具有幾個比較重要的方法,用來向集合中加減PropertySource:

addFirst
addLast
addBefore
addAfter
remove

4.4、PropertyResolver

實現這個類的接口具有解析PropertySource、根據PropertySource轉換文本中的占位符的能力.

public interface PropertyResolver { //是否包含某個屬性 
    boolean containsProperty(String key); //獲取屬性值 如果找不到返回null 
 String getProperty(String key); //獲取屬性值,如果找不到返回默認值 
 String getProperty(String key, String defaultValue); //獲取指定類型的屬性值,找不到返回null 
    <T> T getProperty(String key, Class<T> targetType); //獲取指定類型的屬性值,找不到返回默認值 
    <T> T getProperty(String key, Class<T> targetType, T defaultValue); //獲取屬性值為某個Class類型,找不到返回null,如果類型不兼容將拋出ConversionException 
    <T> Class<T> getPropertyAsClass(String key, Class<T> targetType); //獲取屬性值,找不到拋出異常IllegalStateException 
    String getRequiredProperty(String key) throws IllegalStateException; //獲取指定類型的屬性值,找不到拋出異常IllegalStateException 
    <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException; //替換文本中的占位符(${key})到屬性值,找不到不解析 
 String resolvePlaceholders(String text); //替換文本中的占位符(${key})到屬性值,找不到拋出異常IllegalArgumentException 
    String resolveRequiredPlaceholders(String text) throws IllegalArgumentException; } 

它的實現類主要有兩種:

  • 各種Resolver:主要是PropertySourcesPropertyResolver
  • 各種Evironment

PropertySourcesPropertyResolver:

MutablePropertySources sources = new MutablePropertySources(); sources.addLast(new MapPropertySource("map", new HashMap<String, Object>() { { put("name", "wang"); put("age", 12); } }));//向MutablePropertySources添加一個MapPropertySource
 PropertyResolver resolver = new PropertySourcesPropertyResolver(sources); System.out.println(resolver.containsProperty("name"));//輸出 true
System.out.println(resolver.getProperty("age"));//輸出 12
System.out.println(resolver.resolvePlaceholders("My name is ${name} .I am ${age}.")); //輸出 My name is wang .I am 12.

4.5、Environment 

Spring抽象了一個Environment來表示Spring應用程序環境配置,它整合了各種各樣的外部化配置的資源類型,並且提供統一訪問的方法。

public interface Environment extends PropertyResolver { //得到當前明確激活的剖面 
 String[] getActiveProfiles(); //得到默認激活的剖面,而不是明確設置激活的 
 String[] getDefaultProfiles(); //是否接受某些剖面 
    boolean acceptsProfiles(String... profiles); } 

從API上可以看出,除了可以解析相應的屬性信息外,還提供了剖面相關的API,目的是: 可以根據剖面有選擇的進行注冊組件/配置。比如對於不同的環境注冊不同的組件/配置(正式機、測試機、開發機等的數據源配置) 我們再看看它的繼承關系:

拋開復雜的繼承關系,發現它的實現類主要有兩個:

  • StandardEnvironment:標准環境,普通Java應用時使用,會自動注冊System.getProperties() 和 System.getenv()到環境;
  • StandardServletEnvironment:標准Servlet環境,其繼承了StandardEnvironment,Web應用時使用,除了StandardEnvironment外,會自動注冊ServletConfig(DispatcherServlet)、ServletContext及有選擇性的注冊JNDI實例到環境; 

使用示例如下:

//會自動注冊 System.getProperties() 和 System.getenv() 
Environment environment = new StandardEnvironment(); System.out.println(environment.getProperty("file.encoding"));  

4.6、ConfigurableEnvironment 

基礎知識已經介紹的差不多了,我們繼續回歸到SpringApplication的run方法的分析中。

接着進入構造 ConfigurableEnvironment 的階段,該類是用來處理我們外部化配置的:

public class SpringApplication { ... public ConfigurableApplicationContext run(String... args) { ... try { ... ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); ... } ... }

在 SpringApplication 運行階段的 run 方法中通過 prepareEnvironment 方法了創建 ConfigurableEnvironment 的實現類對象,ConfigurableEnvironment 是一個接口,且繼承了 Environment 。進入創建方法:

    private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) { // Create and configure the environment
        ConfigurableEnvironment environment = getOrCreateEnvironment(); configureEnvironment(environment, applicationArguments.getSourceArgs()); ConfigurationPropertySources.attach(environment); listeners.environmentPrepared(environment); bindToSpringApplication(environment); if (!this.isCustomEnvironment) { environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment, deduceEnvironmentClass()); } ConfigurationPropertySources.attach(environment); return environment; }

這里通過 getOrCreateEnvironment 方法返回具體的 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(); } }

可以看到,這里通過 webApplicationType 屬性來判斷當前應用的類型,有 Servlet 、 Reactive 、 非Web 3種類型,該屬性是在SpringApplication 准備階段確定的,這里我們通常都是 Servlet 類型,返回的是 StandardServletEnvironment 實例。

public class StandardServletEnvironment extends StandardEnvironment implements ConfigurableWebEnvironment { /** Servlet context init parameters property source name: {@value}. */
    public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams"; /** Servlet config init parameters property source name: {@value}. */
    public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams"; /** JNDI property source name: {@value}. */
    public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties"; /** * Customize the set of property sources with those contributed by superclasses as * well as those appropriate for standard servlet-based environments: * <ul> * <li>{@value #SERVLET_CONFIG_PROPERTY_SOURCE_NAME} * <li>{@value #SERVLET_CONTEXT_PROPERTY_SOURCE_NAME} * <li>{@value #JNDI_PROPERTY_SOURCE_NAME} * </ul> * <p>Properties present in {@value #SERVLET_CONFIG_PROPERTY_SOURCE_NAME} will * take precedence over those in {@value #SERVLET_CONTEXT_PROPERTY_SOURCE_NAME}, and * properties found in either of the above take precedence over those found in * {@value #JNDI_PROPERTY_SOURCE_NAME}. * <p>Properties in any of the above will take precedence over system properties and * environment variables contributed by the {@link StandardEnvironment} superclass. * <p>The {@code Servlet}-related property sources are added as * {@link StubPropertySource stubs} at this stage, and will be * {@linkplain #initPropertySources(ServletContext, ServletConfig) fully initialized} * once the actual {@link ServletContext} object becomes available. * @see StandardEnvironment#customizePropertySources * @see org.springframework.core.env.AbstractEnvironment#customizePropertySources * @see ServletConfigPropertySource * @see ServletContextPropertySource * @see org.springframework.jndi.JndiPropertySource * @see org.springframework.context.support.AbstractApplicationContext#initPropertySources * @see #initPropertySources(ServletContext, ServletConfig) */ @Override protected void customizePropertySources(MutablePropertySources propertySources) { propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME)); propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME)); if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) { propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME)); } super.customizePropertySources(propertySources); } @Override public void initPropertySources(@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) { WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig); } }

而該類又繼承了 StandardEnvironment 類。且重寫了 customizePropertySources 方法,並調用了父類的 customizePropertySources 方法。我們繼續往下深入: 

public class StandardEnvironment extends AbstractEnvironment { /** System environment property source name: {@value}. */
    public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment"; /** JVM system properties property source name: {@value}. */
    public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties"; /** * Customize the set of property sources with those appropriate for any standard * Java environment: * <ul> * <li>{@value #SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME} * <li>{@value #SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME} * </ul> * <p>Properties present in {@value #SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME} will * take precedence over those in {@value #SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME}. * @see AbstractEnvironment#customizePropertySources(MutablePropertySources) * @see #getSystemProperties() * @see #getSystemEnvironment() */ @Override protected void customizePropertySources(MutablePropertySources propertySources) { propertySources.addLast( new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties())); propertySources.addLast( new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment())); } }

繼續看它的 AbstractEnvironment 父抽象類:

public abstract class AbstractEnvironment implements ConfigurableEnvironment { ... private final MutablePropertySources propertySources = new MutablePropertySources(); ... public AbstractEnvironment() { customizePropertySources(this.propertySources); } ... }

可以看到,最終會有一個 AbstractEnvironment 抽象類。在 StandardServletEnvironment 初始化時,會調用 AbstractEnvironment 的構造方法,里面調用了子類重寫的 customizePropertySources 方法,且入參是 MutablePropertySources 對象,該對象是 Environment 的一個屬性,是底層真正存儲外部化配置的。之后, StandardServletEnvironment 和 StandardEnvironment 的 customizePropertySources 方法相繼執行,主要是往 MutablePropertySources 對象中添加外部化配置。其中我們前面所說的環境變量和系統屬性是在 StandardEnvironment 重寫的方法中進行加載。

我們回到外面的 prepareEnvironment 方法,繼續往下走。接着執行的是 configureEnvironment 方法,該方法主要是把啟動參數加入到 MutablePropertySources 中。之后,我們斷點看看有多少種外部化配置: 

有四種,且真正存儲數據的是 MutablePropertySources 中的 PropertySource 實現類集合。

以環境變量為例:

之后,還調用了 SpringApplicationRunListeners 的 environmentPrepared 階段方法,表示 ConfigurableEnvironment 構建完成:

class SpringApplicationRunListeners { ... private final List<SpringApplicationRunListener> listeners; public void environmentPrepared(ConfigurableEnvironment environment) { for (SpringApplicationRunListener listener : this.listeners) { listener.environmentPrepared(environment); } } ... }

真正調用的是 SpringApplicationRunListener 集合中的 environmentPrepared 方法。 SpringApplicationRunListener 是一個接口,它具有唯一實現類 EventPublishingRunListener : 

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered { ... private final SimpleApplicationEventMulticaster initialMulticaster; @Override public void environmentPrepared(ConfigurableEnvironment environment) { this.initialMulticaster .multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment)); } ... }

可以看到,最終通過 SimpleApplicationEventMulticaster 的 multicastEvent 方法發布 ApplicationEnvironmentPreparedEvent 事件。 執行調監聽器的 onApplicationEvent 方法。監聽該事件的監聽器有: 

其中一個名為 ConfigFileApplicationListener 的監聽器,監聽到該事件后會進行加載 application 和 YAML 配置文件的操作,接下來,我們具體的來看一看該類實現。

4.7、ConfigFileApplicationListener 

我們直接進入該類:

public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered { ... private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/"; public static final String CONFIG_NAME_PROPERTY = "spring.config.name"; public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location"; public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location"; private static final String DEFAULT_NAMES = "application"; @Override public void onApplicationEvent(ApplicationEvent event) { // 1、通過 instanceof 判斷事件的類型,如果是 ApplicationEnvironmentPreparedEvent 事件,則執行 onApplicationEnvironmentPreparedEvent 方法
        if (event instanceof ApplicationEnvironmentPreparedEvent) { onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event); } ... } private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) { // 2、調用 loadPostProcessors 方法,返回 Environment 的后置處理器集合,我們跳到 2.1 查看方法實現
        List<EnvironmentPostProcessor> postProcessors = loadPostProcessors(); // 2.2、把自己也加入該集合
        postProcessors.add(this); AnnotationAwareOrderComparator.sort(postProcessors); // 2.3、遍歷 EnvironmentPostProcessor 集合,執行它們的 postProcessEnvironment 方法,我們跳到 3 查看當前類的該方法實現
        for (EnvironmentPostProcessor postProcessor : postProcessors) { postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication()); } } // 2.1 是我們比較熟悉的 loadFactories 方法,loadFactories 方法是從 spring.factories 文件中加載 key 為 EnvironmentPostProcessor 的實現類集合
    List<EnvironmentPostProcessor> loadPostProcessors() { return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader()); } // 3、 執行到該方法時,會調用 addPropertySources 方法,入參是上文加載 ConfigurableEnvironment 對象
 @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { addPropertySources(environment, application.getResourceLoader()); } protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) { RandomValuePropertySource.addToEnvironment(environment); // 4、 我們主要關注這里,通過 Loader 的構造方法創建該對象,並調用它的 load 方法
        new Loader(environment, resourceLoader).load(); } private class Loader { private final ConfigurableEnvironment environment; private final List<PropertySourceLoader> propertySourceLoaders; // 4.1、 構造方法中會初始化一些屬性
 Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) { ... this.environment = environment; // 又是我們比較熟悉的 loadFactories 方法, loadFactories 方法是從 spring.factories 文件中加載 key 為 PropertySourceLoader 的實現類集合。這里加載的是 PropertiesPropertySourceLoader 和 YamlPropertySourceLoader 兩個實現類,看類名可初步斷定是處理 properties 和 YAML 文件的
            this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,getClass().getClassLoader()); } public void load() { ... // 5、這里會繼續調用它重載的 load 方法
            load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true)); ... // 9、這是最后一步,將當前類中的 MutablePropertySources 中的 PropertySource 對象,全部塞到 ConfigurableEnvironment 的 MutablePropertySources 對象中。我們跳到 9.1 進行查看
 addLoadedPropertySources(); } private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) { // 5.1、首先執行 getSearchLocations 方法,看方法名大致能猜出是獲取搜索路徑的,我們跳到 5.1.1 查看該方法的實現
            getSearchLocations().forEach((location) -> { // 5.2、開始遍歷該集合,先判斷該路徑是否是以反斜杠結尾,是的話則該路徑為文件夾;不是的話,則該路徑為文件的完整路徑,類似於 classPath:/application.properties 
                boolean isFolder = location.endsWith("/"); // 5.3、 如果是文件夾路徑,則通過 getSearchNames 獲取文件的名稱,不是則返回空集合,我們跳到 5.3.1 查看 getSearchNames 方法
                Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES; // 5.4、再調用 load 的重載方法,這里,location 是路徑名,name是文件名,我們跳到 6 進行查看
                names.forEach((name) -> load(location, name, profile, filterFactory, consumer)); }); } // 5.1.1、這個方法就是獲取加載 application 和 YAML 文件路徑的
        private Set<String> getSearchLocations() { // 可以看到 CONFIG_LOCATION_PROPERTY 的值為 spring.config.location,也就是說,先判斷我們有沒有手動設置搜索路徑,有的話直接返回該路徑。該值一般通過啟動參數的方式設置
            if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) { return getSearchLocations(CONFIG_LOCATION_PROPERTY); } // 該 CONFIG_ADDITIONAL_LOCATION_PROPERTY 變量的值為 spring.config.additional-location,這也是用於手動設置搜索路徑,不過和上面不同的是,不會覆蓋 接下來默認的搜索路徑
            Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY); // 這里就是獲取默認的搜索路徑,通過 DEFAULT_SEARCH_LOCATIONS 變量的值 classpath:/,classpath:/config/,file:./,file:./config/,將該值用逗號分隔,加入集合並返回。到這一步,我們至少獲取到了4個加載 application 和 YAML 文件的路徑
            locations.addAll(asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS)); return locations; } // 5.3.1 
        private Set<String> getSearchNames() { // CONFIG_LOCATION_PROPERTY 變量值為 spring.config.name ,同樣先判斷有沒有手動設置文件名稱,有的話,直接返回
            if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) { String property = this.environment.getProperty(CONFIG_NAME_PROPERTY); return asResolvedSet(property, null); } // 如果沒有,則通過 DEFAULT_NAMES 變量值返回默認的文件名,變量值為 application
            return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES); } // 6、
        private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) { // 6.1、 上面 5.2 說過 name 為空時,表示 location 是完整的文件路徑。之后進入這個 if 
            if (!StringUtils.hasText(name)) { // 6.1.1、propertySourceLoaders 屬性是在 4.1 處被初始化的,存儲的是 PropertiesPropertySourceLoader 和 YamlPropertySourceLoader 兩個類。這里對這兩個類進行遍歷
                for (PropertySourceLoader loader : this.propertySourceLoaders) { // 我們跳到 6.1.2 查看 canLoadFileExtension 方法實現,入參 location 是文件的完整路徑
                    if (canLoadFileExtension(loader, location)) { // 這里又是一個 load 重載方法,我們跳到 7 進行查看
 load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer); return; } } } Set<String> processed = new HashSet<>(); for (PropertySourceLoader loader : this.propertySourceLoaders) { // 6.2 這里和 6.1.3 類似,獲取文件擴展名
                for (String fileExtension : loader.getFileExtensions()) { if (processed.add(fileExtension)) { // 進入 6.3、查看該方法實現。關注重點的兩個參數:一個是路徑名 + 文件名,還有一個 “.” +文件擴展名
                        loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory, consumer); } } } } // 6.1.2、 該方法作用是 判斷 name 完整路徑名是否以指定的文件擴展名結尾
        private boolean canLoadFileExtension(PropertySourceLoader loader, String name) { // 6.1.3、調用 PropertySourceLoader 的 getFileExtensions 方法。當你的實現類是 PropertiesPropertySourceLoader 時,該方法返回 properties、xml;如果是 YamlPropertySourceLoader 則返回 yml、yaml。從這里可以看出,能被處理的文件格式有這四種
            return Arrays.stream(loader.getFileExtensions()) .anyMatch((fileExtension) -> StringUtils.endsWithIgnoreCase(name, fileExtension)); } // 6.3 到了這里,prefix 和 fileExtension 都是進行拼接好的值,如 prefix = classpath:/applicarion,fileExtension = .properties
        private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) { ... // 這里同樣調用節點 7 的重載方法,通過 prefix + fileExtension 形成完整的文件路徑名,通過入參進行傳遞。如 classpath:/applicarion.properties
            load(loader, prefix + fileExtension, profile, profileFilter, consumer); } // 7、
        private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter, DocumentConsumer consumer) { try { // 這里調用 ResourceLoader 的 getResource 方法,通過 location 文件路徑,讀取獲取該文件資源,之后就好辦了
                Resource resource = this.resourceLoader.getResource(location); ... // 具體解析在過程 loadDocuments 中,這里就不繼續跟蹤了,大致是以流的方式解析文件。解析之后會生成一個 PropertySource 對象,該對象在上面說過,表示一個外部化配置源對象,存儲配置中的數據。之后,會將該對象封裝到 Document 中
                List<Document> documents = loadDocuments(loader, name, resource); ... if (!loaded.isEmpty()) { // 遍歷 documents 集合,當執行 consumer.accept 時會進入 addToLoaded 方法,這是 Java8 的寫法。consumer 對象參數來自節點 5 。我們跳到 8 查看 addToLoaded 實現
                    loaded.forEach((document) -> consumer.accept(profile, document)); if (this.logger.isDebugEnabled()) { StringBuilder description = getDescription("Loaded config file ", location, resource, profile); this.logger.debug(description); } } } catch (Exception ex) { throw new IllegalStateException("Failed to load property " + "source from location '" + location + "'", ex); } } // 8、BiConsumer 是 JAVA8 的函數接口,表示定義一個帶有兩個參數且不返回結果的操作,通過節點 5 我們知道,這個操作是 MutablePropertySources::addFirst 。
        private DocumentConsumer addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>> addMethod, boolean checkForExisting) { return (profile, document) -> { if (checkForExisting) { for (MutablePropertySources merged : this.loaded.values()) { if (merged.contains(document.getPropertySource().getName())) { return; } } } MutablePropertySources merged = this.loaded.computeIfAbsent(profile, (k) -> new MutablePropertySources()); // 當調用 BiConsumer 的 accept 方法時,定義的操作會執行,兩個入參分別是 MutablePropertySources 對象和配置文件源對象 PropertySource。該操作會調用 MutablePropertySources 的 addFirst 方法把該配置文件源對象添加至其中。最后我們去看看前面 load 方法中的最后一步 9
 addMethod.accept(merged, document.getPropertySource()); }; } // 9.1
        private void addLoadedPropertySources() { // 獲取當前上下文環境中的 MutablePropertySources 對象
            MutablePropertySources destination = this.environment.getPropertySources(); // 獲取當前類中的 MutablePropertySources 集合
            List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values()); Collections.reverse(loaded); String lastAdded = null; Set<String> added = new HashSet<>(); // 遍歷 loaded 集合及其中所有的 PropertySource ,也就是 application 或 YAML 配置文件源對象
            for (MutablePropertySources sources : loaded) { for (PropertySource<?> source : sources) { if (added.add(source.getName())) { // 我們進入 9.2 查看該方法,主要參數是上下文環境中的 MutablePropertySources 對象和配置文件源對象
 addLoadedPropertySource(destination, lastAdded, source); lastAdded = source.getName(); } } } } // 9.2 
        private void addLoadedPropertySource(Mutab lePropertySources destination, String lastAdded, PropertySource<?> source) { if (lastAdded == null) { if (destination.contains(DEFAULT_PROPERTIES)) { destination.addBefore(DEFAULT_PROPERTIES, source); } else { destination.addLast(source); } } else { // 最后通過將 source 添加到 environment 中的 MutablePropertySources 對象中。
 destination.addAfter(lastAdded, source); } } // 至此,properties 和 YAML 配置文件就被加載到了上下文環境共享的 Environment 中,之后如 @Value 等獲取值都是從該對象中獲取
 } }

可以看到,ConfigFileApplicationListener 主要功能就是將 properties 和 YAML 文件加載到 Environment 中。另外還存在一個 @PropertySource 注解,也是加載指定的配置文件到 Environment 中。 

我們回到最外面的 prepareEnvironment 方法,來看看執行完監聽方法時 ConfigurableEnvironment 中加載了多少種外部化配置:

有九種,包括新增的properties、yml文件。至此, Environment 的創建過程及加載外部化配置的過程就到這里結束,我們簡要回顧一下該流程:

  • 首先 Environment 是一個較為特殊的類,術語稱之為應用運行時的環境。它存儲了所有的外部化配置,可以通過它獲取任意配置數據,並且 @Value、 @ConfigurationProperties 等其它獲取配置數據的方式都依賴於該類。
  • 通過判斷應用的類型,來創建不同環境的 Environment ,有 Servlet、Reactive、非 Web 類型。
  • 之后會相繼添加外部化配置到該類中,每種外部化配置都對應了一個 PropertySource 配置源對象。
  • 重點介紹了加載 properties 和 YAML 的方式。主要是通過回調 Spring Boot 的監聽器 ConfigFileApplicationListener 進行處理。

加載完之后,就可以在應用中通過 @Autowired 注入該對象,獲取任意外部化配置屬性。 

五、@ConfigurationProperties和@EnableConfigurationProperties

介紹完了Environment類,我們順便介紹一下@ConfigurationProperties:

眾所周知,當Spring Boot 集成外部組件后,就可在 properties 或 YAML 配置文件中定義組件需要的屬性,如 Redis 組件:

spring.redis.url=redis://user:password@example.com:6379
spring.redis.host=localhost spring.redis.password=123456 spring.redis.port=6379

其中都是以 spring.redis 為前綴。這其實是 Spring Boot 為每個組件提供了對應的 Properties 配置類,並將配置文件中的屬性值給映射到配置類中,而且它們有個特點,都是以 Properties 結尾,如 Redis 對應的配置類是 RedisProperties: 

@ConfigurationProperties(prefix = "spring.redis") public class RedisProperties { private String url; private String host = "localhost"; private String password; private int port = 6379; ... }
其中有個名為@ConfigurationProperties的注解,它的prefix參數就是約定好的前綴。該注解的功能就是將配置文件中的屬性和 Properties 配置類中的屬性進行映射,來達到自動配置的目的。
這個過程分為兩步,第一步是注冊 Properties 配置類,第二步是綁定配置屬性,過程中還涉及到一個注解,它就是 @EnableConfigurationProperties ,該注解是用來觸發那兩步操作的。我們以Redis為例來看它使用方式:
... @EnableConfigurationProperties(RedisProperties.class) public class RedisAutoConfiguration { ... }

可以看到它的參數是RedisProperties配置類。該注解是屬於@Enable 模塊注解,所以,該注解中必然有@Import 導入的實現了ImportSelector或ImportBeanDefinitionRegistrar接口的類,具體的功能都由導入的類來實現。我們進入該注解: 

@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(EnableConfigurationPropertiesImportSelector.class) public @interface EnableConfigurationProperties { /** * Convenient way to quickly register {@link ConfigurationProperties} annotated beans * with Spring. Standard Spring Beans will also be scanned regardless of this value. * @return {@link ConfigurationProperties} annotated beans to register */ Class<?>[] value() default {}; }

果不其然,通過 @Import 導入了 EnableConfigurationPropertiesImportSelector 類,整個的處理流程都是在該類中進行處理:

class EnableConfigurationPropertiesImportSelector implements ImportSelector { private static final String[] IMPORTS = { ConfigurationPropertiesBeanRegistrar.class.getName(), ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() }; @Override public String[] selectImports(AnnotationMetadata metadata) { return IMPORTS; } ... }

該類實現了 ImportSelector 接口,並重寫了selectImports 方法,該方法返回的類會被Spring加載。可以看到這里返回了兩個類,其中 ConfigurationPropertiesBeanRegistrar 就是用來注冊 Properties 配置類的,而 ConfigurationPropertiesBindingPostProcessorRegistrar 則是用來綁定配置屬性,且它們都實現了 ImportBeanDefinitionRegistrar 接口,會在重寫的 registerBeanDefinitions 方法中進行直接注冊 Bean 的操作。具體可以參考博客:SpringBoot(六)外部化配置 - @ConfigurationProperties

[1]SpringBoot(一)自動裝配基礎

[2]SpringBoot(二)自動裝配正文 - @SpringBootApplication、@EnableAutoConfiguration

[3]SpringBoot(三)SpringApplication啟動類准備階段(轉載)

[4]SpringBoot(四)SpringApplication啟動類運行階段(轉載)

[5]SpringBoot事件監聽機制

[6]SpringBoot(五)外部化配置 - Environment(轉載)

[7]SpringBoot(六)外部化配置 - @ConfigurationProperties

[8]springboot自動配置、和帶@EnableXXX開關的自動配置,自定義starter


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM