spring boot外部化配置源码分析

spring boot版本为:2.1.6.RELEASE

前言

Spring Framework 3.1 开始引入 Environment 抽象,它统一 Spring 配置属性的存储,包括占位符处理和类型转换,不仅完整地替换 PropertyPlaceholderConfigurer,而且还支持更丰富的配置属性源(PropertySource)。

理解springboot外部化配置原理,有助于帮助我们理解spring cloud config、nacos config等分布式配置中心。

源码分析

Environment的初始化

在springboot启动流程的run方法中,有一个 prepareEnvironment 方法,这个方法就是用来准备Environment这个对象的。

SpringApplication

public ConfigurableApplicationContext run(String... args) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    configureHeadlessProperty();
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // 准备Environment
        ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
        configureIgnoreBeanInfo(environment);
        Banner printedBanner = printBanner(environment);
        context = createApplicationContext();
        exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context);
        prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        refreshContext(context);
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
        }
        listeners.started(context);
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        listeners.running(context);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

进入prepareEnvironment(listeners, applicationArguments)方法:

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments) {
    // Create and configure the environment
    // 创建environment对象
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    // 配置environment对象
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    // 发送ApplicationEnvironmentPreparedEvent类型的事件
    listeners.environmentPrepared(environment);
    bindToSpringApplication(environment);
    if (!this.isCustomEnvironment) {
        environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
                deduceEnvironmentClass());
    }
    ConfigurationPropertySources.attach(environment);
    return environment;
}

这个方法主要就是创建和配置spring容器的环境信息,进入getOrCreateEnvironment看它是怎么创建环境的,进去后发现这个方法,就是根据当前的webApplication类型匹配对应的environment,在servlet容器中就是StandardServletEnvironment ,如果是spring webflux,则是StandardReactiveWebEnvironment

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

StandardServletEnvironment 是整个spring boot应用运行环境的实现类,后面所有的关于环境相关的配置操作都是基于这个类,它的类的结构图如下:

图片.png

StandardServletEnvironment的初始化过程会做一些事情,就是配置一些基本的属性来源。StandardServletEnvironment 会初始化父类 AbstractEnvironment ,在这个类的构造方法中,会调用一个自定义配置文件的方法,这个是spring中比较常见的实现手法,前面在看ribbon、eureka中都有看到。

AbstractEnvironment

public AbstractEnvironment() {
    customizePropertySources(this.propertySources);
}

customizePropertySources 这个方法被 StandardServletEnvironment 重写了,所以会调用StandardServletEnvironment 中的 customizePropertySources 方法。不难看出,这里是将几个不同的配置源封装成 StubPropertySource 添加到
MutablePropertySources 中,调用 addLast 是表示一直往最后的位置添加。

  • SERVLET_CONFIG_PROPERTY_SOURCE_NAME:servlet config的配置信息
  • SERVLET_CONTEXT_PROPERTY_SOURCE_NAME: 这个是servlet初始化的上下文,也就是以前我们在web.xml中配置的 context-param 。
  • JNDI_PROPERTY_SOURCE_NAME: 加载jndi.properties配置信息。

StandardServletEnvironment

@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_PROPERTIES_PROPERTY_SOURCE_NAME: 系统变量,通过System.setProperty设置的变量,默认可以看到 java.version 、 os.name 等。SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME: 系统环境变量,也就是我们配置JAVA_HOME的地方。

StandardEnvironment

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

这里要明确一点,就是添加PropertySource的目的其实就是要告诉Environment,解析哪些位置的属性文件进行加载。而在这个添加过程中,所有的添加都是基于 addLast ,也就是最早添加的PropertySource会放在最前面。 systemEnvironment 是在 systemProperties 前面,这点很重要。因为前面的配置会覆盖后面的配置,也就是说系统变量中的配置比系统环境变量中的配置优先级更高。

MutablePropertySources

在上面的代码中可以看到,所有的外部资源配置都是添加到了一个MutablePropertySources对象中,这个对象封装了属性资源的集合。而从 MutablePropertySources 命名来说,Mutable是一个可变的意思,也就是意味着它动态的管理了PropertySource的集合。

