SpringBoot(2) 环境

767 阅读7分钟

一个项目有不同的环境,比如开发测试环境、预发布环境、生产环境等,不同环境对应不同的配置,在SpringBoot中最直观的区别就是配置文件。当各监听器执行完应用启动事件之后(ApplicationStartingEvent),后面一步就是确定环境了。

初始化环境

SpringApplication->run():
    // 构建环境
    ConfigurableEnvironment environment = prepareEnvironment(listeners,
            applicationArguments);
SpringApplication->prepareEnvironment():
    // 构建标准Servlet环境
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    listeners.environmentPrepared(environment);
SpringApplication->configureEnvironment():
    configurePropertySources(environment, args); // --1
    configureProfiles(environment, args); // --2
SpringApplication->configurePropertySources(): // --1
    // 如果有设置成员变量defaultProperties属性值,将其作为属性源添加到末尾
    if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
        ......
    }
    // 如果有命令行参数配置,将其作为属性源添加到头部
    if (this.addCommandLineProperties && args.length > 0) {
        ......
    }
SpringApplication->configureProfiles(): // --2
    // 如果有设置成员变量additionalProfiles属性值,将其添加上
    Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
    // 如果环境变量和系统变量中有设置spring.profiles.active,将其值添加上
    profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
    // 设置为环境的activeProfiles属性
    environment.setActiveProfiles(StringUtils.toStringArray(profiles));

本文所讲的环境即为StandardServletEnvironment。在实例化类的时候,会给予它默认的四个属性源,其中第三个和第四个分别是环境变量和系统环境变量,会给成员变量defaultProfiles添加默认配置值default

image.png
图一

假设在没有其它属性源的情况下,获得属性是根据properySourceList的先后顺序来的,并且只要有值就返回,也就是说环境变量中的配置比系统环境变量中的配置优先级更高。

配置文件的加载

创建完环境后,触发监听器调用应用环境准备完毕事件(ApplicationEnvironmentPreparedEvent),重点关注ConfigFileApplicationListener这个监听器。

ConfigFileApplicationListener->addPropertySources():
    // 又给属性源集合添加RandomValuePropertySource属性源
    RandomValuePropertySource.addToEnvironment(environment);
    // 实例化Loader的时候,将会添加下图两个属性源加载类
    new Loader(environment, resourceLoader).load();

image.png
图二

ConfigFileApplicationListener->Loader->load():
    this.profiles = new LinkedList<>();
    // 已经解析过的配置文件
    this.processedProfiles = new LinkedList<>();
    this.activatedProfiles = false;
    this.loaded = new LinkedHashMap<>(); 
    // 初始化,此处假设profiles一个是null,一个是default
    initializeProfiles();
    while (!this.profiles.isEmpty()) {
        Profile profile = this.profiles.poll();
        if (profile != null && !profile.isDefaultProfile()) {
            // 如果不是default,就添加进环境的activeProfiles中
            addProfileToEnvironment(profile.getName());
        }
        // --3
        load(profile, this::getPositiveProfileFilter,
                // --4
                addToLoaded(MutablePropertySources::addLast, false));
        this.processedProfiles.add(profile);
    }
    resetEnvironmentProfiles(this.processedProfiles);
    load(null, this::getNegativeProfileFilter,
            addToLoaded(MutablePropertySources::addFirst, true));
    // 将解析到的属性源放到环境的propertySourceList最后
    // 添加的时候会将集合倒序
    addLoadedPropertySources();
ConfigFileApplicationListener->Loader->load(): // --3
    getSearchLocations().forEach((location) -> {
        boolean isFolder = location.endsWith("/");
        // names默认是application
        Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
        names.forEach(
            (name) -> load(location, name, profile, filterFactory, consumer));
    });
