Spring Boot外部化配置實戰解析

宜信技術學院發表於2019-06-03

一、流程分析

1.1 入口程式

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

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

1.2 關鍵流程思維導圖

1.3 關鍵流程詳解

對入口程式中標記的四步,分析如下

1.3.1 SpringApplication#getRunListeners

載入 META-INF/spring.factories  

獲取 SpringApplicationRunListener  

的例項集合,存放的物件是 EventPublishingRunListener 型別 以及自定義的 SpringApplicationRunListener 實現型別

1.3.2 SpringApplication#prepareEnvironment

prepareEnvironment 方法中,主要的三步如下

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;
}
1) getOrCreateEnvironment 方法

在 WebApplicationType.SERVLET web應用型別下,會建立 StandardServletEnvironment,本文以 StandardServletEnvironment 為例,類的層次結構如下

當建立 StandardServletEnvironment,StandardServletEnvironment 父類 AbstractEnvironment 呼叫 customizePropertySources 方法,會執行 StandardServletEnvironment#customizePropertySources和 StandardEnvironment#customizePropertySources ,原始碼如下AbstractEnvironment

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 
StringSERVLET_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 順序:

  • servletConfigInitParams

  • servletContextInitParams

  • jndiProperties

  • systemProperties

  • systemEnvironment

PropertySources 與 PropertySource 關係為 1 對 N

2) configureEnvironment 方法

呼叫 configurePropertySources(environment, args), 在方法裡面設定 Environment 的 PropertySources , 包含 defaultProperties 和

SimpleCommandLinePropertySource(commandLineArgs),PropertySources 新增 defaultProperties 到最後,新增  

SimpleCommandLinePropertySource(commandLineArgs)到最前面

PropertySources 順序:

  • commandLineArgs

  • servletConfigInitParams

  • servletContextInitParams

  • jndiProperties

  • systemProperties

  • systemEnvironment

  • defaultProperties

3) 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
    // 載入自己 ConfigFileApplicationListener
    postProcessors.add(this);
    // 按照 Ordered 進行優先順序排序
    AnnotationAwareOrderComparator.sort(postProcessors);
    // 回撥 EnvironmentPostProcessor
    for (EnvironmentPostProcessor postProcessor : postProcessors) {
        postProcessor.postProcessEnvironment(event.getEnvironment(),                                            event.getSpringApplication());
    }
}
List
    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(載入 查詢出來的 PropertySource 到 PropertySources,並確保放置到 defaultProperties 的前面 )

預設的查詢位置,配置為

"classpath:/,classpath:/config/,file:./,file:./config/",查詢順序從後向前

PropertySources 順序:

  • commandLineArgs

  • servletConfigInitParams

  • servletContextInitParams

  • jndiProperties

  • systemProperties

  • systemEnvironment

  • random

  • application.properties ...

  • defaultProperties

1.3.3 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
}
1)applyInitializers 方法

會遍歷執行所有的 ApplicationContextInitializer#initialize

擴充套件點

  • 自定義 ApplicationContextInitializer
2)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
        .remove(DEFAULT_PROPERTIES);
    if (defaultProperties != null) {
        environment.getPropertySources().addLast(defaultProperties);
    }
}

PropertySourceOrderingPostProcessor 是 BeanFactoryPostProcessor

1.3.4 SpringApplication#refreshContext

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

AbstractApplicationContext#refresh 呼叫 invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors), 然後進行 BeanFactoryPostProcessor 的回撥處理 ,比如 PropertySourceOrderingPostProcessor 的回撥(原始碼見上文)

PropertySources 順序:

  • commandLineArgs

  • servletConfigInitParams

  • servletContextInitParams

  • jndiProperties

  • systemProperties

  • systemEnvironment

  • random

  • application.properties ...

  • @PropertySource annotations on your @Configuration classes  

  • defaultProperties

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

二、擴充套件外部化配置屬性源

2.1 基於 EnvironmentPostProcessor 擴充套件

public class CustomEnvironmentPostProcessor 
implements EnvironmentPostProcessor

2.2 基於 ApplicationEnvironmentPreparedEvent 擴充套件

public class 
ApplicationEnvironmentPreparedEventListener implements ApplicationListener

2.3 基於 SpringApplicationRunListener 擴充套件

public class CustomSpringApplicationRunListener implements SpringApplicationRunListener, Ordered

可以重寫方法 environmentPrepared、contextPrepared、contextLoaded 進行擴充套件

2.4 基於 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 請求方式獲取的

2.5 基於 ApplicationPreparedEvent 擴充套件

public class ApplicationPreparedEventListener 
implements ApplicationListener

2.6 擴充套件實戰

2.6.1 擴充套件配置

在 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

以上的擴充套件可以選取其中一種進行擴充套件,只是屬性源的載入時機不太一樣

2.6.2 擴充套件例項程式碼

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

三、參考資料

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

作者:石建偉

來源: 宜信技術學院


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69918724/viewspace-2646566/,如需轉載,請註明出處,否則將追究法律責任。

相關文章