SpringBoot 扩展篇:ConfigFileApplicationListener源码解析

SpringBoot外化配置源码解析:Profile处理实现 基于Profile 的处理实现 在日常使用中我们可以通过配置 spring.profiles.active 指定一组不同环境的配置文件,比如application-dev.properties、application-test.properties、application-prod.properties。那么,profile 是如何被加载使用的呢?本节带大家重点分析一下 ConfigFileApplicationListener 类中基于 profile 的文件加载处理逻辑。 在 ConfigFileA 阅读详情

1.概述

SpringBoot的配置文件加载由ConfigFileApplicationListener完成的,它会加载application.properties、application.yml等配置文件,还支持用户配置和扩展。本文从源码的角度分析它的原理。

加载完毕的配置信息最终都会放入到Environment中。

2. ConfigFileApplicationListener定义

在这里插入图片描述
ConfigFileApplicationListener定义在spring.factories中。监听器注册和执行原理参考:SpringBoot 源码解析3:事件监听器

3. ConfigFileApplicationListener回调链路

3.1 SpringApplication#run

public ConfigurableApplicationContext run(String... args) {
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
	ConfigurableApplicationContext context = null;
	configureHeadlessProperty();
	SpringApplicationRunListeners listeners = getRunListeners(args);
	listeners.starting();
	try {
		ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
		ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
		configureIgnoreBeanInfo(environment);
		Banner printedBanner = printBanner(environment);
		context = createApplicationContext();
		prepareContext(context, environment, listeners, applicationArguments, printedBanner);
		refreshContext(context);
		afterRefresh(context, applicationArguments);
		stopWatch.stop();
		if (this.logStartupInfo) {
			new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
		}
		listeners.started(context);
		callRunners(context, applicationArguments);
	}
	catch (Throwable ex) {
		handleRunFailure(context, ex, listeners);
		throw new IllegalStateException(ex);
	}

	try {
		listeners.running(context);
	}
	catch (Throwable ex) {
		handleRunFailure(context, ex, null);
		throw new IllegalStateException(ex);
	}
	return context;
}

这是SpringBoot启动最基础的方法,调用了prepareEnvironment。

3.2 SpringApplication#prepareEnvironment

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
		ApplicationArguments applicationArguments) {
	// Create and configure the environment
	// 创建environment对象
	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;
}
  1. getOrCreateEnvironment创建StandardServletEnvironment,所有的启动参数和配置文件信息都会保存到environment中。environment中默认创建了4个propertySource,分别用来存放系统属性和servlet属性。
    在这里插入图片描述

  2. configureEnvironment配置环境信息,此时配置文件还没解析。

  3. listeners.environmentPrepared,调用监听器ConfigFileApplicationListener解析配置文件。最终回调了ConfigFileApplicationListener#onApplicationEvent,这里是解析文件的核心逻辑。

@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
	this.initialMulticaster
			.multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
}

监听器发布的是ApplicationEnvironmentPreparedEvent类型的事件。

  1. bindToSpringApplication解析完毕所有的配置文件信息之后,将spring.main.*的环境变量与当前的springApplication对象的属性绑定。比如allowBeanDefinitionOverriding配置就是在这里读取的。

3.3 配置environment

SpringApplication#configureEnvironment

protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
	if (this.addConversionService) {
		ConversionService conversionService = ApplicationConversionService.getSharedInstance();
		environment.setConversionService((ConfigurableConversionService) conversionService);
	}
	configurePropertySources(environment, args);
	configureProfiles(environment, args);
}
  1. 第二个参数args为SpringBoot启动参数。
  2. configurePropertySources方法会将启动参数解析保存到environment中。
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));
		}
	}
}

通过addFirst会将启动参数的属性添加到第一个PropertySources,优先级最高。

protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
	if (this.propertySources != null) {
		for (PropertySource<?> propertySource : this.propertySources) {
			if (logger.isTraceEnabled()) {
				logger.trace("Searching for key '" + key + "' in PropertySource '" +
						propertySource.getName() + "'");
			}
			Object value = propertySource.getProperty(key);
			if (value != null) {
				if (resolveNestedPlaceholders && value instanceof String) {
					value = resolveNestedPlaceholders((String) value);
				}
				logKeyFound(key, propertySource, value);
				return convertValueIfNecessary(value, targetValueType);
			}
		}
	}
	if (logger.isTraceEnabled()) {
		logger.trace("Could not find key '" + key + "' in any property source");
	}
	return null;
}

