Spring Boot 外部化配置實戰解析

來源:[宜信技術學院 ] html

做者:石建偉java

1、流程分析

入口程序

SpringApplication#run(String... args) 方法中,外部化配置關鍵流程分爲如下四步git

public ConfigurableApplicationContext run(String... args) {
    ...
    SpringApplicationRunListeners listeners = getRunListeners(args); // 1
    listeners.starting();
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
            args);
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                                                                 applicationArguments); // 2
        configureIgnoreBeanInfo(environment);
        Banner printedBanner = printBanner(environment);
        context = createApplicationContext();
        exceptionReporters = getSpringFactoriesInstances(
            SpringBootExceptionReporter.class,
            new Class[] { ConfigurableApplicationContext.class }, context);
        prepareContext(context, environment, listeners, applicationArguments,
                       printedBanner); // 3
        refreshContext(context); // 4
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass)
                .logStarted(getApplicationLog(), stopWatch);
        }
        listeners.started(context);
        callRunners(context, applicationArguments);
    }
    ...
}

關鍵流程思惟導圖

關鍵流程詳解

對入口程序中標記的四步,分析以下github

一、SpringApplication#getRunListeners

加載 META-INF/spring.factories 獲取 SpringApplicationRunListener 的實例集合,存放的對象是 EventPublishingRunListener 類型 以及自定義的 SpringApplicationRunListener 實現類型web

二、SpringApplication#prepareEnvironment

prepareEnvironment 方法中,主要的三步以下spring

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
    ApplicationArguments applicationArguments) {
    // Create and configure the environment
    ConfigurableEnvironment environment = getOrCreateEnvironment(); // 2.1
    configureEnvironment(environment, applicationArguments.getSourceArgs()); // 2.2
    listeners.environmentPrepared(environment); // 2.3
    ...
    return environment;
}

2.一、getOrCreateEnvironment 方法bootstrap

WebApplicationType.SERVLET web應用類型下,會建立 StandardServletEnvironment,本文以 StandardServletEnvironment 爲例,類的層次結構以下springboot

當建立 StandardServletEnvironmentStandardServletEnvironment 父類 AbstractEnvironment 調用 customizePropertySources 方法,會執行 StandardServletEnvironment#customizePropertySourcesStandardEnvironment#customizePropertySources ,源碼以下app

AbstractEnvironmentdom

public AbstractEnvironment() {
    customizePropertySources(this.propertySources);
    if (logger.isDebugEnabled()) {
        logger.debug("Initialized " + getClass().getSimpleName() + " with PropertySources " + this.propertySources);
    }
}

StandardServletEnvironment#customizePropertySources

