您的当前位置:首页正文

SpringBoot源码学习之Environment环境变量和配置文件读取

2024-11-30 来源:个人技术集锦

1.目录&前言

前言:

SpringBoot项目的启动流程中,Environment变量的设置是在Spring容器中Bean定义对象的读取、Bean对象创建以前完成的,因此了解其生命周期对于理解整个Spring框架都大有益处。

本文篇幅稍长,字数超18000+,码字不易,尤其是原创技术类文章,各位如果觉得还行请点赞、收藏。

2.准备工作

SpringBoot中,解读Environment变量的读取需要引入至少两个JAR包:spring-boot以及spring-core,不过这里我们引入spring-boot-starter即可, 因为它包含了这两个JAR包。

注意这里是引入小于2.4的版本,因为在2.4+的版本,对配置文件的读取做了升级,并且个人觉得新版本的实现逻辑比旧版本的要更难阅读理解,但本文主要是为了理解SpringBoot中的一些基本大致原理,因此在这里以较为简单的版本进行解读。

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>2.3.1.RELEASE</version>
  </dependency>
</dependencies>

3.源码解读

源码内容较多,本章节将挑选重点、关键点源码进行解读,把这些关键点串联起来后,可以理解其源码大概的设计思路。

本章节会详细介绍Enviroment变量的完整的生命周期和特点、以及其如归并配置文件数据时用到的一些设计模式等。

SpringBoot框架中,是通过运行Main函数中的SpringApplication.run(primarySource.class, args)代码行启动项目的,其中primarySource是启动类的class对象。

SpringApplication#run(Class<?> primarySource, String... args)方法里面实际上是通过创建一个具体的SpringApplication的实例对象,后续再调用run(String... args)去完成SpringBoot框架的一个搭建工作。

Enviroment变量的创建正是在run(String... args)进行的,不过在这里有一个个人觉得比较巧妙的地方,那就是在SpringApplication的构造函数里面,设置了会在后面Enviroment变量创建过程中用到的监听器对象——ApplicationListener。

因此本章节将主要介绍SpringApplicationEnviroment变量的创建过程,以及其中值得深究的一些东西,如设计模式等。

3.1  SpringApplication

SpringBoot程序的启动直接依赖于SpringApplication对象,那么让我们看看它的创建特点是什么?

在下面的伪代码可以看到,其构造函数中入参有一个Class对象,代表的是启动类的class对象,接着就是设置标记其的primarySourcesmainApplicationClass等属性,这么做的原因其中之一是为了后面以启动类为入口 ,做等工作。

但本章节重点是setListeners(getSpringFactoriesInstances(ApppliplicationListener.class))这一行代码,因为会通过这引入ApplicationListener监听器对象,这有什么用呢?

其中一个用处就是引入配置文件的工作需要的监听器对象——ConfigFileApplicationListener

public class SpringApplication {
	public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
        // 配置文件的读取关键在这里,读取所有META/spring.factories文件下的
        // ApplicationListener的候选类并生成对象
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		this.mainApplicationClass = deduceMainApplicationClass();
	}
}

3.1.1  设置ApppliplicationListener

SpringApplication构造函数中,可以看到有下面这么一行代码,就是设置监听器,那么此目的何在呢?

setInitializers((Collection)getSpringFactoriesInstances(ApplicationListener.class));

其实这些监听器对象是一种后面回调使用的,是一种监听器的设计模式。主要是为了比如说,有些资源的加载过程中需要被修饰、增强之类的,那么就提前准备好监听器对象,在程序认为合适的地方再调用这些监听器发挥其作用。

通过这一行代码会读取本项目下所有的JAR包中路径为META-INFspring.factories中的keyApplicationContextInitializer的全限定类名下的内容。

如下图所示,在引入的spring-bootJAR包下,有一个ConfigFileApplicationListener类,它就是后面将会负责读取项目中常见的如application.propertiesapplication.yaml等配置文件的数据并加载到Enviroment变量中。