如果有多个相同的key在不同的propertySource中,在通过key从environment中获取值的时候,会遍历所有的PropertySources,获取到第一个就会返回。
3. configureProfiles方法

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

通过environment.getActiveProfiles() 获取spring.profiles.active的值,此时的配置文件还没有解析,获取到的是启动参数中的值。

4. 环境准备事件 ConfigFileApplicationListener#onApplicationEvent

由上文可知,发布的是ApplicationEnvironmentPreparedEvent类型的事件

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

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

loadPostProcessors会从spring.factories中加载所有EnvironmentPostProcessor类型的处理器。
SpringBoot 基础概念:SpringApplication#getSpringFactoriesInstances
最终将自己加入到这些处理器中,然后依次执行postProcessEnvironment方法。

4. 加载配置类

加载配置类的核心逻辑的入口在 ConfigFileApplicationListener#postProcessEnvironment。

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

可以看到,加载配置的逻辑交给了Loader。

4.1 Loader相关属性介绍

Loader是ConfigFileApplicationListener的内部类。

构造器

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

propertySourceLoaders 是从spring.factories文件中加载的配置文件加载器。
在这里插入图片描述
PropertiesPropertySourceLoader负责读取*.properties、*.xml中的内容

public class PropertiesPropertySourceLoader implements PropertySourceLoader {

	private static final String XML_FILE_EXTENSION = ".xml";

	@Override
	public String[] getFileExtensions() {
		return new String[] { "properties", "xml" };
	}
	....
}

YamlPropertySourceLoader负责读取*.yml、*.yaml文件中的内容

public class YamlPropertySourceLoader implements PropertySourceLoader {

	@Override
	public String[] getFileExtensions() {
		return new String[] { "yml", "yaml" };
	}
}

ConfigFileApplicationListener中的属性

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

private static final Bindable<List<String>> STRING_LIST = Bindable.listOf(String.class);

private static final Set<String> LOAD_FILTERED_PROPERTY;

static {
	Set<String> filteredProperties = new HashSet<>();
	filteredProperties.add("spring.profiles.active");
	filteredProperties.add("spring.profiles.include");
	LOAD_FILTERED_PROPERTY = Collections.unmodifiableSet(filteredProperties);
}

Loader类中的属性

private final Log logger = ConfigFileApplicationListener.this.logger;

// environment,Spring所有解析的配置信息和启动参数都放入到了environment中 
private final ConfigurableEnvironment environment;

// 占位符解析器,解析${key}
private final PropertySourcesPlaceholdersResolver placeholdersResolver;

// 资源加载器,加载文件资源
private final ResourceLoader resourceLoader;

private final List<PropertySourceLoader> propertySourceLoaders;

// profile对应spring.profiles.active、spring.profiles.include、spring.profiles.default对应的配置属性
private Deque<Profile> profiles;

private List<Profile> processedProfiles;

private boolean activatedProfiles;

private Map<Profile, MutablePropertySources> loaded;

private Map<DocumentsCacheKey, List<Document>> loadDocumentsCache = new HashMap<>();

4.2 Loader加载配置文件

FilteredPropertySource#apply
static void apply(ConfigurableEnvironment environment, String propertySourceName, Set<String> filteredProperties,
		Consumer<PropertySource<?>> operation) {
	MutablePropertySources propertySources = environment.getPropertySources();
	PropertySource<?> original = propertySources.get(propertySourceName);
	// 判断environment中是否有名称为"defaultProperties"的资源
	if (original == null) {
		// 如果没有defaultProperties资源,那么就回调Loader类中的Consumer方法
		operation.accept(null);
		return;
	}
	// 如果有defaultProperties资源,就封装成FilteredPropertySource
	propertySources.replace(propertySourceName, new FilteredPropertySource(original, filteredProperties));
	try {
		// 回调Loader类中的Consumer方法
		operation.accept(original);
	}
	finally {
		// 替换PropertySource
		propertySources.replace(propertySourceName, original);
	}
}