/** 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";

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

StandardEnvironment#customizePropertySources

/** 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";

@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
    propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
    propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,getSystemEnvironment());
}

PropertySources 順序:

  1. servletConfigInitParams
  2. servletContextInitParams
  3. jndiProperties
  4. systemProperties
  5. systemEnvironment

PropertySourcesPropertySource 關係爲 1 對 N

2.二、configureEnvironment 方法

調用 configurePropertySources(environment, args), 在方法裏面設置 EnvironmentPropertySources , 包含 defaultPropertiesSimpleCommandLinePropertySource (commandLineArgs),PropertySources 添加 defaultProperties 到最後,添加 SimpleCommandLinePropertySource (commandLineArgs)到最前面

PropertySources 順序:

  1. commandLineArgs

  2. servletConfigInitParams

  3. servletContextInitParams

  4. jndiProperties

  5. systemProperties

  6. systemEnvironment

  7. defaultProperties

2.三、listeners.environmentPrepared 方法

會按優先級順序遍歷執行 SpringApplicationRunListener#environmentPrepared,好比 EventPublishingRunListener 和 自定義的 SpringApplicationRunListener

  • EventPublishingRunListener 發佈 ApplicationEnvironmentPreparedEvent 事件

  • ConfigFileApplicationListener 監聽 ApplicationEvent 事件 、處理 ApplicationEnvironmentPreparedEvent 事件,加載全部 EnvironmentPostProcessor 包括本身,而後按照順序進行方法回調

    • ConfigFileApplicationListener#postProcessEnvironment 方法回調 ,而後addPropertySources 方法調用 RandomValuePropertySource#addToEnvironment,在 systemEnvironment 後面添加 random,而後添加配置文件的屬性源(詳見源碼 ConfigFileApplicationListener.Loader#load()
  • 擴展點

  • 自定義 SpringApplicationRunListener ,重寫 environmentPrepared 方法

  • 自定義 EnvironmentPostProcessor

  • 自定義 ApplicationListener 監聽 ApplicationEnvironmentPreparedEvent 事件

ConfigFileApplicationListener,便是 EnvironmentPostProcessor ,又是 ApplicationListener ,類的層次結構以下

@Override
public void onApplicationEvent(ApplicationEvent event) {
    // 處理 ApplicationEnvironmentPreparedEvent 事件
    if (event instanceof ApplicationEnvironmentPreparedEvent) {
        onApplicationEnvironmentPreparedEvent(
            (ApplicationEnvironmentPreparedEvent) event);
    }
    // 處理 ApplicationPreparedEvent 事件
    if (event instanceof ApplicationPreparedEvent) {
        onApplicationPreparedEvent(event);
    }
}

private void onApplicationEnvironmentPreparedEvent(
    ApplicationEnvironmentPreparedEvent event) {
    // 加載 META-INF/spring.factories 中配置的 EnvironmentPostProcessor
    List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
    // 加載本身 ConfigFileApplicationListener
    postProcessors.add(this);
    // 按照 Ordered 進行優先級排序
    AnnotationAwareOrderComparator.sort(postProcessors);
    // 回調 EnvironmentPostProcessor
    for (EnvironmentPostProcessor postProcessor : postProcessors) {
        postProcessor.postProcessEnvironment(event.getEnvironment(),
                                             event.getSpringApplication());
    }
}

List<EnvironmentPostProcessor> loadPostProcessors() {
    return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class,
                                               getClass().getClassLoader());
}

@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
                                   SpringApplication application) {
    addPropertySources(environment, application.getResourceLoader());
}

/**
  * Add config file property sources to the specified environment.
  * @param environment the environment to add source to
  * @param resourceLoader the resource loader
  * @see #addPostProcessors(ConfigurableApplicationContext)
  */
protected void addPropertySources(ConfigurableEnvironment environment,
                                  ResourceLoader resourceLoader) {
    RandomValuePropertySource.addToEnvironment(environment);
    // 添加配置文件的屬性源
    new Loader(environment, resourceLoader).load();
}

RandomValuePropertySource

public static void addToEnvironment(ConfigurableEnvironment environment) {
    // 在 systemEnvironment 後面添加 random
    environment.getPropertySources().addAfter(
        StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
        new RandomValuePropertySource(RANDOM_PROPERTY_SOURCE_NAME));
    logger.trace("RandomValuePropertySource add to Environment");
}

添加配置文件的屬性源:

執行 new Loader(environment, resourceLoader).load(); , 調用 load(Profile, DocumentFilterFactory, DocumentConsumer) (getSearchLocations() 獲取配置文件位置,能夠指定經過 spring.config.additional-location 、spring.config.location 、spring.config.name 參數或者使用默認值 ), 而後調用 addLoadedPropertySources -> addLoadedPropertySource(加載 查找出來的 PropertySourcePropertySources,並確保放置到 defaultProperties 的前面 )

默認的查找位置,配置爲 "classpath:/,classpath:/config/,file:./,file:./config/" ,查找順序從後向前

PropertySources 順序:

  1. commandLineArgs
  2. servletConfigInitParams
  3. servletContextInitParams
  4. jndiProperties
  5. systemProperties
  6. systemEnvironment
  7. random
  8. application.properties …
  9. defaultProperties

三、SpringApplication#prepareContext

prepareContext 方法中,主要的三步以下

private void prepareContext(ConfigurableApplicationContext context,
                            ConfigurableEnvironment environment, 
                            SpringApplicationRunListeners listeners,
                            ApplicationArguments applicationArguments, 
                            Banner printedBanner) {
    ...
    applyInitializers(context); // 3.1
    listeners.contextPrepared(context); //3.2
    ...
    listeners.contextLoaded(context); // 3.3
}

3.一、applyInitializers 方法

會遍歷執行全部的 ApplicationContextInitializer#initialize

  • 擴展點
  • 自定義 ApplicationContextInitializer

3.二、listeners.contextPrepared 方法

會按優先級順序遍歷執行 SpringApplicationRunListener#contextPrepared,好比 EventPublishingRunListener 和 自定義的 SpringApplicationRunListener

  • 擴展點
  • 自定義 SpringApplicationRunListener ,重寫 contextPrepared 方法

3.三、listeners.contextLoaded 方法