本小节介绍只做“来龙”的工作,介绍了ConfigFileApplicationListener等监听器是如何引入的,而“去脉”的工作——读取配置文件的原理等将会在后面小节进行详细讲解。

3.2  run(String... args)

run(String... args)方法里面做了许多事情,其中一件事情是本文要讲解的的,创建Environment变量和读取配置文件并将其数据归并。

 如下伪代码所示,会创建一个实例是ConfigurableEnvironment的变量(后简称Environment变量),它是通过preparenEnvironment(...)方法完成的。

其实这个方法也是干了好几件事情,第一件事情就是创建一个具体的实例对象,而按照项目背景的不同所创建的实际变量对象也不一样。

接着就是跟上一个小节讲到的设置ApppliplicationListener监听器对象直接相关的,因为在这里将会应用到负责读取配置文件的监听器——ConfigFileApplicationListener。

不过,请注意看preparenEnvironment(...)的入参是SpringApplicationRunListeners而非从JAR包指定路径META-INF/spring.factories下的ApppliplicationListener对象。这里我们不需要过度探究,仅仅将SpringApplicationRunListeners视作是所有的ApppliplicationListener对象封装对象即可,通过它可以获取所有的监听器对象。

public class SpringApplication {
    public ConfigurableApplicationContext run(String... args) {
        ConfigurableApplicationContext context = null;
        ....
        SpringApplicationRunListeners listeners = getRunListeners(args);
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // 创建environment变量
		ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
        ....
    }
	private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// 创建一个ConfigurableEnvironment变量对象
		ConfigurableEnvironment environment = getOrCreateEnvironment();
        // 主要是创建一个转换后的命令行配置源&为environment设置激活的配置文件
		configureEnvironment(environment, applicationArguments.getSourceArgs());
        // 添加一个名叫configurationProperties的配置到environment配置源集合的第一位
		ConfigurationPropertySources.attach(environment);
        // 这便是ConfigFileApplicationListener等监听器派上用场的地方
		listeners.environmentPrepared(environment);
        ......
		return environment;
	}
}

3.2.1  创建Environment对象

prepareEnvironment(...)方法中,做到第一件事情就是创建一个Environment变量,让我们看看究竟是怎么被创建的?

创建Environment变量是在getOrCreateEnvironment()方法中完成的,其创建逻辑也很简单,就是根据当前SpringApplicationwebApplicationType的变量值去选出某个实例对象。

public class SpringApplication {
   	private ConfigurableEnvironment getOrCreateEnvironment() {
		if (this.environment != null) {
			return this.environment;
		}
		switch (this.webApplicationType) {
		case SERVLET:
			return new StandardServletEnvironment();
		case REACTIVE:
			return new StandardReactiveWebEnvironment();
		default: // 非web项目会创建这个
			return new StandardEnvironment();
		}
	}
}

那么这个webApplicationType又是如何确定的呢?

其实是SpringApplication构造函数中的this.mainApplicationClass=deduceMainApplicationClass()这一行代码确定的。

在下图可以看到,确定mainApplicationClass变量值的时候,会去判断当前项目下是否存在某些指定的class文件。

javax.servlet.Servletclass文件,如果没有它则SpringBoot认为这不是一个Web项目,因为Web项目需要引入Servlet容器,如tomcat服务器,肯定会使用到Servlet。

因此,秉着浅入深出的原则,本文的将会以StandardEnvironment为例子进行讲解。因为其他的两个变量对象StandardServletEnvironmentStandardReactiveWebEnvironment实际上都是继承了StandardEnvironment。

3.2.1.1  StandardEnvironment对象

StandardEnvironment,无论是从中文译名还是其类注释上都可以知道,它是一个标准的Environment变量,那么此标准为何标准呢?

在下面伪代码中可以看到,StandardEnvironment对象没有显示声明任何构造函数也没有任何成员变量,(JAVA知识复习:但是JVM默认会帮所有类都写一个无参构造函数(如果没有的情况下))。

但是它继承了父类AbstractEnvironment,从其父类上可以看到有一个MutablePropertySources propertySources的成员变量以及构造函数中会调用customizePropertySources方法。