如果environment中没有名称为“defaultProperties”属性资源,那么就直接回调Loader中的Consumer方法 (defaultProperties) -> { … } ,参数为null。

Springboot默认是没有defaultProperties的
在这里插入图片描述

ConfigFileApplicationListener.Loader#load()

对加载配置文件时所需的属性初始化。

void load() {
	FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
			(defaultProperties) -> {
				this.profiles = new LinkedList<>();
				this.processedProfiles = new LinkedList<>();
				this.activatedProfiles = false;
				this.loaded = new LinkedHashMap<>();
				// 初始化profile
				initializeProfiles();
				while (!this.profiles.isEmpty()) {
					Profile profile = this.profiles.poll();
					if (isDefaultProfile(profile)) {
						addProfileToEnvironment(profile.getName());
					}
					// 加载配置文件
					load(profile, this::getPositiveProfileFilter,
							addToLoaded(MutablePropertySources::addLast, false));
					this.processedProfiles.add(profile);
				}
				load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
				// 将加载到的PropertySources放入到environment的最后
				addLoadedPropertySources();
				// 将所有加载到了的profiles,设置到environment中
				applyActiveProfiles(defaultProperties);
			});
}
  1. (defaultProperties) -> { … } 是一个函数接口 Consumer,所以需要先看 FilteredPropertySource#apply方法,在apply方法内部回调这个Consumer。
  2. initializeProfiles:初始化profile。
  3. profiles.poll先入先出,依次加载profile,后续的配置文件中有profile,也会放入到profiles中。
  4. addLoadedPropertySources方法,在profiles循环完毕,所有配置加载完毕,将读取到的内容添加到environment中。
ConfigFileApplicationListener.Loader#initializeProfiles

初始化profile。日常工作中profile指的是dev、uat、prod等配置,但是我们的思维不要局限于这里。

private void initializeProfiles() {
	// The default profile for these purposes is represented as null. We add it
	// first so that it is processed first and has lowest priority.
	// 1. 添加一个为null的profile
	this.profiles.add(null);
	// 获取spring.profiles.active
	Set<Profile> activatedViaProperty = getProfilesFromProperty(ACTIVE_PROFILES_PROPERTY);
	// 获取spring.profiles.include
	Set<Profile> includedViaProperty = getProfilesFromProperty(INCLUDE_PROFILES_PROPERTY);
	// 获取不在当前spring.profiles.active和spring.profiles.include范围内,并且之前获取到的spring.profiles.active(环境)
	List<Profile> otherActiveProfiles = getOtherActiveProfiles(activatedViaProperty, includedViaProperty);
	// 2. 添加不在当前spring.profiles.active和spring.profiles.include范围内的,之前获取到的spring.profiles.active
	this.profiles.addAll(otherActiveProfiles);
	// Any pre-existing active profiles set via property sources (e.g.
	// System properties) take precedence over those added in config files.
	// 3. 添加spring.profiles.include对应的profile
	this.profiles.addAll(includedViaProperty);
	// 4. 添加spring.profiles.active对应的profile
	addActiveProfiles(activatedViaProperty);
	// 5. 如果没有spring.profiles.active和spring.profiles.include,那么就使用spring.profiles.default
	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);
		}
	}
}

profiles添加的优先顺序,决定了profile加载的顺序,先进先出

  1. 添加一个为null的profile。因为就算用户配置了spring.profiles.active=dev,不仅要加载application-dev.yml文件,application.yml文件也需要被加载。
  2. 添加不在当前spring.profiles.active和spring.profiles.include范围内的,之前获取到的spring.profiles.active。前期在SpringBoot启动的时候在SpringApplication#configureProfiles方法中就已经获取到了启动参数中的spring.profiles.active。
  3. 添加spring.profiles.include对应的profile。
  4. 添加spring.profiles.active对应的profile。
  5. 如果没有spring.profiles.active和spring.profiles.include,那么就使用spring.profiles.default对应的profile。
Loader#addLoadedPropertySources

将配置文件中加载的属性放入到environment中。

private void addLoadedPropertySources() {
	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())) {
				addLoadedPropertySource(destination, lastAdded, source);
				lastAdded = source.getName();
			}
		}
	}
}

