Spring Boot 配置初始化流程

本文深入探讨了SpringBoot中配置文件的加载流程,包括ConfigFileApplicationListener的作用、配置文件加载的优先级、EnvironmentPostProcessor切面的应用,以及Loader如何解析和加载配置文件。
摘要由CSDN通过智能技术生成

Spring Boot会按照一定的优先级读取配置文件,优先级见官网,注册到系统环境中的时间是不一样的,也就是说配置生效范围不同,例如在application.yml中设置config是不会生效的:

 

spring:
  config:
    location: 'file:./my-app-config/application.properties'

也就是说,加载资源逻辑先被执行了,然后配置才被读入到Spring中,自然配置参数是无效的。怎么让配置参数在适合的时机读取到Spring中,怎么保证执行的先后顺序,这就需要对Spring Boot配置文件的读取流程有一定的了解。
为了探究加载的顺序,可以写一个Hello World的Spring Boot程序,配置文件中将日志等级调到最低:

 

spring:
  main:
    web-application-type: none
logging:
  path: '../springbootconfig/'
  level:
    org.springframework.boot:
      trace

启动程序后查看输出的日志,截取了前几行,可以发现配置文件的加载和ConfigFileApplicationListener相关:

 

2019-05-26 14:19:50.623 DEBUG 13852 --- [restartedMain] .c.l.ClasspathLoggingApplicationListener : Application started with classpath: [file:/G:/spring%20boot%20configuration/out/production/classes/, file:/G:/spring%20boot%20configuration/out/production/resources/]
2019-05-26 14:19:50.729  INFO 13852 --- [restartedMain] demo.App                                 : Starting App on DESKTOP-HTJRA28 with PID 13852 (started by 66439 in G:\spring boot configuration)
2019-05-26 14:19:50.729  INFO 13852 --- [restartedMain] demo.App                                 : The following profiles are active: prod
2019-05-26 14:19:50.729 DEBUG 13852 --- [restartedMain] o.s.boot.SpringApplication               : Loading source class demo.App
2019-05-26 14:19:50.811 TRACE 13852 --- [restartedMain] o.s.b.c.c.ConfigFileApplicationListener  : Skipped missing config 'file:./config/application.properties' (file:./config/application.properties)
2019-05-26 14:19:50.811 TRACE 13852 --- [restartedMain] o.s.b.c.c.ConfigFileApplicationListener  : Skipped missing config 'file:./config/application.xml' (file:./config/application.xml)

ConfigFileApplicationListener

截取部分该类上的注释,简单翻译下:

EnvironmentPostProcessor会从已知的路径加载系统环境的配置参数,默认的,会从以下路径读取application.properties以及application.yml
classpath:
file:./
classpath:config/
file:./config/:
加载路径和配置文件名,可以使用setSearchLocations(String)serSearchNames(String)定义,此外,也能通过配置项中的spring.config.locationspring.config.name来定义。

回顾之前说的在application.yml中设置config不会生效,这里就很容易理解了,以spring.config.location为例结合源码:当设置了spring.config.location就返回自定义的搜索路径,否则用默认的搜索路径,所以在默认搜索路径下的配置文件设置spring.config.location是不会有效果的,应该已经过了获取路径的逻辑了。

 

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/";
    locations.addAll(
            asResolvedSet(ConfigFileApplicationListener.this.searchLocations,
                    DEFAULT_SEARCH_LOCATIONS));
    return locations;
}

大致了解了ConfigFileApplicationListener是做什么的,接下来需要知道这个类是什么时候执行相关的方法的。首先从类型知道只是一个listener,类声明是:
public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered,先来看所实现的接口。

EnvironmentPostProcessor

就是Spring上下文的配置切面,同样,简单翻译说明下该类的注释。

当应用的上下文被刷新,允许自定义应用的Evironment。实现EnvironmentPostProcessor的类必须在META-INF/spring.factories中以org.springframework.boot.env.EnvironmentPostProcessor为键,实现类的全限定名为值声明,多个实现类用逗号分隔。官方建议同时实现org.springframework.core.Ordered接口定义优先级。

