(二)SpringBoot启动过程的分析-环境信息准备

-- 以下内容均基于2.1.8.RELEASE版本

由上一篇(一)SpringBoot启动过程的分析-启动流程概览可以发现在run方法内部启动SpringBoot应用时采用多个步骤来实现,本文记录启动的第二个环节:环境信息准备

阅读提示

阅读本文应当关注的重点:

  1. 应用一开始启动阶段通过SPI扩展机制获取的接口,往往他们就是Spring框架扩展的一部分
  2. 有关于事件发布的接口,它们用于处理不同阶段的任务,这里也是Spring框架扩展的一部分
  3. 默认配置和指定环境的配置(profiles)他们的关系(是否覆盖,优先级等)
  4. 配置文件加载的路径和名称(包括指定加载的位置和指定加载的名称)
  5. 配置加载完毕之后的存放位置和获取方式

什么是环境信息(Environment)

Spring中的Environment描述的是当前运行应用环境的概念,它提供了对profils以及对应的应用程序配置文件和系统配置的访问和管理。因此在应用程序启动之前需要将所需要的所有配置进行初始化。环境信息的入口位于SpringApplication.run()方法中。

创建和配置环境信息

// SpringApplication.java

ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {

	// ①
	ConfigurableEnvironment environment = getOrCreateEnvironment();
	// ②
	configureEnvironment(environment, applicationArguments.getSourceArgs());
	// ③
	ConfigurationPropertySources.attach(environment);
	// ④
	listeners.environmentPrepared(environment);
	// ⑤
	bindToSpringApplication(environment);
	if (!this.isCustomEnvironment) {
		environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
	}
	// ⑥
	ConfigurationPropertySources.attach(environment);
	return environment;
}

该方法为SpringBoot启动阶段准备环境信息的入口,初始化配置等功能均在此完成。

① - 获取or创建一个ConfigurableEnvironment
② - 配置环境
③ - 将ConfigurationPropertySource支持附加到指定的环境
④ - 触发环境信息准备完毕事件
⑤ - 绑定环境信息到当前应用程序
⑥ - 可以思考一下为什么此处又调用一次

创建ConfigurableEnvironment对象

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

创建环境对象是根据当前应用的类型进行选择性创建,默认创建标准的环境对象,此处我们的应用一般为Servlet,所以它会创建对应的StandardServletEnvironment。

public class StandardServletEnvironment extends StandardEnvironment implements ConfigurableWebEnvironment {

    public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";
    public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";
    public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties";

    public StandardServletEnvironment() {
    }

    protected void customizePropertySources(MutablePropertySources propertySources) {
        propertySources.addLast(new StubPropertySource("servletConfigInitParams"));
        propertySources.addLast(new StubPropertySource("servletContextInitParams"));
        if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
            propertySources.addLast(new JndiPropertySource("jndiProperties"));
        }
        super.customizePropertySources(propertySources);
    }

    public void initPropertySources(@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
        WebApplicationContextUtils.initServletPropertySources(this.getPropertySources(), servletContext, servletConfig);
    }
}

StandardServletEnvironment类继承了StandardEnvironment, StandardEnvironment继承了AbstractEnvironment, AbstractEnvironment的构造方法中调用了customizePropertySources(this.propertySources)方法,它在StandardServletEnvironment和AbstractEnvironment中均有实现,因此均会被执行。

在StandardServletEnvironment代码中可以看到它负责初始化了两个配置集(此处是一个key对应一组配置,笔者称其为配置集)分别是servletContextInitParams和servletConfigInitParams。接着调用父类的customizePropertySources(propertySources)即StandardEnvironment类中的实现。同时还有一个initPropertySources()方法用于初始化环境对象中的配置。此处虽然是初始化环境对象,但是里面的配置除了系统运行环境之外都是空对象。

public class StandardEnvironment extends AbstractEnvironment {

	/** System environment property source name: {@value}. */
	public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";

	/** JVM system properties property source name: {@value}. */
	public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";

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

}

StandardEnvironment内部初始化了两个系统级别的配置集分别是systemEnvironment和systemProperties; 至此StandardServletEnvironment环境对象创建完毕,它内部已经存放了4个配置集。

配置环境信息