loaded存放的是已经加载过的属性,它是一个LinkedHashMap,key为profile,value为propertySource,一个propertySource对应一个配置文件。会将profile加载的顺序颠倒,通过addLoadedPropertySource添加到environment中。在environment中,先加入的property,优先级越高。
所以,后加载的profile,优先级就越高。这也就是为什么上述Loader#initializeProfiles方法中this.profiles.add(null)这行代码的意义,就是为了将没有profile的文件的优先级降到最低。

ConfigFileApplicationListener.Loader#getSearchLocations()
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;
}

获取文件父路径

  1. spring.config.location强制指定文件路径,只能从这个路径下面寻找文件
  2. spring.config.additional-location额外的文件查找路径
  3. 默认了四个路径:classpath:/,classpath:/config/,file:./,file:./config/,在asResolvedSet方法中将顺序颠倒了。
  4. locations中可配置文件父路径,也可能是文件的绝对路径。
ConfigFileApplicationListener.Loader#load()
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));
	});
}
  1. 先遍历所有的locations,如果location为文件,那么就直接通过location加载配置。如果为文件夹,那么就查询获取文件名称,通过文件夹+文件名称去加载文件。
  2. getSearchNames() : 获取文件名称,可通过spring.config.name配置文件名称,没有配置则使用"application"。这也解释了SpringBoot启动的时候,为什么回去加载application.yml文件。
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);
}
ConfigFileApplicationListener.Loader#load()
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;
			}
		}
		throw new IllegalStateException("File extension of config file location '" + location
				+ "' is not known to any PropertySourceLoader. If the location is meant to reference "
				+ "a directory, it must end in '/'");
	}
	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);
			}
		}
	}
}

location可能为文件的全路径(spring.config.additional-location配置),为全路径则文件名称name为null。分成了两种方式加载文件,其实两种方式的逻辑是一样的。我们关注下半部分location + name, “.” + fileExtension加载方式。
在这里插入图片描述
先使用Properties加载器,在使用Yaml加载器。这就是为什么properties文件优先级高于yaml文件的原因。

ConfigFileApplicationListener.Loader#loadForFileExtension

通过文件的扩展名称加载

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) {
		// 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);
			}
		}
	}
	// Also try the profile-specific section (if any) of the normal file
	load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}
  1. prefix为文件夹路径+文件名称,比如:file:./config/application
  2. 如果profile不为空,那么就使用prefix + “-” + profile + fileExtension加载,比如:file:./config/application-dev.yml。
  3. 最后,不管profile是否为空,都会通过prefix + fileExtension加载,比如:file:./config/application.yml。
ConfigFileApplicationListener.Loader#load
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;
		}
		// 读取配置文件中配置,转换成Document
		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<>();
		// 添加新的active和include的profile
		for (Document document : documents) {
			if (filter.match(document)) {
				addActiveProfiles(document.getActiveProfiles());
				addIncludedProfiles(document.getIncludeProfiles());
				loaded.add(document);
			}
		}
		// 将此次加载的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);
	}
}
  1. 通过一系列的校验,比如文件资源、文件扩展名是否存在。当校验都通过了,才会去加载资源。
  2. loadDocuments方法读取配置文件中的信息,封装成了Document返回。虽然返回的是List,实际上List中只有一个元素,因为每次只会加载一个资源文件。可能是Spring为了扩展,而返回List吧。
private List<Document> loadDocuments(PropertySourceLoader loader, String name, Resource resource)
		throws IOException {
	DocumentsCacheKey cacheKey = new DocumentsCacheKey(loader, resource);
	List<Document> documents = this.loadDocumentsCache.get(cacheKey);
	if (documents == null) {
		List<PropertySource<?>> loaded = loader.load(name, resource);
		documents = asDocuments(loaded);
		this.loadDocumentsCache.put(cacheKey, documents);
	}
	return documents;
}

加载资源文件,根据文件的扩展名,回调了对应的PropertiesPropertySourceLoader#load、YamlPropertySourceLoader#load。

  1. resourceLoader.getResource获取文件资源支持通过URL获取。DefaultResourceLoader#getResource。