也就是说,当实例化StandardEnvironment对象时,其所拥有的变量都来源于父类(JAVA知识复习:所有子类调用构造函数都会先调用父类的构造函数)。

这个MutablePropertySources的变量,它有一个集合属性负责装载来源不同的配置源数据,如JVM配置源、配置文件配置源等。集合中的元素就是PropertySource对象,有namesource两个属性,前者就是配置源的名称,后者就是承载实际配置数据的结构,通常是key-value的数据结构。

父类AbstractEnvironment构造函数中调用的方法customizePropertySources方法,实际会调用到子类StandardEnvironment重写的方法(JAVA知识复习:子类引用访问父类中的方法时,优先调用子类重写的方法,而非父类方法)

public class StandardEnvironment extends AbstractEnvironment {
  @Override
  protected void customizePropertySources(MutablePropertySources propertySources){
        // 往配置源集合对象添加JVM参数的配置源
		propertySources.addLast(
				new PropertiesPropertySource("systemProperties", 
                                                       getSystemProperties()));
        // 往配置源集合对象添加运行程序所在的本地环境变量
		propertySources.addLast(
				new SystemEnvironmentPropertySource("systemEnvironment",
                                                       getSystemEnvironment()));
  }
}
public abstract class AbstractEnvironment implements ConfigurableEnvironment {
   // 实际上可看作它是一个集合对象,集合中的元素是PropertySource对象,有name和source
   // 属性,source通常都是key-value形式的数据结构,name则是这个配置源的名称
   private final MutablePropertySources propertySources = new MutablePropertySources();
   public AbstractEnvironment() {
        // 这个是protected方法,具体由各个子类实现并调用
		customizePropertySources(this.propertySources);
	}
}

public class MutablePropertySources implements PropertySources {
	private final List<PropertySource<?>> propertySourceList = 
                                                         new CopyOnWriteArrayList<>();
}
public abstract class PropertySource<T> {
	protected final String name; // 配置源名称
	protected final T source;  // 存储配置数据的数据结构,一般是key-value的结构
}

那么在StandardEnvironment#customizePropertySources(...)方法中又做了什么操作呢?

从伪代码可看出逻辑简单,只是创建了两个名称分别为systemPropertiessystemEnvironmentPropertiesPropertySource配置源对象。

两配置源数据的获取底层是通过getSystemProperties()getSystemEnvironment()方法获取的,其实就是分别从JVM参数和程序所在的本地环境变量中获取的配置源数据,它们都是key-value的形式存在。

注意看它们是通过addLast方法添加的,即添加到MutablePropertySources.propertySourceList集合变量的最后面去。这个操作就决定了本地环境变量的优先级高于JVM参数,因为Environment变量的读取是将propertySourceList集合以顺序的方式遍历出所有配置源。

而本小节开始前的疑问,为什么StandardEnvironment是一个标准变量对象呢?标准之处正是在于它拥有JVM参数本地环境变量的配置源对象,这无论你的项目是不是WEB项目,这两个配置源都是标配。

Environment变量创建完成后,在本文来说,后面重要的事情就是读取配置文件了。

3.2.2  配置文件的读取

配置文件的读取和StandardEnvironment对象创建时添加的JVM参数本地环境变量配置源不同,相较于后者的获取是通过JAVA原生API去获取,前者则涉及资源的加载、数据的读取,比后者更稍微复杂。

前面介绍过,SpringBoot对于配置文件的读取是通过ConfigFileApplicationListener此监听器完成的,这是一种监听器的设计思想。

这么做的好处是什么呢?其实这跟一个对象的创建时机有关系,比如说这里的配置文件数据读取出来后肯定需要一个Enviroment变量接收,那么在什么时候创建合适呢?

答案从源码就可以找到,就是在应用此监听器之前。因为除了ConfigFileApplicationListener这个监听器有特定用途外,也有其他的监听器也需要用到Enviroment变量。