前面说道环境信息就是profiles和配置文件以及系统环境信息的封装,在开发过程中我们根据官方指南和经验得知一个profile是有一套配置信息的,那么它的功能实现肯定是首先获取profile,然后再获取配置。但是我们在环境对象创建的时候发现它已经初始化了4个配置集,并且包含了系统配置(当前应用所运行的载体的配置信息)。但是这些配置信息除了系统配置之外其他都为空值,可以推断它只是预先初始化好配置容器,后续进行填充更新。

因此我们可以引出几个问题:

  • 配置什么时候被填充?
  • 被填充的配置由何而来?
// SpringApplication.java

protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
	// ①
	if (this.addConversionService) {
		ConversionService conversionService = ApplicationConversionService.getSharedInstance();
		environment.setConversionService((ConfigurableConversionService) conversionService);
	}
	// ②
	configurePropertySources(environment, args);
	// ③
	configureProfiles(environment, args);
}

① - 注册ConversionService, 它是Spring内部用来处理统一类型转换的服务(将配置文件中的不同类型转换为它所要表示的类型例如:xxx.enable = false,将会将false转换为真正的boolean对象),一些常见的转换服务有StringToDurationConverter、DurationToStringConverter、StringToDataSizeConverter等,见名知意,若想了解更多,可参考此文: 抹平差异,统一类型转换服务ConversionService
② - 配置属性资源
③ - 配置Profiles

配置属性资源

// SpringApplication.java

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

① - 获取环境对象中的配置对象
② - 检查并插入默认配置
③ - 检查并插入命令行配置

配置Profiles

// SpringApplication.java

protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
	// ①
	environment.getActiveProfiles(); 
	// ②
	Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
	profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
	// ③
	environment.setActiveProfiles(StringUtils.toStringArray(profiles));
}

@Override
public String[] getActiveProfiles() {
	return StringUtils.toStringArray(doGetActiveProfiles());
}

protected Set<String> doGetActiveProfiles() {
	synchronized (this.activeProfiles) {
		if (this.activeProfiles.isEmpty()) {
			// ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active"
			String profiles = getProperty(ACTIVE_PROFILES_PROPERTY_NAME);
			if (StringUtils.hasText(profiles)) {
				setActiveProfiles(StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(profiles)));
			}
		}
		return this.activeProfiles;
	}
}

① - environment.getActiveProfiles()方法主要用于配置哪些配置文件处于活动状态,默认情况下是default,default对应application.properties、application.yml等默认Spring配置文件,其他诸如application-dev.yml、application-sit.yml等则可以通过在默认配置文件中指定spring.profiles.active或者是在应用启动阶段使用-Dspring.profiles.active=xxx来激活状态。从doGetActiveProfiles()方法可以看出它会从已经解析的默认配置文件中寻找spring.profiles.active配置,若不为空就加入profiles列表。
② - 获取所有的profiles,并将当前获取的profile放入排序
③ - 设置为活动profile。

// ConfigurationPropertySources.java

public static void attach(Environment environment) {
	Assert.isInstanceOf(ConfigurableEnvironment.class, environment);
	MutablePropertySources sources = ((ConfigurableEnvironment) environment).getPropertySources();
	PropertySource<?> attached = sources.get(ATTACHED_PROPERTY_SOURCE_NAME);
	if (attached != null && attached.getSource() != sources) {
		sources.remove(ATTACHED_PROPERTY_SOURCE_NAME);
		attached = null;
	}
	if (attached == null) {
		sources.addFirst(new ConfigurationPropertySourcesPropertySource(ATTACHED_PROPERTY_SOURCE_NAME, new SpringConfigurationPropertySources(sources)));
	}
}

attach(Environment environment)方法会将environment已有的配置信息包装为SpringConfigurationPropertySources, 使用configurationProperties作为key,构建成一个ConfigurationPropertySourcesPropertySource对象重新插入environment最前方,例如environment此时有4个配置,那么此时将会将这4个配置包装为SpringConfigurationPropertySources对象,再构建为ConfigurationPropertySourcesPropertySource对象(此时该对象内部也是4个配置environment对象内部也是4个配置),加入environment(此时environment对象内部有5个配置)。到这一步环境信息的准备已经完毕。
总结一下环境信息准备的内容:

  • 初始化可变环境信息对象,并初始化4个系统级别的配置集
  • 将应用启动的参数信息填入环境信息对象
  • 将已有的配置重新包装为SpringConfigurationPropertySources对象放入环境信息中