这里需要说明下在Spring中,Environment、Profile、Property的关系。

  • Environment
    和字面意思一样,是一个应用的运行环境,是面向系统层面的,提供统一访问各种配置源的配置的方式。也就是Spring提供的一个对外接口,用来访问在当前环境下系统各个参数。
    Environment包含一个或多个Profile。
  • Profile
    系统中并没有Profile这个类(其实有一个ConfigFileApplicationListener.Profile的内部类,但也仅仅记录了name[String]和defaultProfile[boolean]两个属性),是用个String在系统中表达的。在现实中可能会有多个不同的运行环境(注意不是Spring的Environment),例如开发环境、测试环境、生成环境等,不同的环境下应该会有不同的配置或者bean,这时就可以用profile来标志不同的环境。需要说明的是,Profile并不局限与用作环境的分隔,你可以将任何概念定义为一个Profile,毕竟在Spring中它只是一个String。
    在配置上,一个Profile会有多个Property。
  • Property
    这个就是我们所说的配置了。

总之,Spring会根据Profile的不同,合并各个Profile下的配置文件(last win),然后注入到Environment中。

SmartApplicationListener

监听系统事件的一个接口,不同于ApplicationListener,额外提供了supportsEventType(eventType), supportsSourceType(sourceType)两个过滤方法,官方建议使用GenericApplicationListenerGenericApplicationListener额外还继承了Ordered接口。

Ordered

该接口只有一个方法int getOrder(),看名字也知道是用来排序的,例如排优先级等,不过排优先级的话建议使用PriorityOrdered接口。

ConfigFileApplicationListener监听的事件

说完了ConfigFileApplicationListener所实现的各个接口,首先应该关注的就是该监听器所监听的事件,也就是SmartApplicationListener的实现:

 

@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
    return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType)
            || ApplicationPreparedEvent.class.isAssignableFrom(eventType);
}

@Override
public boolean supportsSourceType(Class<?> aClass) {
    return true;
}

监听器会对两个应用程序事件做处理,先来看这两个事件是什么,具体的Spring启动流程网上有很多讲解,这里就不再赘述:

  • ApplicationEnvironmentPreparedEvent
    SpringApplication启动时,当Environment第一次准备好做检查、修改时,会发布该事件。
  • ApplicationPreparedEvent
    SpringApplication启动并且ApplicationContext已经准备完毕但未刷新时发布事件,此时bean已经加载完成Environment已经可以使用。

也就是说,会在两个时刻处理配置文件,首先来看在ApplicationEnvironmentPreparedEvent,主要是获取了切面(具体有哪些切面在EnvironmentPostProcessor切面讨论),然后执行对Environment的修改:

 

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    //从META-INF/spring.factories从读取所有EnvironmentPostProcessor的实现
    List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
    postProcessors.add(this);
    AnnotationAwareOrderComparator.sort(postProcessors);//优先级排序
    //处理Environment
    for (EnvironmentPostProcessor postProcessor : postProcessors) {
        postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
    }
}

接下来是处理Environment的方法:

 

@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
    addPropertySources(environment, application.getResourceLoader());
}
//添加配置的到指定的Environment
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
    //随机数配置生成器,用于解析配置文件中random.开头的配置值
    RandomValuePropertySource.addToEnvironment(environment);
    //一个内部类,用于加载配置,主要的逻辑都在这个内部类中
    new Loader(environment, resourceLoader).load();
}

Loader的逻辑在后边Loader会详细分析,先回过头看下对另一个事件ApplicationPreparedEvent的处理:

 

private void onApplicationPreparedEvent(ApplicationEvent event) {
    this.logger.replayTo(ConfigFileApplicationListener.class);
    addPostProcessors(((ApplicationPreparedEvent) event).getApplicationContext());
}
//ApplicationContext添加对PropertySource优先级的处理切面
protected void addPostProcessors(ConfigurableApplicationContext context) {
    context.addBeanFactoryPostProcessor(new PropertySourceOrderingPostProcessor(context));
}

Environment可以使用后,会优先级做调整。

EnvironmentPostProcessor切面

从前面的分析从可以知道,当Spring发布ApplicationEnvironmentPreparedEvent事件时,监听器会获取当前类加载器上的所有EnvironmentPostProcessor切面,然后按优先级逐个处理。这里用静态分析的方式已经无法继续阅读源码,以下的分析会在调试模式下进行。
一个Spring上下文只会发布一次ApplicationEnvironmentPreparedEvent,分析onApplicationEnvironmentPreparedEvent方法,首先需要知道类加载器上都加载了哪些EnvironmentPostProcessor的实现:

 

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

通过调试可知,ClassLoader是ApplicationClassLoader,也就是默认的系统类加载器,继续调试代码可以发现,Spring将加载记录存在了一个cache里,第一次加载时会从META-INF/spring.factories读取类的全限定名,这也是为什么要求实现了EnvironmentPostProcessor接口的类需要在META-INF/spring.factories注册的原因。
Hello World加载到的类名如下,这些类会被稍后实例化并按Ordered接口返回的值排序:

EnvironmentPostProcessor子类


排序后返回的List会添加上ConfigFileApplicationListener,也就是一直在讨论的这个监听器,最后的结果如下,这些都是会影响到Environment的切面,我们来逐个简单分析:

排序后的结果

 

  1. SystemEnvironmentPropertySourceEnvironmentPostProcessor
    这个切面就是把系统参数--environment.getPropertySources().get(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)PropertySource类用OriginAwareSystemEnvironmentPropertySource进行封装,然后replace。
  2. SpringApplicationJsonEnvironmentPostProcessor
    将当前参数(也就是系统参数)的key为spring.application.json或者SPRING_APPLICATION_JSON(存在前者则忽略后者)的字符串解析为json,并将解析到的json包装成JsonPropertySource,注入到配置中。如果存在JNDI则注入到JNDI前,否则如果存在系统环境变量,则注入到系统环境变量前(本例的情形),否则作为第一个PropertySource。
  3. CloudFoundryVcapEnvironmentPostProcessor
    这个是Spring Cloud相关的,这里并未用到,不展开了。
  4. ConfigFileApplicationListener
    也就是前面讨论的监听器,这里不再重复。后面会讨论Loader的实现。
  5. DevToolsHomePropertiesPostProcessor
    具体在另外一片文章Spring Boot 配置和配置文件中的Devtools全局配置有提到,这里不再赘述
  6. DevToolsPropertyDefaultsPostProcessor
    项目依赖spring-boot-devtools后,会导入一些spring-boot-devtools的默认配置,代码很简单自行调试源码即可。

Loader

加载配置文件的逻辑几乎都在Loader里,所以单独拿出来分析下Loader的逻辑。LoaderConfigFileApplicationListener的一个内部类,首先看构造函数:

 

Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
    this.environment = environment;
    //资源加载策略,提供从文件路径、类路径等加载资源的实现
    this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader());
    //加载配置的策略,提供从资源加载为配置的实现
    this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, getClass().getClassLoader());
}

资源加载器是DefaultResourceLoader,可以从文件路径、类路径、URL加载资源。
配置加载器默认有两种:

  • PropertiesPropertySourceLoader
    从.properties\xml加载配置
  • YamlPropertySourceLoader
    从.yml.yaml加载配置

加载.load()方法如下,为了方便,标记为load0:

 

public void load0() {
    this.profiles = new LinkedList<>();//所有的profiles
    this.processedProfiles = new LinkedList<>();//已经处理好了的profiles
    this.activatedProfiles = false;//当前处理的profile是不是active的
    this.loaded = new LinkedHashMap<>();//已经加载了的profiles
    initializeProfiles();//初始化
    //到这一步,profiles会有两个值,null和default
    while (!this.profiles.isEmpty()) {
        Profile profile = this.profiles.poll();
        //为了方便,标记喂load1
        load1(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
        this.processedProfiles.add(profile);
    }
    load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
    //将所有this.loaded的配置加载到Environment中
    addLoadedPropertySources();
}

初始化方法:初始化了一些状态、default profile:

 

private void initializeProfiles() {
    // null代表profile为default,并作为最低优先级的profile
    this.profiles.add(null);
    // 获取spring.profiles.active/spring.profiles.include标识的profile
    Set<Profile> activatedViaProperty = getProfilesActivatedViaActiveProfileProperty();
    //通过this.environment.getActiveProfiles()添加已经被标注为激活的profile
    processOtherActiveProfiles(activatedViaProperty);
    //如果activatedViaProperty不为空,则调用this.environment.addActiveProfile(profile)添加到environment中
    addActiveProfiles(activatedViaProperty);
    if (this.profiles.size() == 1) { // 只有一个默认值null,defaultProfileName为default
        for (String defaultProfileName : this.environment.getDefaultProfiles()) {
            ConfigFileApplicationListener.Profile defaultProfile = new ConfigFileApplicationListener.Profile(defaultProfileName, true);
            this.profiles.add(defaultProfile);//添加default profile
        }
    }
}

addToLoaded:当配置文件转换喂Document后,进行合并及添加到Environment中

 

//第一个参数时添加PropertySource的方法,用的是MutablePropertySources::addLast, 第二个参数是false
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());
        addMethod.accept(merged, document.getPropertySource());
    };
}