會按優先級順序遍歷執行 SpringApplicationRunListener#contextLoaded,好比 EventPublishingRunListener 和 自定義的 SpringApplicationRunListener

  • EventPublishingRunListener 發佈 ApplicationPreparedEvent 事件

  • ConfigFileApplicationListener 監聽 ApplicationEvent 事件 處理 ApplicationPreparedEvent 事件

  • 擴展點

  • 自定義 SpringApplicationRunListener ,重寫 contextLoaded 方法

  • 自定義 ApplicationListener ,監聽 ApplicationPreparedEvent 事件

ConfigFileApplicationListener

@Override
public void onApplicationEvent(ApplicationEvent event) {
    // 處理 ApplicationEnvironmentPreparedEvent 事件
    if (event instanceof ApplicationEnvironmentPreparedEvent) {
        onApplicationEnvironmentPreparedEvent(
            (ApplicationEnvironmentPreparedEvent) event);
    }
    // 處理 ApplicationPreparedEvent 事件
    if (event instanceof ApplicationPreparedEvent) {
        onApplicationPreparedEvent(event);
    }
}

private void onApplicationPreparedEvent(ApplicationEvent event) {
    this.logger.replayTo(ConfigFileApplicationListener.class);
    addPostProcessors(((ApplicationPreparedEvent) event).getApplicationContext());
}

// 添加 PropertySourceOrderingPostProcessor 處理器,配置 PropertySources
protected void addPostProcessors(ConfigurableApplicationContext context) {
    context.addBeanFactoryPostProcessor(
        new PropertySourceOrderingPostProcessor(context));
}

PropertySourceOrderingPostProcessor

// 回調處理(在配置類屬性源解析)
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
    throws BeansException {
    reorderSources(this.context.getEnvironment());
}

// 調整 PropertySources 順序,先刪除 defaultProperties, 再把 defaultProperties 添加到最後
private void reorderSources(ConfigurableEnvironment environment) {
    PropertySource<?> defaultProperties = environment.getPropertySources()
        .remove(DEFAULT_PROPERTIES);
    if (defaultProperties != null) {
        environment.getPropertySources().addLast(defaultProperties);
    }
}

PropertySourceOrderingPostProcessorBeanFactoryPostProcessor

四、SpringApplication#refreshContext

會進行 @Configuration 配置類屬性源解析,處理 @PropertySource annotations on your @Configuration classes,但順序是在 defaultProperties 以後,下面會把 defaultProperties 調整到最後

AbstractApplicationContext#refresh 調用 invokeBeanFactoryPostProcessors (PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors), 而後進行 BeanFactoryPostProcessor 的回調處理 ,好比 PropertySourceOrderingPostProcessor 的回調(源碼見上文)

PropertySources 順序:

  1. commandLineArgs

  2. servletConfigInitParams

  3. servletContextInitParams

  4. jndiProperties

  5. systemProperties

  6. systemEnvironment

  7. random

  8. application.properties …

  9. @PropertySource annotations on your @Configuration classes

  10. defaultProperties

不推薦使用這種方式,推薦使用在 refreshContext 以前準備好,@PropertySource 加載太晚,不會對自動配置產生任何影響

2、擴展外部化配置屬性源

一、基於 EnvironmentPostProcessor 擴展

public class CustomEnvironmentPostProcessor implements EnvironmentPostProcessor

二、基於 ApplicationEnvironmentPreparedEvent 擴展

public class ApplicationEnvironmentPreparedEventListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent>

三、基於 SpringApplicationRunListener 擴展

public class CustomSpringApplicationRunListener implements SpringApplicationRunListener, Ordered

能夠重寫方法 environmentPrepared、contextPrepared、contextLoaded 進行擴展

四、基於 ApplicationContextInitializer 擴展

public class CustomApplicationContextInitializer implements ApplicationContextInitializer

關於與 Spring Cloud Config Client 整合,對外部化配置加載的擴展(綁定到Config Server,使用遠端的property sources 初始化 Environment),參考源碼 PropertySourceBootstrapConfiguration(是對 ApplicationContextInitializer 的擴展)、ConfigServicePropertySourceLocator#locate

獲取遠端的property sources是 RestTemplate 經過向 http://{spring.cloud.config.uri}/{spring.application.name}/{spring.cloud.config.profile}/{spring.cloud.config.label} 發送 GET 請求方式獲取的

五、基於 ApplicationPreparedEvent 擴展

public class ApplicationPreparedEventListener implements ApplicationListener<ApplicationPreparedEvent>

六、擴展實戰

6.一、擴展配置