通过监听器的触发加载应用程序配置信息

这里从监听器来触发ApplicationEnvironmentPreparedEvent事件。在SpringBoot中ConfigFileApplicationListener监听了此事件,意味着将由它来完成配置信息的加载工作。

// ConfigFileApplicationListener.java

public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {

	private static final String DEFAULT_PROPERTIES = "defaultProperties";

	// Note the order is from least to most specific (last one wins)
	private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";

	private static final String DEFAULT_NAMES = "application";

	private static final Set<String> NO_SEARCH_NAMES = Collections.singleton(null);

	private static final Bindable<String[]> STRING_ARRAY = Bindable.of(String[].class);

	/**
	 * The "active profiles" property name.
	 */
	public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";

	/**
	 * The "includes profiles" property name.
	 */
	public static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";

	/**
	 * The "config name" property name.
	 */
	public static final String CONFIG_NAME_PROPERTY = "spring.config.name";

	/**
	 * The "config location" property name.
	 */
	public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";

	/**
	 * The "config additional location" property name.
	 */
	public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";

	/**
	 * The default order for the processor.
	 */
	public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;

	private final DeferredLog logger = new DeferredLog();

	private String searchLocations;

	private String names;

	private int order = DEFAULT_ORDER;
	
	// ...省略部分代码
}
  • 实现了EnvironmentPostProcessor意味着它也具有处理环境信息的能力,它本身也作为一个EnvironmentPostProcessor。
  • 实现了Ordered接口意味着它具有排序能力(相同接口实现列表中的排序优先级),并且它的优先级被设置为了最高-10(Integer.MIN_VALUE为最高优先级,+10就相当于数值加大优先级减10):DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10。

通过事件的触发执行

// ConfigFileApplicationListener.java

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

// 执行ApplicationEnvironmentPreparedEvent事件
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());
	}
}

① - 通过SPI方式获取了所有的EnvironmentPostProcessor。
② - 将所有的Processor进行优先级排序
③ - 执行所有的Processor

默认状态下一共会有4个Processor他们分别是(按照优先级由高至低):

SystemEnvironmentPropertySourceEnvironmentPostProcessor
SpringApplicationJsonEnvironmentPostProcessor
CloudFoundryVcapEnvironmentPostProcessor
ConfigFileApplicationListener

很明显处理系统参数的Processor优先级最高,分别看看他们都做了什么操作

SystemEnvironmentPropertySourceEnvironmentPostProcessor
// SystemEnvironmentPropertySourceEnvironmentPostProcessor.java

public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
	String sourceName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME;
	PropertySource<?> propertySource = environment.getPropertySources().get(sourceName);
	if (propertySource != null) {
		replacePropertySource(environment, sourceName, propertySource);
	}
}

private void replacePropertySource(ConfigurableEnvironment environment, String sourceName, PropertySource<?> propertySource) {
	// ①
	Map<String, Object> originalSource = (Map<String, Object>) propertySource.getSource();
	// ②
	SystemEnvironmentPropertySource source = new OriginAwareSystemEnvironmentPropertySource(sourceName, originalSource);
	// ③
	environment.getPropertySources().replace(sourceName, source);
}

① - 获取系统参数的名称StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME: systemEnvironment
② - 根据systemEnvironment名称获取配置对象propertySource
③ - 替换配置

这里的替换配置是指将原有environment中SystemEnvironmentPropertySource {name='systemEnvironment'}的source对象由UnmodifiableMap格式转换为OriginAwareSystemEnvironmentPropertySource实际就是转换为PropertySource,这是Spring对各种资源的一种统一封装方式

SpringApplicationJsonEnvironmentPostProcessor
CloudFoundryVcapEnvironmentPostProcessor

Cloud Foundry 云平台相关配置,无需关注

ConfigFileApplicationListener
// ConfigFileApplicationListener.java

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

// 负责将配置文件添加到指定环境中去
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
	RandomValuePropertySource.addToEnvironment(environment);
	new Loader(environment, resourceLoader).load();
}
  • 新增一个随机值属性源
  • 构建一个新的资源加载器并加载资源