@Override
public Resource getResource(String location) {
	Assert.notNull(location, "Location must not be null");

	for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
		Resource resource = protocolResolver.resolve(location, this);
		if (resource != null) {
			return resource;
		}
	}

	if (location.startsWith("/")) {
		return getResourceByPath(location);
	}
	else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
		return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
	}
	else {
		try {
			// Try to parse the location as a URL...
			URL url = new URL(location);
			return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
		}
		catch (MalformedURLException ex) {
			// No URL -> resolve as resource path.
			return getResourceByPath(location);
		}
	}
}
  1. asDocuments,将加载到的资源封装成Document
private List<Document> asDocuments(List<PropertySource<?>> loaded) {
	if (loaded == null) {
		return Collections.emptyList();
	}
	return loaded.stream().map((propertySource) -> {
		Binder binder = new Binder(ConfigurationPropertySources.from(propertySource),
				this.placeholdersResolver);
		return new Document(propertySource, binder.bind("spring.profiles", STRING_ARRAY).orElse(null),
				getProfiles(binder, ACTIVE_PROFILES_PROPERTY), getProfiles(binder, INCLUDE_PROFILES_PROPERTY));
	}).collect(Collectors.toList());
}

通过binder将propertySource中的spring.profiles.active和spring.profiles.include解析成String数组,分别绑定到了Document对象的activeProfiles和includeProfiles属性,以便后面使用。
6. 遍历所有的Document对象,实际上只有一个Document,因为文件资源只有一个。将Document中的activeProfiles和includeProfiles重新加入到profiles中。因为最外面第一层Loader#load()中正在遍历profiles,下次循环会重新加载后续的profile。
7. loaded.forEach((document) -> consumer.accept(profile, document));

配置文件加载顺序总结

  1. 遍历profile。假如启动脚本中有spring.profiles.active、spring.profiles.include的profile为dev,则遍历null、dev。现进先出,先加载为null的profile。越先加载的profile,优先级越低。
  2. 遍历location。相关配置:spring.config.location、spring.config.additional-location,默认配置为classpath:/,classpath:/config/,file:./,file:./config/。注意:会先将location的顺序颠倒,再去加载。
  3. 遍历文件名称。相关配置spring.config.name。默认application。
  4. 判断文件名称是否为空。如果location不是文件夹(不以“/”结尾,那么就认为不是文件夹),则使用location去加载文件。否则就拼接文件名称加载。
  5. 遍历文件扩展名。先遍历资源加载器,每一个资源加载器都支持不同的文件扩展名。PropertiesPropertySourceLoader支持properties、xml,YamlPropertySourceLoader支持yml、yaml。
  6. 最终将加载到的所有信息放入到ConfigFileApplicationListener.Loader#loaded。loaded是LinkedHashMap,key为profile,value为对应的文件名称加载是所有资源propertySource。最终会颠倒profile加载的顺序,将propertySource放入到environment中。
  7. 放入environment的先后顺序决定了取配置的优先级,越先加入到environment中的propertySource,优先级越高。

问题:为什么先加入到environment中的propertySource,优先级越高?

environment#getProperty()获取属性key所对应的值。调用链路如下

AbstractEnvironment#getProperty(java.lang.String)

public String getProperty(String key) {
	return this.propertyResolver.getProperty(key);
}

org.springframework.core.env.PropertySourcesPropertyResolver#getProperty(java.lang.String)

public String getProperty(String key) {
	return getProperty(key, String.class, true);
}

PropertySourcesPropertyResolver#getProperty(java.lang.String, java.lang.Class, boolean)

protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
	if (this.propertySources != null) {
		for (PropertySource<?> propertySource : this.propertySources) {
			if (logger.isTraceEnabled()) {
				logger.trace("Searching for key '" + key + "' in PropertySource '" +
						propertySource.getName() + "'");
			}
			Object value = propertySource.getProperty(key);
			if (value != null) {
				if (resolveNestedPlaceholders && value instanceof String) {
					value = resolveNestedPlaceholders((String) value);
				}
				logKeyFound(key, propertySource, value);
				return convertValueIfNecessary(value, targetValueType);
			}
		}
	}
	if (logger.isTraceEnabled()) {
		logger.trace("Could not find key '" + key + "' in any property source");
	}
	return null;
}