public class MutablePropertySources implements PropertySources {

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

    // 省略...
}

通过上面的类图可以发现AbstractEnvironment 实现了文件解析器ConfigurablePropertyResolver ,而在下面这段代码中我们把 MutablePropertySources 传递到PropertySourcesPropertyResolver 中。这样就可以让 AbstractEnvironment 具备文件解析的功能,只是这个功能,委托给了PropertySourcesPropertyResolver来实现。

public abstract class AbstractEnvironment implements ConfigurableEnvironment {
    //省略...
    private final MutablePropertySources propertySources = new MutablePropertySources();

    private final ConfigurablePropertyResolver propertyResolver =
            new PropertySourcesPropertyResolver(this.propertySources);
    //省略...
}

现在回退到SpringApplication类的prepareEnvironment方法,我们继续来看 configureEnvironment 这个方法,这个方法有三个作用:

  • addConversionService :添加类型转化的服务,我们知道properties文件中配置的属性都是String类型的,而转化为Java对象之后要根据合适的类型进行转化,而 ConversionService 是一套通用的转化方案,请参考《SpringMVC之类型转换Converter》,这里把这个转化服务设置到当前的Environment,很显然,就是为Environment配置解析时提供一个类型转化的解决方案。
  • configurePropertySources: 配置Environment中的propertysources
  • configureProfiles :配置profiles
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
    if (this.addConversionService) {
        ConversionService conversionService = ApplicationConversionService.getSharedInstance();
        environment.setConversionService((ConfigurableConversionService) conversionService);
    }
    configurePropertySources(environment, args);
    configureProfiles(environment, args);
}

进入configurePropertySources方法:

protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
    MutablePropertySources sources = environment.getPropertySources();
    if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
        sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties));
    }
    if (this.addCommandLineProperties && args.length > 0) {
        String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
        if (sources.contains(name)) {
            PropertySource<?> source = sources.get(name);
            CompositePropertySource composite = new CompositePropertySource(name);
            composite.addPropertySource(
                    new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
            composite.addPropertySource(source);
            sources.replace(name, composite);
        }
        else {
            sources.addFirst(new SimpleCommandLinePropertySource(args));
        }
    }
}
  • 设置 defaultProperties 属性来源,此处可以在构造SpringApplication对象时,通过他的setDefaultProperties方法传入额外的配置。
  • 设置commandLineProperties来源,如果设置了命令行参数,则会加载SimpleCommandLinePropertySource 作为propertySource

到目前为止,还是在初始化外部化配置的数据来源。接着进入configureProfiles方法,这个方法就比较容易理解,就是配置当前激活的profiles,将当前的activeProfiles设置到enviroment中。这样就能够使得我们完成不同环境下配置的获取问题。

protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
    environment.getActiveProfiles(); // ensure they are initialized
    // But these ones should go first (last wins in a property key clash)
    Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
    profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
    environment.setActiveProfiles(StringUtils.toStringArray(profiles));
}

此时environment中的activeProfiles集合还是空的。additionalProfiles可以通过SpringApplication对象的setAdditionalProfiles方法设置。

经过上面的操作spring的配置信息都已加载完成,但有一个很重要的配置还没有加载,那就是springboot的yml或properties文件中的配置信息,现在回退到SpringApplication类的prepareEnvironment类。

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments) {
    // 省略...
    listeners.environmentPrepared(environment);
    // 省略...
    return environment;
}

进入listeners.environmentPrepared方法:

SpringApplicationRunListeners

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

循环遍历SpringApplicationRunListener对象集合,然后调用它的environmentPrepared方法。在springboot环境中,默认在META-INF/spring.factories中,配置的实现类是EventPublishingRunListener

EventPublishingRunListener

@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
    this.initialMulticaster
            .multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
}

内部会委托给SimpleApplicationEventMulticaster对象,进行广播ApplicationEnvironmentPreparedEvent事件。在这个类内部最终会循环遍历spring容器中实现了ApplicationListener的bean,然后调用它的onApplicationEvent方法。