构建资源加载器
// ConfigFileApplicationListener$Loader.java

Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
	this.environment = environment;
	// ①
	this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
	// ②
	this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
	// ③
	this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, getClass().getClassLoader());
}

① - 创建属性资源占位符解析器PropertySourcesPlaceholdersResolver用于解析${}这样的配置
② - 获取资源加载器
③ - 通过SPI获取扩展的属性资源加载器此处获取了PropertiesPropertySourceLoader(用于解析properties、xml配置文件)和 YamlPropertySourceLoader(用于解析yml、yaml配置文件)

使用资源加载器加载配置文件

通过构造器初始化了不同资源的加载器(properties、xml、yml),通过load()方法开始真正加载应用中的配置文件

// ConfigFileApplicationListener$Loader.java

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

// ①
private void initializeProfiles() {
	this.profiles.add(null);
	Set<Profile> activatedViaProperty = getProfilesActivatedViaProperty();
	this.profiles.addAll(getOtherActiveProfiles(activatedViaProperty));
	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);
		}
	}
}

① - 从environment或者spring.profiles.active/spring.profiles.include来初始化profiles值,对于默认的profiles这里设置为null,方便首先处理,因为后续profiles为空的配置将被首先处理,且优先级最低。这里的默认profiles对应的配置文件为application.*配置文件
② - 开始加载配置文件
在加载配置文件之前,先介绍一下传入方法的两个参数,一个是DocumentFilter,一个是DocumentConsumer,他们分别用于判断当前profile和处理加载到的资源是否匹配; this::getPositiveProfileFilter表示当前对象内部的getPositiveProfileFilter方法

// ConfigFileApplicationListener$Loader.java
// 获取肯定的Profiles过滤器,这里是预设立场认为当前的配置文件(document)符合当前操作的profile
// 在默认情况下不会有人去配置文件指定spring.profiles值
// 在默认profile的配置文件也需要加载,例如:spring.profiles.active=test 那么除了application-test.properties之外,application.properties也需要加载
private DocumentFilter getPositiveProfileFilter(Profile profile) {
	return (Document document) -> {
		if (profile == null) {
			return ObjectUtils.isEmpty(document.getProfiles());
		}
		return ObjectUtils.containsElement(document.getProfiles(), profile.getName()) && this.environment.acceptsProfiles(Profiles.of(document.getProfiles()));
	};
}

如果当前文档获取的profile(配置文件内部指定的spring.profiles属性值)和传入的profile匹配就返回true不匹配返回false,返回值决定了当前操作的document是否需要加载。

同时还存在着另外一个DocumentFilter

// ConfigFileApplicationListener$Loader.java
// 这里预设了一个立场:当前spring.profiles.active = spring.profiles
private DocumentFilter getNegativeProfileFilter(Profile profile) {
	return (Document document) -> (profile == null && !ObjectUtils.isEmpty(document.getProfiles()) && this.environment.acceptsProfiles(Profiles.of(document.getProfiles())));
}

如果当前文档获取的profile(在配置文件中指定了spring.profiles=xxx)和传入的profile匹配就返回true不匹配返回false,事实上如果你当前使用spring.profiles.active=test,然后又在application-test.yml中写了
spring.profiles = dev 这真是一个骚操作,请勿这样使用!这样将会导致当前解析的配置文件环境归属是以内部指定的参数来确定。正确方式应该是在启动时指定当前应用的活动profile即:spring.profiles.active = test, 同时配置文件命名为application-test.yml。

// ConfigFileApplicationListener$Loader.java

private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
	// ①
	getSearchLocations().forEach((location) -> {
		boolean isFolder = location.endsWith("/");
		Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
		// ②
		names.forEach(
		// ③
		(name) -> load(location, name, profile, filterFactory, consumer)
		);
	});
}

① - 在加载配置之前获取到所有需要加载的配置文件默认存放路径列表
② - 在每个路径下再次获取需要获取的文件名称列表,最终使用的是双层循环来实现所有配置加载
③ - 开始加载配置文件

获取配置文件路径

// ConfigFileApplicationListener$Loader.java

private Set<String> getSearchLocations() {
	// ①
	if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
		return getSearchLocations(CONFIG_LOCATION_PROPERTY);
	}
	// ②
	Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
	// ③
	locations.addAll(asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
	return locations;
}