我们可以很清晰的看到,遍历了propertySources,从propertySource取到不为null的值。解析占位符、转换值类型之后,就返回了。

遗留问题:

  1. bootstrap.yml加载逻辑BootstrapApplicationListener。
  2. nacos中的配置文件如何加载到的?getSearchLocations中使用URL协议吗?nacos源码研究。
[Dify 实战] 如何在内网环境部署 Dify 应用:彻底离线的企业级方案解析 企业内网环境如何离线部署Dify?本文提供详细教程:Dify作为开源AI平台,支持完全离线部署,无需外网访问。文中介绍两种部署方式:推荐使用Docker一键部署,也可选择源码安装定制化方案。重点讲解内网模型配置(支持Ollama/GPT-OSS等)、知识库存储(MinIO/PostgreSQL)及内网代理设置,并给出常见问题排查方法。该方案适用于金融、政府等对数据安全要求高的场景,确保敏感数据不泄露,实现完全本地化的AI应用闭环。 阅读详情

相关推荐

Tesseract in Java - Intellij IDEA对Tess4J的配置和使用

最近要弄OCR,搜到这个,官网全英文,很烦,看看有没有中文的博客介绍,擦,一个比一个奇葩,还是自己按照官方的来吧,外网慢的很,传到csdn给大家下载。中文训练库下载地址: http://download.youkuaiyun.com/detail/d_dmelon/9916532Tess4J3.4下载地址: http://download.youkuaiyun.com/detail/d_dmelon/9916519安装

Lewis的博客 1万+

SpringBoot源码(七): ConfigFileApplicationListener

介绍 configFileApplicationListener是spring中对于配置文件解析的监听器,主要是负责对于配置文件的解析,并且加入到environment中去。 源码 在上一章中prepareEnvironment方法,会发送广播ApplicationEnvironmentPreparedEvent事件到广播,然后各个监听器接收处理。对于如何设别到监听器就不多解释了,之前也介绍过。别的监听器暂时先不看,先看最重要的configFileApplicationListener。直接看C.

xby7437的博客 2730

Jetson Orin Nano GPIO 实战,用 devmem 玩转 Pinmux 引脚复用配置

devmem 是一个直接读写寄存器的工具,通过 mmap 将 /dev/mem 中的物理地址区域映射到用户空间进程地址空间,从而实现直接读写,正常情况下在 /dev 下可以看到 mem。可以看到对应 bit5 bit6 分别为 1,对应配置为 E_IO_HV 为 ENABLE 以及 E_INPUT 为 ENABLE。需要 TRM 手册查看相关寄存器,以及安装 busybox,busybox 内置了 devmem。根据 TRM 手册,找到 I2C_SDA 的 pinmux 地址偏移是 0x18。

darcsdn 1643

死磕源码系列【ConfigFileApplicationListener监听器源码解析】