ConfigFileApplicationListener->Loader->load():
    Set<String> processed = new HashSet<>();
    for (PropertySourceLoader loader : this.propertySourceLoaders) {
        // properties的扩展名是properties, xml;yaml的扩展名是yml,yaml
        for (String fileExtension : loader.getFileExtensions()) {
            if (processed.add(fileExtension)) {
                loadForFileExtension(loader, location + name, "." + fileExtension,
                        profile, filterFactory, consumer);
            }
        }
    }
ConfigFileApplicationListener->Loader->loadForFileExtension():
    if (profile != null) {
        // 加上扩展名再读一次,类似application-file.yml这种
        ......
    }
    // 将文件位置+文件名+文件扩展名拼接形成最终的location
    load(loader, prefix + fileExtension, profile, profileFilter, consumer);

getSearchLocations()为要查找的文件位置,如下图所示,资源查找时,file:./开头表示当前项目路径下的文件,classpath:/表示当前项目下target/classes路径下的文件

image.png
图三

ConfigFileApplicationListener->Loader->load():
    Resource resource = this.resourceLoader.getResource(location);
    // 如果resource为空或者resource不存在,说明文件不存在,直接返回
    ......
    // 读取resource文件解析属性值,得到一个属性源
    // 然后将文档中的spring.profiles.active给Document的activeProfiles成员变量赋值、spring.profiles.include给includeProfiles成员变量赋值,将属性源给propertySource成员变量赋值
    // 最终返回单一的list集合
    List<Document> documents = loadDocuments(loader, name, resource);
    ......
    for (Document document : documents) {
        if (filter.match(document)) {
            // --5 给profiles添加activeProfiles 
            addActiveProfiles(document.getActiveProfiles()); 
            // 将include的放在profiles最前面
            addIncludedProfiles(document.getIncludeProfiles());
            loaded.add(document);
        }
    }
    if (!loaded.isEmpty()) {
        // --4处的loader添加,key为当前profile,value为包含该属性源的多属性源类对象
        loaded.forEach((document) -> consumer.accept(profile, document));
    }
ConfigFileApplicationListener->Loader->addActiveProfiles(): // --5
    // 没有则直接返回
    if (profiles.isEmpty()) {
        return;
    }
    // 第一次进来,activatedProfiles为false
    if (this.activatedProfiles) {
        return;
    }
    this.profiles.addAll(profiles);
    this.activatedProfiles = true;
    // 移除default
    removeUnprocessedDefaultProfiles();

spring.profiles.activespring.profiles.include的区别是,1. 前者只能添加一次,后者可以添加多次;2. 同属性文件下,先解析include,再解析active

image.png
图四

image.png
图五

假如环境配置如图一所示,并且配置文件application-file存在,那么环境变量最终如图五所示

总结

  1. 初始化时,加上环境变量和系统变量作为属性源,defaultProfiles属性添加default
  2. 启动前有设置defaultProperties,将默认配置加到属性源最后,有设置命令行参数,将命令行参数作为属性源加到最前
  3. 找到属性源中的spring.profiles.active,添加进环境的activeProfiles变量中
  4. 启动前有设置additionalProfiles,添加进环境的activeProfiles变量中并放在最前面
  5. 监听器执行ApplicationEnvironmentPreparedEvent事件,其中ConfigFileApplicationListener监听器:
    1. 添加随机值属性源
    2. 监听器的profiles添加null,找属性源中的spring.profiles.activespring.profiles.include
      1. 如果有的话,activatedProfiles设置成true,添加进监听器的profiles中
      2. 如果没有的话,profiles添加default
    3. profiles出栈,根据图三的文件位置顺序查找项目中是否有application.properties,application.xml,application.yml,application.yaml
      1. 文件存在的话,解析文件,查看是否有设置spring.profiles.activespring.profiles.include,处理完之后,将文件源属性添加进loaded变量中
        • 对于active的处理,如果activatedProfiles为true则跳过,否则添加进监听器的profiles中,删除default
        • 对于include的处理,添加到监听器profiles最前面
    4. profiles出栈,添加进环境的activeProfiles中,查找application-xxx.properties,application-xxx.xml,application-xxx.yml,application-xxx.yaml,重复步骤3.1,直至所有profiles出栈
    5. 将解析出来的文件属性源倒序排序,属性源没有defaultProperties则添加进环境的属性源集合末尾,否则插入到defaultProperties(确保默认属性在最后)之前
  6. 其它监听器的操作
  7. 属性源集合首位添加ConfigurationPropertySourcesPropertySource(代码见SpringApplication->prepareEnvironment():ConfigurationPropertySources.attach(environment)),将之前的所有属性源放入作为缓存,返回SpringBoot的run方法