比如LoggingApplicationListener这个监听器,就是从Enviroment变量获取日志级别等配置,去设置程序日志的输出级别;又比如,如本人公司前不久全面下线阿波罗等远程配置中心,在短时间内如何兼容没有远程配置中心呢?这时候就可以自己定义一个监听器,自己实现远程数据的初始化接入等操作。

public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
   	private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
		try {
            // 所有ApplicationListener对象都会回调此方法
			listener.onApplicationEvent(event);
		}
    }
}

在调用ApplicationListener监听器时,SpringApplicationRunListeners会将Environment变量封装到一个ApplicationEnvironmentPreparedEvent的事件对象中,它继承了ApplicationEvent接口,所有实现了ApplicationListener接口的类都重写了onApplicationEvent(E event)方法,在这里可以完成各自的逻辑。

那么ConfigFileApplicationListener是怎么读取配置文件的?

这是ConfigFileApplicationListener重写的方法onApplicationEvent(E event),事件实际类型是ApplicationEnvironmentPreparedEvent,因此onApplicationEnvironmentPreparedEvent方法里面会完成读取配置文件的工作。

public class ConfigFileApplicationListener {
   	@Override
	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationEnvironmentPreparedEvent) {
            // 这里正是处理Enviroment变量对象的
			onApplicationEnvironmentPreparedEvent(
                                 (ApplicationEnvironmentPreparedEvent) event);
		}
		if (event instanceof ApplicationPreparedEvent) {
			onApplicationPreparedEvent(event);
		}
	}
}

这里我们直入主题,底层实现配置文件的读取工作是在ConfigFileApplicationListener的内部类Loader的load()方法完成的。

如下伪代码所示,对于初次运行的应用程序,实际上会进入以下的lamada函数中运行的,当这一段lamada函数执行完成,配置文件的配置源会被归并到Enviroment变量。

在这段lamada函数中,关键点有三处地方: initializeProfiles()方法、load(profile, ...)方法addLoadedPropertySources()方法。

这三处方法分工其实还挺明确:

initializeProfiles()负责寻找配置文件的作用域范围,如devsituatpro等作用域

load(profile, ...)在前面的基础上读取这些作用域的配置文件

addLoadedPropertySources()则是将读取到的配置文件数据添加到Environment对象中

3.2.2.1  initializeProfiles()方法

initializeProfiles()方法中,主要做了一件事情,就是找到当前SpringBoot应用程序所有需要用到的配置文件对应的作用域,即常见的开发环境使用dev结尾的文件、sit环境使用sit结尾的、uat使用uat结尾的、生产环境使用pro结尾的。

从以下伪代码中,最终目的是将Profile对象添加到Deque<Profile> profiles这个先进先出的优先队列集合中,目的是为了在lamada函数中遍历它获取每一个Profile对象去执行load(profile, ...)方法。

这个Profile文件就两个属性:namedefaultProfile,分别表示作用域的名称和是否是Spring默认的配置文件(当指定了作用域则是false)。后面对profiles集合的遍历就是传入这些知道了作用域的Profile文件。因为读取配置文件的时候,肯定是按照环境的不同找到不同作用域的文件。

从下伪代码看到,会往profiles集合添加一个null对象,为什么要添加一个null对象呢?这个null对象其实就是对应着默认(即无后缀)作用域的application.xml或者application.yaml【为什么要是application前缀呢?其他不行吗?后面将会解答】配置文件,Spring认为每个程序都应该有一个默认的配置文件。