ConfigFileApplicationListener监听器主要用来处理环境配置相关业务,其加载是在springboot启动时通过SPI方式获取(之前的文章已经将结果); ConfigFileApplicationListener监听器类继承SmartApplicationListener接口实现了supportsEventType方法,判定监听器实际上支持的时间类型: @Override public boolean supportsEventType(Class<? extends A.

OceanSky的专栏 1991

(四)美赛画图篇:画图模板+画图工具

美赛画图篇:画图模板+画图工具(一)画图模板(二)画图工具 因为散点图、折线图等多与数据有关,且需相关软件生成,故此处多是通用模板。直接替换文字内容即可。 (一)画图模板 1、Visio圆圈图 适合于:指标体系(E题常用) 点击下载:1 C币 如果不想花费且时间充足,悄咪咪地说,在visio-模板搜索可以找到很多。(稍微会耗一点时间) 2、简单指标体系图PPT 适合于指标体系 点击下载:0 C币...

weixin_43689393的博客 1万+

【用unity实现100个游戏之19】制作一个3D传送门游戏,实现类似鬼打墙,迷宫,镜子,任意门效果(附项目源码)

本文介绍了Unity中实现传送门效果的完整流程。通过第一人称角色控制器实现移动,使用PortalView脚本和自定义Unlit Shader渲染门内场景,巧妙利用相机位置和旋转同步实现动态视角。重点解决了近裁剪面问题,确保远距离渲染正确性。最后通过PortalTeleporter脚本实现传送功能,检测碰撞并计算相对位置进行瞬移。该方法无需复杂插件,代码简洁高效,适用于各类3D场景中的传送门实现。

向宇的博客,专注php/web全栈 unity游戏开发,欢迎大家评论纠错 1万+

Spring Boot源码之旅五十二ConfigFileApplicationListener配置文件加载原理一

作者简介:大家好,我是哥,前中兴通讯、美团架构师,现某互联网公司联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬学习必须往深处挖,挖的越深,基础越扎实!

smart_an的专栏 1112

3.1 快速启动Flink集群

在本实战中,我们将快速启动Apache Flink 1.13.0集群,并在Hadoop集群环境中提交作业。首先,我们会在本地下载Flink安装包并上传至云主机,解压并配置环境变量。接着,我们将启动Flink集群,通过访问Web UI进行监控管理。然后,我们会配置集群,将Flink分发到从节点,并在集群上提交作业。最后,我们将通过命令行提交作业,并在Web UI上查看任务节点的输出结果。这个过程涵盖了Flink集群的部署、配置和作业提交,为大数据处理提供了一个完整的实战经验。

howard2005的专栏 727

ConfigFileApplicationListener 触发 ApplicationEnvironmentPreparedEvent 事件执行逻辑详解

跟着Spring Boot项目启动过程,让我们了解下在环境准备阶段,ConfigFileApplicationListener 触发 ApplicationEnvironmentPreparedEvent 事件具体做了哪些事?

若千年后 1233

2. Android_逆向的历史与发展

学完本节,希望你能够:了解 Android 逆向工程的发展历程,从早期 APK 破解到现代应用保护;入门先把这几类「在干啥」搞清楚,后面学脱壳、反反调试会顺很多。(反编译 APK 得到的类汇编代码,相当于程序的「菜谱」)就能绕过验证,到后来混淆、加固、反调试一起上,逆向难度越来越高。(第三节 什么是 CPU 指令集),帮你搞清楚「反汇编看到的那些指令」是从哪来的、和 Android 逆向有啥关系。有必要——先搞清「没加固时怎么玩」,再学脱壳和对抗,思路才顺。读起来费劲,但逻辑还在,耐心一点还能跟。

长沙火山 1389

对雷达中相位补偿概念的一些理解

基于多普勒相位补偿的速度扩展方法和DBF测角,两者本质上都是对相位进行补偿。

qq_42235129的博客 4030

SpringBoot 源码分析 - ConfigFileApplicationListener配置文件加载原理

是Spring Boot中负责处理配置文件加载的关键组件,它是Spring框架接口的一个实现,主要关注于事件,这个事件在Spring Application的初始化阶段发生,此时环境已经准备就绪但尚未刷新上下文。

qq_33240556的博客 529

EMB电子机械制动器夹紧力预估策略分析

本文探讨了电子机械制动器(EMB)夹紧力的四种预估方法:1)基于电机电流的方法简单经济但易受干扰;2)基于位置传感器的方法实时性好但依赖模型精度;3)直接力测量精度最高但成本较高;同时提出未来将结合AI和数字孪生技术提升预估精度。研究显示,准确的夹紧力预估对制动安全性、能效和舒适性至关重要,当前面临温度稳定性、参数漂移等技术挑战。随着智能算法的发展,EMB夹紧力预估将实现更高精度和可靠性。

m0_59012156的博客 849

SpringBoot源码简读——4.1 补充-ConfigFileApplicationListener

之前在学习监听器的时候知道了ConfigFileApplicationListener接口会进行配置的处理 ConfigFileApplicationListener 此监听器对配置的处理主要在ApplicationEnvironmentPreparedEvent事件阶段 onApplicationEnvironmentPreparedEvent 对在ApplicationEnviron...

大风的博客 3260

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

【SpringBoot 从入门到架构师】第20章:SpringBoot核心源码解析

├─ 创建 BootstrapContext├─ 发布 ApplicationStartingEvent├─ 准备 Environment(加载配置)├─ 发布 ApplicationEnvironmentPreparedEvent├─ 创建 ApplicationContext├─ 准备 Context(加载 Bean 定义)├─ 发布 ApplicationPreparedEvent├─ refreshContext() // Spring 容器核心初始化。