① - 首先判断有没有指定配置文件路径spring.config.location, 若设置了指定位置,则获取指定位置。
② - 从spring.config.additional-location配置指定的地方获取。
③ - 从默认位置classpath:/,classpath:/config/,file:./,file:./config/中获取

从配置文件路径的获取逻辑来看,若指定了spring.config.location则默认位置的配置将不会被读取

获取配置文件名称

// ConfigFileApplicationListener$Loader.java

private Set<String> getSearchNames() {
	// ①
	if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
		String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
		return asResolvedSet(property, null);
	}
	// ②
	return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
}

① - 从属性spring.config.name判断是否指定了配置文件的名称
② - 若未指定名称则使用默认的名称,即ConfigFileApplicationListener.DEFAULT_NAMES = "application"。

根据路径、名称、profile来加载配置文件

// ConfigFileApplicationListener$Loader.java

private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
	if (!StringUtils.hasText(name)) {
		// ① 
		for (PropertySourceLoader loader : this.propertySourceLoaders) {
			if (canLoadFileExtension(loader, location)) {
				load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
				return;
			}
		}
	}
	Set<String> processed = new HashSet<>();
	// ②
	for (PropertySourceLoader loader : this.propertySourceLoaders) {
		for (String fileExtension : loader.getFileExtensions()) {
			// ③
			if (processed.add(fileExtension)) {
				// ④
				loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory, consumer);
			}
		}
	}
}

① - 处理配置文件名称为空的情况,例如spring.config.name = " "。若配置文件为空字符串,将会获取不到文件后缀名,并不会去加载它。
② - 处理配置文件名称不为空的情况,循环所有的属性资源加载器分别用他们来尝试解析可能存在的不同类型的配置文件,这里使用路径(location)+ 文件名(name)+ "-" + profile + 后缀名(properties / xml / yml / yaml)来拼接出一个文件的完整路径
③ - 保存处理文件类型的记录
④ - 加载具有后缀名的文件

加载具有后缀名的文件

// ConfigFileApplicationListener$Loader.java

private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
	// ①
	DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
	DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
	// ②
	if (profile != null) {
		// ③
		String profileSpecificFile = prefix + "-" + profile + fileExtension;
		// ④
		load(loader, profileSpecificFile, profile, defaultFilter, consumer);
		// ⑤
		load(loader, profileSpecificFile, profile, profileFilter, consumer);
		// ⑥
		for (Profile processedProfile : this.processedProfiles) {
			if (processedProfile != null) {
				String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
				// ⑦
				load(loader, previouslyLoaded, profile, profileFilter, consumer);
			}
		}
	}
	// ⑧
	load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}

① - 获取默认profile和指定profile的文档过滤器,用于判断当前配置文件内部指定的spring.profiles是否和spring.profiles.active相同,相同则加载此配置反之。
② - 处理profile不为空的情况
③ - 组装配置文件名称,例如dev环境:classpath:/application-dev.properties / classpath:/application-dev.xml / classpath:/application-dev.yml / classpath:/application-dev.yaml
④ - 基于defaultFilter来加载配置 这里的过滤器是指当前获取的文件是哪个profile,以配置文件所属的profile为准
⑤ - 基于profileFilter来加载配置 这里强制要求配置文件所属的profile必须和当前处理的profile一致
⑥ - 处理特殊情况的配置,例如application.properties里面指定了spring.profiles=dev, test;那么在profile = test的时候也能读取application-dev.properties, 骚操作,一般情况不建议搞这么复杂,特殊配置交叉使用完全可以通过配置中心隔离
⑦ - 加载非当前profile的配置文件
⑧ - 默认的profile(profile == null)也尝试去加载一下文件

加载配置

// ConfigFileApplicationListener$Loader.java

