SpringBoot源码分析之环境和配置文件的加载


SpringBoot把配置文件的加载封装成了PropertySourceLoader接口,该接口的定义如下:
public interface PropertySourceLoader {
     // 支持的文件后缀
     String[] getFileExtensions();
     // 把资源Resource加载成属性源PropertySource
     PropertySource load(String name, Resource resource, String profile)
     throws IOException;
}


 
PropertySource是Spring对name/value键值对的封装接口。该定义了getSource()方法,这个方法会返回得到属性源的源头。比如MapPropertySource的源头就是一个Map,PropertiesPropertySource的源头就是一个Properties。

PropertySource目前的实现类有不少,比如上面提到的MapPropertySource和PropertiesPropertySource,还有RandomValuePropertySource(source是Random)、SimpleCommandLinePropertySource(source是CommandLineArgs,命令行参数)、ServletConfigPropertySource(source是ServletConfig)等等。

PropertySourceLoader接口目前有两个实现类:PropertiesPropertySourceLoader和YamlPropertySourceLoader。

PropertiesPropertySourceLoader支持从xml或properties格式的文件中加载数据。

YamlPropertySourceLoader支持从yml或者yaml格式的文件中加载数据。

Environment的构造以及PropertySource的生成

Environment接口是Spring对当前程序运行期间的环境的封装。主要提供了两大功能:profile和property(父接口PropertyResolver提供)。目前主要有StandardEnvironment、StandardServletEnvironment和MockEnvironment3种实现,分别代表普通程序、Web程序以及测试程序的环境。

下面这段代码就是SpringBoot的run方法内调用的,它会在Spring容器构造之前调用,创建环境信息:
// SpringApplication.class
private ConfigurableEnvironment prepareEnvironment(
      SpringApplicationRunListeners listeners,
      ApplicationArguments applicationArguments) {
  // Create and configure the environment
  // 如果是web环境,创建StandardServletEnvironment
  // 否则,创建StandardEnvironment
  // StandardServletEnvironment继承自StandardEnvironment,StandardEnvironment继承AbstractEnvironment
  // AbstractEnvironment内部有个MutablePropertySources类型的propertySources属性,用于存储多个属性源PropertySource
  // AbstractEnvironment构造函数有一个模板方法customizePropertySources(this.propertySources)
  //StandardServletEnvironment调用customizePropertySources先生成两个StubPropertySource,servletConfigInitParams和servletContextInitParams
  // StandardEnvironment会默认加上2个PropertySource。分别是MapPropertySource(调用System.getProperties()配置)和SystemEnvironmentPropertySource(调用System.getenv()配置)   
  /*@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);
  }
  */

  ConfigurableEnvironment environment = getOrCreateEnvironment();
  // 如果设置了一些启动参数args,添加基于args的SimpleCommandLinePropertySource
  // 还会配置profile信息,比如设置了spring.profiles.active启动参数,设置到环境信息中
  configureEnvironment(environment, applicationArguments.getSourceArgs());

  // 触发ApplicationEnvironmentPreparedEvent事件
  listeners.environmentPrepared(environment);
  if (!this.webEnvironment) {
      environment = new EnvironmentConverter(getClassLoader())
            .convertToStandardEnvironmentIfNecessary(environment);
  }
  return environment;
}



 
在 SpringBoot源码分析之SpringBoot的启动过程这篇文章中,我们分析过SpringApplication启动的时候会使用 工厂加载机制初始化一些初始化器和监听器。其中org.springframework.boot.context.config.ConfigFileApplicationListener这个监听器会被加载:
// spring-boot-version.release/META-INF/spring.factories
org.springframework.context.ApplicationListener=\
...
org.springframework.boot.context.config.ConfigFileApplicationListener,\
...
ConfigFileApplicationListener会监听SpringApplication启动的时候发生的事件,它的监听代码:

@Override
public void onApplicationEvent(ApplicationEvent event) {
     // 应用环境信息准备好的时候对应的事件。此时Spring容器尚未创建,但是环境已经创建
     if (event instanceof ApplicationEnvironmentPreparedEvent) {
       onApplicationEnvironmentPreparedEvent(
               (ApplicationEnvironmentPreparedEvent) event);
     }
     // Spring容器创建完成并在refresh方法调用之前对应的事件
     if (event instanceof ApplicationPreparedEvent) {
     onApplicationPreparedEvent(event);
     }
}

private void onApplicationEnvironmentPreparedEvent(
     ApplicationEnvironmentPreparedEvent event) {
     // 使用工厂加载机制读取key为org.springframework.boot.env.EnvironmentPostProcessor的实现类
     List postProcessors = loadPostProcessors();
     // 加上自己。ConfigFileApplicationListener也是一个EnvironmentPostProcessor接口的实现类
     postProcessors.add(this);
     // 排序
     AnnotationAwareOrderComparator.sort(postProcessors);
     // 遍历这些EnvironmentPostProcessor,并调用postProcessEnvironment方法
     for (EnvironmentPostProcessor postProcessor : postProcessors) {
          postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
     }
}

ConfigFileApplicationListener也是一个EnvironmentPostProcessor接口的实现类,在这里会被调用:

// ConfigFileApplicationListener的postProcessEnvironment方法
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,SpringApplication application) {
     // 添加属性源到环境中
     addPropertySources(environment, application.getResourceLoader());
     // 配置需要ignore的beaninfo
     configureIgnoreBeanInfo(environment);
     // 从环境中绑定一些参数到SpringApplication中
     bindToSpringApplication(environment, application);
}

protected void addPropertySources(ConfigurableEnvironment environment,
               ResourceLoader resourceLoader) {
     // 添加一个RandomValuePropertySource到环境中
     // RandomValuePropertySource是一个用于处理随机数的PropertySource,内部存储一个Random类的实例
     RandomValuePropertySource.addToEnvironment(environment);
     // 构造一个内部类Loader,并调用它的load方法,将application.properties和带profile的properties放入environment的propertySources
     new Loader(environment, resourceLoader).load();
}

Loader是一个内部类,主要作用是委托加载属性文件加载动作主要由load方法来完成

private class Loader {

  private final Log logger = ConfigFileApplicationListener.this.logger;

  private final ConfigurableEnvironment environment;

  private final ResourceLoader resourceLoader;

  private PropertySourcesLoader propertiesLoader;

  private Queue profiles;

  private List processedProfiles;

  private boolean activatedProfiles;

  Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
      this.environment = environment;
      this.resourceLoader = resourceLoader == null ? new DefaultResourceLoader()
            : resourceLoader;
  }

  public void load() {
      1.创建PropertySourcesLoader
      this.propertiesLoader = new PropertySourcesLoader();
      this.activatedProfiles = false;
      this.profiles = Collections.asLifoQueue(new LinkedList());
      this.processedProfiles = new LinkedList();

      // Pre-existing active profiles set via Environment.setActiveProfiles()
      // are additional profiles and config files are allowed to add more if
      // they want to, so don't call addActiveProfiles() here.
     
      2.获取环境信息中激活的profile
      Set initialActiveProfiles = initializeActiveProfiles();
      this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
      //如果没设置profile,默认使用default这个profile,并添加到profiles队列中
      if (this.profiles.isEmpty()) {
        for (String defaultProfileName : this.environment.getDefaultProfiles()) {
            Profile defaultProfile = new Profile(defaultProfileName, true);
            if (!this.profiles.contains(defaultProfile)) {
              this.profiles.add(defaultProfile);
            }
        }
      }

      // The default profile for these purposes is represented as null. We add it
      // last so that it is first out of the queue (active profiles will then
      // override any settings in the defaults when the list is reversed later).
     
     //最后会添加一个null到profiles队列中(为了获取没有指定profile的配置文件。比如环境中有application.yml和appliation-dev.yml,这个null就保证优先加载application.yml文件)
      this.profiles.add(null);
     //3.循环处理profiles,查找文件位置然后去加载文件
      while (!this.profiles.isEmpty()) {
        Profile profile = this.profiles.poll();
          //位置查找方法
        for (String location : getSearchLocations()) {
            if (!location.endsWith("/")) {
              // location is a filename already, so don't search for more
              // filenames
               //4.找出的属性源文件被加载
              load(location, null, profile);
            }
            else {
              for (String name : getSearchNames()) {
                  load(location, name, profile);
              }
            }
        }
        this.processedProfiles.add(profile);
      }
     //5.增加ConfigurationProperties
      addConfigurationProperties(this.propertiesLoader.getPropertySources());
  }