gaochufukanyongren的博客 773

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

SpringBoot源码解析:Environment加载配置原理(1)

本文是基于springboot2.1.6源码解析 本篇主要是介绍,spring怎么加载解析配置文件到environment中。至于怎么获取environment中配置,怎么绑定environment到对象中,另开篇再介绍。不想看源码,直接看总结 1. SpringApplication介绍 SpringApplication的run方法 public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch =.

Java动物园的专栏 2104

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

springboot 2.x源码笔记- 配置文件加载 ConfigFileApplicationListener

本人博客原地址:springboot 2.x源码笔记- 配置文件加载 ConfigFileApplicationListener 创作时间: 2019.06.12 18:14:06 springboot的源码(spring)主要分为几个部分 1、构造SpringApplication,完成spring.factories文件中Initializers与Listeners的加载 2、加载配置文件,通过ConfigFileApplicationListener 3、加载BeanDefinitionRegist

qq_22351805的博客 1069

Springboot源码解析之Environment

前言:springboot近些年已经逐渐成为新项目的必备框架。Springboot可以只使用几行代码就可以帮我们搭建一个可运行的项目框架。Springboot的自定义配置也是非常简单,只需要遵循规范在application.yml或application.properties中添加配置,就可以在代码中使用@Value获取到配置。那么springboot就的合适加载的自定义配置?自定义配置又是存储在哪里的呢? Springboot的启动类很简单,一行代码就搞定。 @SpringBootApplicati

朱守成 552

SpringBoot源码解析(七):Environment属性加载与Profile激活原理

SpringBoot源码解析(七):Environment属性加载与Profile激活原理

我从不打没有准备的仗! 9828

SpringBoot源码之属性文件加载原理剖析

本文详细讲解SpringBoot中application.properties属性文件加载的原理分析

波波烤鸭的博客 14万+

Spring Boot 2.2.6 源码之旅五十ConfigFileApplicationListener配置文件加载原理一

Spring Boot 2.2.6 源码之旅五十ConfigFileApplicationListener配置文件加载原理一配置文件加载原理ConfigFileApplicationListener何时加载配置文件ConfigFileApplicationListener的onApplicationEnvironmentPreparedEventloadPostProcessorsEnvironm...

王伟王胖胖的博客 1580

Spring Boot源码之旅五十四ConfigFileApplicationListener配置文件加载原理三

作者简介:大家好,我是哥,前中兴通讯、美团架构师,现某互联网公司联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬学习必须往深处挖,挖的越深,基础越扎实!

smart_an的专栏 1006

Springboot内置ApplicationListener--ConfigFileApplicationListener

源码分析 本文源代码基于 Springboot 2.1.0 package org.springframework.boot.context.config; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import ...

Details Inside Spring 4373

Springboot 源码分析 —— prepareEnvironment() 解析

spring 配置属性加载流程分析。

一步一步往上爬 3841

SpringBoot源码解析:从自动装配到核心机制

SpringBoot作为Java生态中广泛使用的框架,其核心机制如自动装配、启动流程和外部化配置是开发者必须掌握的技术要点。自动装配通过条件注解和配置类加载机制实现组件智能注入,大幅简化了Spring应用的配置复杂度。从技术实现来看,SpringBoot利用Spring框架的扩展点,在应用启动时通过Environment准备运行环境,结合内嵌容器实现开箱即用的Web服务能力。这些设计不仅提升了开发效率,也为微服务架构提供了基础设施支持。通过分析自动配置原理和启动过程源码,开发者可以深入理解框架的扩展机制,解

weixin_33670713的博客 343

Spring Boot application 配置文件加载源码解析:从 application.yml 到 PropertySource 的完整链路

本文基于 doocs/source-code-hunter 仓库中的 [SpringBoot-application-load.md](https://link.gitcode.com/i/5611223531975b863dcad0cc03b29319) 一文,以源码追踪的方式还原 Spring Boot 启动时 `application.yml` / `application.properti

gitblog_01146的博客 315
上一篇: SpringBoot扩展篇:循环依赖源码链路
下一篇: 数据结构算法-红黑树
hao9e
博客等级 码龄7年 189粉丝 51原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值