load使用多态,有不同的实现,为了方便,用序号标记。
load1:遍历所有配置文件的文件夹路径,读取配置。

 

private void load1(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
    //getSearchLocations()一开始就分析过,就是从自定义的路径里读取,没有自定义的就从默认路径读取,会用SpEL进行解析
    getSearchLocations().forEach((location) -> {
        boolean isFolder = location.endsWith("/");
        //非文件夹就是一个空集合
        //getSearchNames()是配置文件名(不包含后缀),如果spring.config.name有定义则取其值,否则默认值为application,会用SpEL进行解析
        Set<String> names = (isFolder ? getSearchNames() : NO_SEARCH_NAMES);
        names.forEach(
                (name) -> load(location, name, profile, filterFactory, consumer));
    });
}

load2:解析某个文件夹下,指定文件名(不包含文件类型)的配置文件。

 

/**
 * location: 文件夹路径
 * name:文件名
 * profile:所属的profile
 */
private void load2(String location, String name, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
    if (!StringUtils.hasText(name)) {//解析.properties\.yml这样的隐藏文件
        for (PropertySourceLoader loader : this.propertySourceLoaders) {
            if (canLoadFileExtension(loader, location)) {
                load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
            }
        }
    }
    //propertySourceLoaders是properties\yml配置解析器
    for (PropertySourceLoader loader : this.propertySourceLoaders) {
        for (String fileExtension : loader.getFileExtensions()) {
            String prefix = location + name;
            fileExtension = "." + fileExtension;
            loadForFileExtension(loader, prefix, fileExtension, profile, filterFactory, consumer);
        }
    

loadForFileExtension:加载指定配置文件

 

/**
 * loader:配置解析器
 * prefix:文件夹+文件名
 * fileExtension:文件类型
 */
private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
    //DocumentFilter就是判断文件是否符合profile
    DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
    DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
    if (profile != null) {
        // Try profile-specific file & profile section in profile file (gh-340)
        String profileSpecificFile = prefix + "-" + profile + fileExtension;
        load(loader, profileSpecificFile, profile, defaultFilter, consumer);
        load(loader, profileSpecificFile, profile, profileFilter, consumer);
        // Try profile specific sections in files we've already processed
        for (Profile processedProfile : this.processedProfiles) {
            if (processedProfile != null) {
                String previouslyLoaded = prefix + "-" + processedProfile
                        + fileExtension;
                load(loader, previouslyLoaded, profile, profileFilter, consumer);
            }
        }
    }
    load3(loader, prefix + fileExtension, profile, profileFilter, consumer);
}

load3:加载指定配置文件

 

//location配置文件完整的路径,包含文件类型
private void load3(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter, DocumentConsumer consumer) {
    try {
        Resource resource = this.resourceLoader.getResource(location);
        String description = getDescription(location, resource);//打印日志用的
        if (profile != null) {
            description = description + " for profile " + profile;
        }
        if (resource == null || !resource.exists()) {//资源不存在
            this.logger.trace("Skipped missing config " + description);
            return;
        }
        if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {//类型为空
            this.logger.trace("Skipped empty config extension " + description);
            return;
        }
        String name = "applicationConfig: [" + location + "]";
        //使用加载器加载,并转换为内部的结构类型
        /*这里实现比较复杂,有兴趣可以自己去调源码,在加载了配置文件后,
         *会去找是否存在spring.profiles\spring.profiles.include的配置,
         *如果有则会把profile加入到Document的activeProfiles\includeProfiles中
         */
        List<Document> documents = loadDocuments(loader, name, resource);
        if (CollectionUtils.isEmpty(documents)) {
            this.logger.trace("Skipped unloaded config " + description);
            return;
        }
        List<Document> loaded = new ArrayList<>();
        for (Document document : documents) {
            if (filter.match(document)) {
                //如果存在activeProfiles\includeProfiles,则this.profiles中,后续继续处理
                addActiveProfiles(document.getActiveProfiles());
                addProfiles(document.getIncludeProfiles());
                loaded.add(document);
            }
        }
        Collections.reverse(loaded);
        if (!loaded.isEmpty()) {
            //添加到Environment中
            loaded.forEach((document) -> consumer.accept(profile, document));
            this.logger.debug("Loaded config file " + description);
        }
    }
    catch (Exception ex) {
        throw new IllegalStateException("Failed to load property "
                + "source from location '" + location + "'", ex);
    }
}



作者:看不见的BUG
链接:https://www.jianshu.com/p/f31af43c29c6
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值