private Set getSearchLocations() {
  Set locations = new LinkedHashSet();
  // User-configured settings take precedence, so we do them first
  if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
      for (String path : asResolvedSet(
            this.environment.getProperty(CONFIG_LOCATION_PROPERTY), null)) {
        if (!path.contains("$")) {
            path = StringUtils.cleanPath(path);
            if (!ResourceUtils.isUrl(path)) {
              path = ResourceUtils.FILE_URL_PREFIX + path;
            }
        }
        locations.add(path);
      }
  }
  locations.addAll(
        asResolvedSet(ConfigFileApplicationListener.this.searchLocations,
              DEFAULT_SEARCH_LOCATIONS));//默认路径
  return locations;
}


由方法可以看出,如果我们配置了spring.config.location属性,系统会获取配置的属性文件,从而加载自定义路径的配置文件,默认的加载路径有 
load方法做了一下工作:
  1. 创建PropertySourcesLoader。PropertySourcesLoader内部有2个属性,分别是PropertySourceLoader集合和MutablePropertySources(内部有PropertySource的集合)。最终加载完毕之后MutablePropertySources属性中的PropertySource会被添加到环境Environment中的属性源列表中。PropertySourcesLoader被构造的时候会使用工厂加载机制获得PropertySourceLoader集合(默认就2个:PropertiesPropertySourceLoader和YamlPropertySourceLoader;可以自己扩展),然后设置到属性中
  2. 获取环境信息中激活的profile(启动项目时设置的spring.profiles.active参数)。如果没设置profile,默认使用default这个profile,并添加到profiles队列中。最后会添加一个null到profiles队列中(为了获取没有指定profile的配置文件。比如环境中有application.yml和appliation-dev.yml,这个null就保证优先加载application.yml文件)
  3. profiles队列取出profile数据,使用PropertySourcesLoader内部的各个PropertySourceLoader支持的后缀去目录(默认识别4种目录classpath:/[类加载目录],classpath:/config/[类加载目录下的config目录],file:./[当前目录],file:./config/[当前目录下的config目录])查找application文件名(这4个目录是默认的,可以通过启动参数spring.config.location添加新的目录,文件名可以通过启动参数spring.config.name修改)。比如目录是file:/,文件名是application,后缀为properties,那么就会查找file:/application.properties文件,如果找到,执行第4步
  4. 找出的属性源文件被加载,然后添加到PropertySourcesLoader内部的PropertySourceLoader集合中。如果该属性源文件中存在spring.profiles.active配置,识别出来并加入第2步中的profiles队列,然后重复第3步
  5. 第4步找到的属性源从PropertySourcesLoader中全部添加到环境信息Environment中。如果这些属性源存在defaultProperties配置,那么会添加到Environment中的属性源集合头部,否则添加到尾部

比如项目中classpath下存在application.yml文件和application-dev.yml,application.yml文件的内容如下:

spring.profiles.active: dev