SimpleApplicationEventMulticaster

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

以上代码其实也是spring事件机制背后的实现原理。

在springboot环境中,ConfigFileApplicationListener类实现了ApplicationListener接口,那么进入ConfigFileApplicationListener类会看到一个onApplicationEvent方法:

ConfigFileApplicationListener

@Override
public void onApplicationEvent(ApplicationEvent event) {
    if (event instanceof ApplicationEnvironmentPreparedEvent) {
        onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
    }
    if (event instanceof ApplicationPreparedEvent) {
        onApplicationPreparedEvent(event);
    }
}

因为现在发布的是ApplicationEnvironmentPreparedEvent类型的事件,所以会进入onApplicationEnvironmentPreparedEvent方法中:

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
    postProcessors.add(this);
    AnnotationAwareOrderComparator.sort(postProcessors);
    for (EnvironmentPostProcessor postProcessor : postProcessors) {
        postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
    }
}

此方法主要做了三件事:

  • spring.factories文件中,加载实现了EnvironmentPostProcessor接口的类。
  • 将当前对象加入到postProcessors列表中,因为当前类实现了EnvironmentPostProcessor接口
  • 循环遍历postProcessors列表,调用postProcessEnvironment方法

由于当前类实现了EnvironmentPostProcessor接口,所以会调用它的postProcessEnvironment方法:

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

进入addPropertySources方法:

protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
    RandomValuePropertySource.addToEnvironment(environment);
    new Loader(environment, resourceLoader).load();
}

这个方法做两个事情:

  • 添加一个RandomValuePropertySourceEnvironmentMutablePropertySources中,放在系统环境变量的后面
  • 加载spring boot中的配置信息,比如application.yml或者application.properties

进入Loaderload方法:

ConfigFileApplicationListener#Loader

public void load() {
    this.profiles = new LinkedList<>();
    this.processedProfiles = new LinkedList<>();
    this.activatedProfiles = false;
    this.loaded = new LinkedHashMap<>();
    initializeProfiles();
    while (!this.profiles.isEmpty()) {
        Profile profile = this.profiles.poll();
        if (profile != null && !profile.isDefaultProfile()) {
            addProfileToEnvironment(profile.getName());
        }
        load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
        this.processedProfiles.add(profile);
    }
    resetEnvironmentProfiles(this.processedProfiles);
    load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
    addLoadedPropertySources();
}

这块的代码就不继续深入分析了,有点绕,感兴趣的小伙伴深入去看看。load 所做的事情如下:

  • 获取默认的配置文件路径,有4种:
    classpath:/,classpath:/config/,file:./,file:./config/*/,file:./config/
  • 遍历所有的路径,拼装配置文件名称。
  • 再遍历解析器,选择yml或者properties解析,将解析结果添加到集合MutablePropertySources当中。

至此,springBoot中的资源文件加载完毕,解析顺序从上到下,所以前面的配置文件会覆盖后面的配置文件。可以看到 application.properties 的优先级最低,系统变量和环境变量的优先级相对较高。

图片.png

扩展外部化配置属性源

扩展点

  1. 基于 EnvironmentPostProcessor 扩展
public class CustomEnvironmentPostProcessor implements EnvironmentPostProcessor
  1. 基于 ApplicationEnvironmentPreparedEvent事件扩展
public class ApplicationEnvironmentPreparedEventListener implements ApplicationListener
  1. 基于 SpringApplicationRunListener 扩展
public class CustomSpringApplicationRunListener implements SpringApplicationRunListener, Ordered
  1. 基于 ApplicationContextInitializer 扩展
public class CustomApplicationContextInitializer implements ApplicationContextInitializer
  1. 基于 ApplicationPreparedEvent 扩展
public class ApplicationPreparedEventListener implements ApplicationListener

配置

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

以上的扩展可以选取其中一种进行扩展,只是属性源的加载时机不太一样,越早越好。

总结

掌握了springboot外部化配置的实现原理,为以后学习和理解spring cloud环境下,加载远程配置中心的配置原理,打下良好的基础。

欢迎关注我的公众号:程序员L札记

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容