public class ConfigFileApplicationListener implements EnvironmentPostProcessor, 
                                            SmartApplicationListener, Ordered {
  private class Loader {
     private void initializeProfiles() {
        // 这个null代表的是application配置文件,也就是说Spring项目必须有一个默认的配置文件
       	this.profiles.add(null);
		Binder binder = Binder.get(this.environment);
		Set<Profile> activatedViaProperty = getProfiles(binder, ACTIVE_PROFILES_PROPERTY);
		Set<Profile> includedViaProperty = getProfiles(binder, INCLUDE_PROFILES_PROPERTY);
		List<Profile> otherActiveProfiles = getOtherActiveProfiles(activatedViaProperty, 
                                                                    includedViaProperty);
        // 若之前在environment对象先指定了activite的profiiles,则过滤active和inclue的后,添加进来
		this.profiles.addAll(otherActiveProfiles);
        // 添加额外引入的
		this.profiles.addAll(includedViaProperty);
		addActiveProfiles(activatedViaProperty);
		if (this.profiles.size() == 1) { // only has null profile
			for (String defaultProfileName : this.environment.getDefaultProfiles()) {
				Profile defaultProfile = new Profile(defaultProfileName, true);
				this.profiles.add(defaultProfile);
			}
		} 
     }
  }
  private static class Profile {
	private final String name; // 作用域的名称,如dev\sit\uat\pro
    private final boolean defaultProfile; // true表示默认的配置文件,指定了作用域则是false
  }
}

然后会从当前的Environment遍历获取spring.profiles.activespring.profiles.include两个配置值,但是目前此时它们只能存在于JVM参数本地环境变量。也就是说,若我们没有提前配置这两个activeinclude的值,最终会帮我们创建两个Profile对象,一个是null对象,一个是名称是defaultProfile对象(实际上在后面的load方法中若读取到spring.profiles.active的配置值时,会移除掉此defaultProfile的对象)。

相信大家对spring.profiles.active并不陌生,因为该配置值决定了不同环境上的应用程序应该使用哪套配置文件。而spring.profiles.include则是spring.profiles.active的一个补充,如果希望程序除了使用application和指定的如application-pro等之外的配置文件,就可以使用include属性指定。

3.2.2.2  load(profile, ...)方法

initializeProfiles()方法结束后,该监听器的profiles集合有两个对象,null对象和名称为defaultProfile对象,其中null对象是无论JVM本地环境变量有没有配置activeinclude的参数都会创建的,而default这个则是JVM环境变量没配置时会创建的。

lamada函数中,会遍历profiles集合获取队首的元素,首个元素就是null对象,那么让我们进去load(null, ...)方法看看发生了什么。

原来,首先是通过getSearchLocations()方法获取配置文件的所在路径,上面的init方法只是确定了配置文件的作用域。

这个答案在getSearchNames()方法找到,从下图的debug中可以看到的确是application。当我们没有在JVM或者本地环境变量配置spring.config.name配置的时候,返回的是默认的application

private Set<String> getSearchNames() {
    // 多个指定配置文件名称使用,分割
	if (this.environment.containsProperty("spring.config.name")) {
		String property = this.environment.getProperty("spring.config.name");
		Set<String> names = asResolvedSet(property, null);
		names.forEach(this::assertValidConfigName);
		return names;
	}
    // 这就是为什么默认是application的原因
	return asResolvedSet(ConfigFileApplicationListener.this.names, "application");
}

当配置文件资源的坐标确定以后就可以开始读取配置文件了。不过配置文件的类型常见的有两种,propertiesyaml。

如下图所示,配置文件的加载是通过PropertySourceLoader对象处理的,这里就有PropertiesYaml两个PropertySourceLoader对象,并且注意到Properties在前,Yaml在后,这个顺序其实决定了不同类型文件的优先级关系。

因为在每一个相同作用域、不同文件类型的配置源对象最终都是以其对应的Profile文件为key,存储到内部类的Loader对象的Map<Profile, MutablePropertySources> loaded集合属性中的,并且之前已经介绍了Profile就两个属性,namedefaultProfile,name表示作用域,也就是在这里会先添加properties的配置源,再添加yaml的配置源,因此properties的优先级高于yaml

配置文件的数据正是封装到Document对象中类型是PropertySource的对象中,PropertySource此前已经介绍过,它内部的泛型字段T source一般都是key-value的数据结构。

此外,注意看Document中还有activeProfilesincludeProfiles的两个类型是Set<Profile>的集合属性字段,它们有什么用呢?其实这两个字段的设计正是为SpringBoot默认每个应用程序都有一个默认的application文件而生。