private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter, DocumentConsumer consumer) {
	try {
		// ①
		Resource resource = this.resourceLoader.getResource(location);
		// ②
		if (resource == null || !resource.exists()) {
			if (this.logger.isTraceEnabled()) {
				StringBuilder description = getDescription("Skipped missing config ", location, resource, profile);
				this.logger.trace(description);
			}
			return;
		}
		// ③
		if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
			if (this.logger.isTraceEnabled()) {
				StringBuilder description = getDescription("Skipped empty config extension ", location, resource, profile);
				this.logger.trace(description);
			}
			return;
		}
		// ④
		String name = "applicationConfig: [" + location + "]";
		List<Document> documents = loadDocuments(loader, name, resource);
		if (CollectionUtils.isEmpty(documents)) {
			if (this.logger.isTraceEnabled()) {
				StringBuilder description = getDescription("Skipped unloaded config ", location, resource, profile);
				this.logger.trace(description);
			}
			return;
		}
		// ⑤
		List<Document> loaded = new ArrayList<>();
		for (Document document : documents) {
			if (filter.match(document)) {
				addActiveProfiles(document.getActiveProfiles());
				addIncludedProfiles(document.getIncludeProfiles());
				loaded.add(document);
			}
		}
		// ⑥
		Collections.reverse(loaded);
		if (!loaded.isEmpty()) {
			// ⑦
			loaded.forEach((document) -> consumer.accept(profile, document));
			if (this.logger.isDebugEnabled()) {
				StringBuilder description = getDescription("Loaded config file ", location, resource, profile);
				this.logger.debug(description);
			}
		}
	}
	catch (Exception ex) {
		throw new IllegalStateException("Failed to load property " + "source from location '" + location + "'", ex);
	}
}

① - 通过资源加载器加载指定路径的资源信息
② - 若资源不存在即配置文件不存在则记录日志
③ - 若配置文件后缀名为空即非法配置文件则调过
④ - 将资源文件转换为Document,并将此次获取的文档保存至缓存loadDocumentsCache
⑤ - 遍历配置文件,并将配置文件中的spring.profiles属性值设置为活动profile(若存在的话)
⑥ - 反转一下已获取的配置
⑦ - 已经加载的文件合并到merged中,实际上就是添加到当前已经加载的配置对象列表propertySourceList中,这里主要用于处理默认的配置文件和当前指定环境的配置文件,在前面可以看到他们被添加进了缓存内部,后面处理这些配置文件将从缓存中获取

重置活动Profile

private void resetEnvironmentProfiles(List<Profile> processedProfiles) {
	// ①
	String[] names = processedProfiles.stream().filter((profile) -> profile != null && !profile.isDefaultProfile()).map(Profile::getName).toArray(String[]::new);
	// ②
	this.environment.setActiveProfiles(names);
}

① - 获取已经处理过的profile名称数组
② - 设置为活动profile。

配置文件的优先级

到现在配置已经加载完毕,但还未将其填充到环境信息对象中,回到最初的load()方法中

public void load() {
	// ... 省略前面步骤
	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();
}

① - 加载配置,没错这里又加载了一次,为何要再次加载一次?对比两次加载,可以看出第一次是在有profile的情况,并且将配置添加到尾部,第二次是没有profile的情况将配置添加到头部,这里主要是为了实现配置的覆盖优先级

例如:
现有profile: spring.profiles.active=dev, test
文件:application.properties, application-dev.properties, application-test.properties, application.yml, application-dev.yml, application-test.yml, application.xml, application-test.xml, application-test.yaml
假设这种奇葩现象存在,若都存在一个配置example.current = xxx 那么究竟要区哪个文件呢?
答案是:
以profile划分:test > 默认 > dev
以文件类型划分: properties > xml > yml > yaml
也就是application-test.properties > application.properties > application-test.xml > application-test.yml > application-test.yaml > application-dev.properties...以此类推

② - 将配置信息按照优先级添加到环境信息对象中

总结

① 配置文件的加载动作是由监听器ConfigFileApplicationListener触发
② 根据指定的不同profile匹配指定的配置文件名称,默认支持的文件后缀为:properties/xml/yml/yaml;默认支持的配置文件路径有:classpath:/,classpath:/config/,file:./,file:./config/
③ 不同类型的文件有对应的解析器处理
④ 配置文件的优先级首先以profile的声明顺序为主,后声明的优先级高,文件类型以默认支持的文件类型顺序为主,最先声明的优先级最高
⑥ 指定profile可以通过参数spring.profiles.active=dev,common指定,也可以在配置文件中声明spring.profiles=dev,common来指定,不推荐使用后者
⑦ 指定配置文件的位置可以通过参数spring.config.location=xxxx来指定
⑧ 指定配置文件的名称可以通过参数spring.config.name=xxxx来指定

posted @ 2021-02-24 16:41  不会发芽的种子  阅读(766)  评论(0编辑  收藏  举报