在 classpath 下添加配置文件 META-INF/spring.factories, 內容以下

# Spring Application Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
springboot.propertysource.extend.listener.CustomSpringApplicationRunListener

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
springboot.propertysource.extend.initializer.CustomApplicationContextInitializer

# Application Listeners
org.springframework.context.ApplicationListener=\
springboot.propertysource.extend.event.listener.ApplicationEnvironmentPreparedEventListener,\
springboot.propertysource.extend.event.listener.ApplicationPreparedEventListener

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
springboot.propertysource.extend.processor.CustomEnvironmentPostProcessor

以上的擴展能夠選取其中一種進行擴展,只是屬性源的加載時機不太同樣

6.二、擴展實例代碼

https://github.com/shijw823/springboot-externalized-configuration-extend.git

PropertySources 順序:

propertySourceName: [ApplicationPreparedEventListener], propertySourceClassName: [OriginTrackedMapPropertySource]

propertySourceName: [CustomSpringApplicationRunListener-contextLoaded], propertySourceClassName: [OriginTrackedMapPropertySource]

propertySourceName: [CustomSpringApplicationRunListener-contextPrepared], propertySourceClassName: [OriginTrackedMapPropertySource]

propertySourceName: [CustomApplicationContextInitializer], propertySourceClassName: [OriginTrackedMapPropertySource]

propertySourceName: [bootstrapProperties], propertySourceClassName: [CompositePropertySource]

propertySourceName: [configurationProperties], propertySourceClassName: [ConfigurationPropertySourcesPropertySource]

propertySourceName: [CustomSpringApplicationRunListener-environmentPrepared], propertySourceClassName: [OriginTrackedMapPropertySource]

propertySourceName: [CustomEnvironmentPostProcessor-dev-application], propertySourceClassName: [OriginTrackedMapPropertySource]

propertySourceName: [ApplicationEnvironmentPreparedEventListener], propertySourceClassName: [OriginTrackedMapPropertySource]

propertySourceName: [commandLineArgs], propertySourceClassName: [SimpleCommandLinePropertySource]

propertySourceName: [servletConfigInitParams], propertySourceClassName: [StubPropertySource]

propertySourceName: [servletContextInitParams], propertySourceClassName: [ServletContextPropertySource]

propertySourceName: [systemProperties], propertySourceClassName: [MapPropertySource]

propertySourceName: [systemEnvironment], propertySourceClassName: [OriginAwareSystemEnvironmentPropertySource]

propertySourceName: [random], propertySourceClassName: [RandomValuePropertySource]

propertySourceName: [applicationConfig: [classpath:/extend/config/springApplicationRunListener.properties]], propertySourceClassName: [OriginTrackedMapPropertySource]

propertySourceName: [applicationConfig: [classpath:/extend/config/applicationListener.properties]], propertySourceClassName: [OriginTrackedMapPropertySource]

propertySourceName: [applicationConfig: [classpath:/extend/config/applicationContextInitializer.properties]], propertySourceClassName: [OriginTrackedMapPropertySource]

propertySourceName: [applicationConfig: [classpath:/extend/config/environmentPostProcessor.properties]], propertySourceClassName: [OriginTrackedMapPropertySource]

propertySourceName: [applicationConfig: [classpath:/extend/config/application.properties]], propertySourceClassName: [OriginTrackedMapPropertySource]

propertySourceName: [applicationConfig: [classpath:/extend/config/config.properties]], propertySourceClassName: [OriginTrackedMapPropertySource]

propertySourceName: [applicationConfig: [classpath:/application.properties]], propertySourceClassName: [OriginTrackedMapPropertySource]

propertySourceName: [springCloudClientHostInfo], propertySourceClassName: [MapPropertySource]

propertySourceName: [applicationConfig: [classpath:/bootstrap.properties]], propertySourceClassName: [OriginTrackedMapPropertySource]

propertySourceName: [propertySourceConfig], propertySourceClassName: [ResourcePropertySource]

propertySourceName: [defaultProperties], propertySourceClassName: [MapPropertySource]

bootstrapProperties 是 獲取遠端(config-server)的 property sources

加載順序也可參考 http://{host}:{port}/actuator/env

PropertySources 單元測試順序:

@TestPropertySource#properties
@SpringBootTest#properties
@TestPropertySource#locations

3、參考資料

https://docs.spring.io/spring-boot/docs/2.0.5.RELEASE/reference/htmlsingle/#boot-features-external-config

相關文章
相關標籤/搜索