当我们没有在JVM参数或者本地环境变量设置spring.profiles.active/include的时侯,会从监听器的profiles集合获取到null元素,即第一次进入load(profile,...)方法时对应的默认application文件读取,如果在这个默认配置文件还没有读取到这两配置值,经过试验验证后,的确配置文件的读取就结束了,Spring容器中只能读到JVM参数本地环境变量、和默认的application配置文件参数。

private static class Document {
    // 每个配置文件的数据都封装到这个PropertySource对象中
	private final PropertySource<?> propertySource;
	private String[] profiles;
	private final Set<Profile> activeProfiles; // 激活的作用域对应的配置文件
	private final Set<Profile> includeProfiles;// 包含的作用域对应的配置文件
	Document(PropertySource<?> propertySource, String[] profiles, Set<Profile> activeProfiles,Set<Profile> includeProfiles) {
	  this.propertySource = propertySource;
	  this.profiles = profiles;
	  this.activeProfiles = activeProfiles;
	  this.includeProfiles = includeProfiles;
    }
}

读取每个配置文件时都会寻找spring.profiles.active/include配置值

每个作用域的优先级都不一样,那么它们的顺序是怎么样的呢?

active>include>默认

当每个配置文件读取到了spring.profiles.active/include的值时,会存储到Document对象的activeProfiles集合和includeProfiles集合,然后若符合条件则为它们创建一个Profiles对象存入该监听器的profiles集合,不过这里的添加顺序也是有讲解的,因为顺序直接影响到activeinclude的优先级。

从上图看,似乎是先添加active作用域的Profile对象进去,后添加include的,其实不是,如果是这样的话,那么在后面addLoadedPropertySources()方法的话,由于会将配置源反转再归并到Enviroment变量,那么这样就会变成include的优先级高于active

实际上,active是高于include的,在下面伪代码可以看到,会先将profiles集合中元素提取到临时变量中,接着清除该集合再添加include作用域的,最后再把临时变量的元素放回profiles集合中。

可能有点绕,还是以一开始JVM和本地环境变量没有配置spring.profiles.active/include为例,在init方法创建了两个Profile对象,一个是null值,一个是名为defaultProfile对象。

null对应的application对象被加载读取到active/include配置的作用域时,会创建对应的Profile对象,并且先执行addActiveProfiles(profiles)方法将active对应的Profile对象添加进去,同时会移除init方法创建的那个default的对象。

此时profiles集合中只有一个active的,接着调用addIncludedProfiles(profiles)方法将include对应的Profile对象添加进去,不过是先取出之前的对象保存起来,再添加,最后将保存的对象再放进去。如此一来,profiles中的元素依次就是includeactive了,并且配置源排序此时是默认的、include的、active的。经过后面反转配置源顺序后,优先级便变成了active>include>默认

public class ConfigFileApplicationListener {
  private class Loader {
     void addActiveProfiles(Set<Profile> profiles) {
		if (profiles.isEmpty()) {
			return;
		}
		this.profiles.addAll(profiles);
		this.activatedProfiles = true;
        // 当读取到spring.profiles.active时,
        // 会移除init()方法创建的那个default的Profile对象
		removeUnprocessedDefaultProfiles();
	 } 
     private void addIncludedProfiles(Set<Profile> includeProfiles) {
        // 先将此前存入的profiles存储到临时变量
		LinkedList<Profile> existingProfiles = new LinkedList<>(this.profiles);
		this.profiles.clear(); // 清空整个profiles集合
		this.profiles.addAll(includeProfiles);// 添加include作用域的Profile对象
		this.profiles.removeAll(this.processedProfiles);// 移除标记过已经处理的Profile对象
		this.profiles.addAll(existingProfiles);// 最后把原来的临时变量的Profile再添加回去
	 }
  }
}

initializeProfiles()方法先添加一个null元素的原因揭秘

至此可以解答之前的疑问,为什么在initializeProfiles()方法先添加一个null元素,因为若不在JVM环境变量配置spring.profiles.active/include的配置时,那么一个配置文件都不会读取到,添加null元素就是为了至少一个读取默认的配置文件