直接启动项目,开始解析,过程如下:

  1. 从环境信息中找出是否设置profile,发现没有设置。 添加默认的profile - default,然后添加到队列里,最后添加null的profile。此时profiles队列中有2个元素:default和null
  2. profiles队列中先拿出null的profile。然后遍历4个目录和2个PropertySourceLoader中的4个后缀(PropertiesPropertySourceLoader的properties和xml以及YamlPropertySourceLoader的yml和yaml)的application文件名。file:./config/application.properties、file:./application.properties、classpath:/config/application.properties、classpath:/application.properties、file:./config/application.xml; file:./application.xml ….
  3. 找到classpath:/application.yml文件,解析成PropertySource并添加到PropertySourcesLoader里的MutablePropertySources中。由于该文件存在spring.profiles.active配置,把dev添加到profiles队列中
  4. profiles队列拿出dev这个profile。由于存在profile,寻找文件的时候会带上profile,重复第3步,比如classpath:/application-dev.yml…
  5. 找到classpath:/application-dev.yml文件,解析成PropertySource并添加到PropertySourcesLoader里的MutablePropertySources中
  6. profiles队列拿出default这个profile。寻找文件发现没有找到。结束

这里需要注意一下一些常用的额外参数的问题,整理如下:

  1. 如果启动程序的时候设置了系统参数spring.profiles.active,那么这个参数会被设置到环境信息中(由于设置了系统参数,在StandardEnvironment的钩子方法customizePropertySources中被封装成MapPropertySource并添加到Environment中)。这样PropertySourcesLoader加载的时候不会加上default这个默认profile,但是还是会读取profile为null的配置信息。spring.profiles.active支持多个profile,比如java -Dspring.profiles.active=”dev,custom” -jar yourjar.jar
  2. 如果设置程序参数spring.config.location,那么查找目录的时候会多出设置的目录,也支持多个目录的设置。这些会在SpringApplication里的configureEnvironment方法中被封装成SimpleCommandLinePropertySource并添加到Environment中。比如java -jar yourjar.jar –spring.config.location=classpath:/custom,file:./custom 1 2 3。有4个参数会被设置到SimpleCommandLinePropertySource中。解析文件的时候会多出2个目录,分别是classpath:/custom和file:./custom
  3. 如果设置程序参数spring.config.name,那么查找的文件名就是这个参数值。原理跟spring.config.location一样,都封装到了SimpleCommandLinePropertySource中。比如java -jar yourjar.jar –spring.config.name=myfile。 这样会去查找myfile文件,而不是默认的application文件
  4. 如果设置程序参数spring.profiles.active。注意这是程序参数,不是系统参数。比如java -jar yourjar.jar –spring.profiles.active=prod。会去解析prod这个profile(不论是系统参数还是程序参数,都会被封装成多个PropertySource存在于环境信息中。最终获取profile的时候会去环境信息中拿,且都可以拿到)
  5. 上面说的每个profile都是在不同文件里的。不同profile也可以存在在一个文件里。因为有profile会去加载带profile的文件的同时也会去加载不带profile的文件,并解析出这个文件中spring.profiles对应的值是profile的数据。比如profile为prod,会去查找application-prod.yml文件,也会去查找application.yml文件,其中application.yml文件只会查找spring.profiles为prod的数据

比如第6点中profile.yml的数据如下:

spring:
    profiles: prod
my.name: 1

---

spring:
    profiles: dev
my.name: 2

这里会解析出spring.profiles为prod的数据,也就是my.name为1的数据。

优先级的问题:由于环境信息Environment中保存的PropertySource是MutablePropertySources,那么会去配置值的时候就存在优先级的问题。比如PropertySource1和PropertySource2都存在custom.name配置,那么会从哪个PropertySource中获取这个custom.name配置呢?它会遍历内部的PropertySource列表,越在前面的PropertySource,越先获取;比如PropertySource1在PropertySource2前面,那么会先获取PropertySource1的配置。MutablePropertySources内部添加PropertySource的时候可以选择元素的位置,可以addFirst,也可以addLast,也可以自定义位置。


参考: https://blog.csdn.net/jamet/article/details/78042486
      http://fangjian0423.github.io/2017/06/10/springboot-environment-analysis/ 写的也可以
     https://my.oschina.net/xiaoqiyiye/blog/1624285 ConfigFileApplicationListener分析  写的很好

你可能感兴趣的:(SpringBoot)