举例:

  1. 属性源properySourceList=[环境变量,系统变量]
    1. 假设profiles为[null,default],null解析出active=[dev],include=[mail,message],此时profiles=[mail,message,dev],loaded=[application.yml]
    2. 假设spring.profiles.active=dev,profiles为[null,dev],null解析出active=[release],include=[mail,message],此时profiles=[mail,message,dev],loaded=[application.yml]
  2. profiles=[message,dev],application-mail不存在
  3. profiles=[dev],application-message存在,此时loaded=[application.yml,application-message.yml]
  4. profiles=[],dev解析出active=[release],include=[db],此时profiles=[db],loaded=[application.yml,application-message.yml,application-dev.yml]
  5. profiles=[],application-db存在,此时loaded=[application.yml,application-message.yml,application-dev.yml,application-db.yml]
  6. 最终环境变量的activeProfiles=[mail,message,dev,db],属性源properySourceList=[环境变量,系统变量,application-db.yml,application-dev.yml,application-message.yml,application.yml,defaultProperties]

application.yml是公共配置文件,位于末尾,这也正解释了相同属性在不同文件中,环境配置文件优先的原因了

应用

环境准备完毕后,后面的很多功能到最后都是使用其成员变量或调用其方法,典型的有:

  1. IoC注入environment,调用getProperty()方法
  2. IoC中@Value给字段赋值,AutowiredAnnotationBeanPostProcessor后置处理器
  3. 注解@ConfigurationProperties(prefix = "xxx")给Bean的成员变量赋值,CommonAnnotationBeanPostProcessor后置处理器,大致过程点击此处

附录1:命令行参数

还记得写的第一个Java程序:

image.png
图六

用命令javac Hello.java,然后再java Hello就可以输出了,java Hello是最简单的启动方式,如果复杂点,可以带上参数,如java -Dlang=java Test hello world,这里的-D就是环境变量,可以用System.getProperty(lang)得到,hello world就是命令行参数,此时args[0]=helloargs[1]=world,当然也可以在idea中设置。

image.png
图七 这里的VM options就是环境变量,Program arguments就是命令行参数

附录2:yml解析文件与properties解析文件

yml解析文件

image.png
图八

image.png
图九

以图八的配置为例,可以看到很明显的层级关系,解析yml的本质是一个递归的过程,层级的递进是父节点与子节点的关系。

我们可以把cache-enabled: falseexecutor-type: reuse当作是一个Pair结构,因此它们的key和value属性都是ScalarNodeconfiguration的value是一个Pair,因此它的value属性是MappingNodemybatis-plus在第一层,所以它的key属性是一个KeyScalarNode

image.png
图十

image.png
图十一

图九的层级在图十用Map结构表示了出来,它是与原来的配置文件最相似的。

深层的key用外层的name补充,最终被转换成图十一的单一key-value的Map集合,使用yml好处是不用重复写key,但是要注意yml书写规范。yml解析配置文件的源代码位于YamlPropertySourceLoader->load()

properties解析文件

properties解析文件就太简单了,大致为一个字符一个字符读,遇到等于号就认为等于号前面的是key,然后跳过等于号,读到换行符之前就认为前面是value,赋值进map,直到文件读完。properties解析配置文件的源代码位于PropertiesPropertySourceLoader->load()

image.png
图十二

除了单一value,解析过程中还有变量替换,集合解析等,但无论是yml,还是properties,虽然处理过程不同,最终结果都是一样的