最终配置文件的数据封装到MutablePropertySourcespropertySourceList集合中去,然后将其存储到Loader对象的Map<Profile, MutablePropertySources> loaded集合变量中,key则是作用域对应的Profile对象,value就是propertySourceList集合。

注意,这个loaded变量的实例是一个LinkedHashMap,是有序的一个Map集合,也就是按添加顺序排序,是一个先进先出的集合。

因为前面说过,不同作用域文件的加载是通过遍历的Profile文件调用load(profile,...)方法执行的。

至此可以解答之前的疑问,为什么在initializeProfiles()方法先添加一个null元素,因为若不在JVM环境变量配置spring.profiles.active/include的配置时,那么一个配置文件都不会读取到,添加null元素就是为了至少一个读取默认的配置文件

最终配置文件的数据封装到MutablePropertySourcespropertySourceList集合中去,然后将其存储到Loader对象的Map<Profile, MutablePropertySources> loaded集合变量中,key则是作用域对应的Profile对象,value就是propertySourceList集合。

注意,这个loaded变量的实例是一个LinkedHashMap,是有序的一个Map集合,也就是按添加顺序排序,是一个先进先出的集合。

3.2.2.3  addLoadedPropertySources()方法

这个方法将会把前面读取到的所有配置文件对应的配置源对象归档到Enviroment变量中。具体操作如下伪代码,首先将Enviroment变量装载不同配置源对象的集合拿出来,然后将loaded中配置文件的配置源对象获取后再将顺序反转。

将加载的配置文件对应的配置源对象顺序反转,意味着什么?

其实通过反转配置文件对应的配置源,是配置数据优先级的分配,在load(profile, ...)方法已经分析过经过反转后,就作用域来说,优先级分配是active>include>默认的application

此外,在load(profile, ...)方法也看到过,不同文件类型的配置文件是通过PropertySourceLoader集合中的对象去加载的,这个集合里面有两个元素,分别是PropertiesPropertySourceLoader以及YamlPropertySourceLoader,前者解析.properties结尾的文件,后者解析.yaml结尾的文件,也就是说,经过反转后,yaml配置文件优先级高于properties文件。

private void addLoadedPropertySources() {
    // Enviroment对象装载配置的对象
	MutablePropertySources destination = this.environment.getPropertySources();
    // 获取所有前面读取加载完的配置文件对应的配置源对象
	List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
    // 注意,这里将配置源对象反转了
	Collections.reverse(loaded);
	String lastAdded = null;
	Set<String> added = new HashSet<>();
	for (MutablePropertySources sources : loaded) {
		for (PropertySource<?> source : sources) {
			if (added.add(source.getName())) {
                // 将配置源添加到destination最后面
				addLoadedPropertySource(destination, lastAdded, source);
				lastAdded = source.getName();
			}
		}
	}
}

4.  简单例子&配置源的优先级验证

优先级分配:

不同类型文件: properties文件 > yaml文件 

同类型文件:spring.profiles.active spring.profiles.include > 默认

优先级是否是上面所说呢?接下来将在本章节验证

如下图所示,首先在默认的application.properties文件中配置spring.profiles.active=dev以及spring.profiles.include=devPlus。

接着再创建三个配置文件:application-dev.propertiesapplication-devPlus.propertiesapplication-dev.yaml。

在这四个配置文件中都配置同一个key是abc的配置值。那么让我们运行程序,结果如下,配置文件优先级顺序如下:

application-dev.properties > application-dev.yaml > application-devPlus.properties > application.properties。

5.  总结

SpringBoot根据项目类型Enviroment变量,然后通过ConfigFileApplicationListener这个监听器对象完成配置文件的读取和将数据并入Enviroment变量中,并且经过分析知道了不同类型文件的优先级、不同作用域下的优先级等。

其实创建Enviroment变量也是SpringBoot框架中最重要的操作之一,因为在整个Spring容器中Bean的生命周期来说,Enviroment变量必须先于Bean创建开始,因此了解其创建过程和原理对于理解SpringBoot/Spring框架也是颇有益处的。

显示全文