SpringBoot 扩展篇:ConfigFileApplicationListener源码解析

SpringBoot 扩展篇:ConfigFileApplicationListener源码解析

    • 1.概述
    • 2. ConfigFileApplicationListener定义
    • 3. ConfigFileApplicationListener回调链路
      • 3.1 SpringApplication#run
      • 3.2 SpringApplication#prepareEnvironment
      • 3.3 配置environment
    • 4. 环境准备事件 ConfigFileApplicationListener#onApplicationEvent
    • 4. 加载配置类
      • 4.1 Loader相关属性介绍
      • 4.2 Loader加载配置文件
        • FilteredPropertySource#apply
        • ConfigFileApplicationListener.Loader#load()
        • ConfigFileApplicationListener.Loader#initializeProfiles
        • Loader#addLoadedPropertySources
        • ConfigFileApplicationListener.Loader#getSearchLocations()
        • ConfigFileApplicationListener.Loader#load()
        • ConfigFileApplicationListener.Loader#load()
        • ConfigFileApplicationListener.Loader#loadForFileExtension
        • ConfigFileApplicationListener.Loader#load
      • 配置文件加载顺序总结
      • 问题:为什么先加入到environment中的propertySource,优先级越高?
      • 遗留问题:

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";@Overridepublic String[] getFileExtensions() {return new String[] { "properties", "xml" };}....
}

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

public class YamlPropertySourceLoader implements PropertySourceLoader {@Overridepublic 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资源,就封装成FilteredPropertySourcepropertySources.replace(propertySourceName, new FilteredPropertySource(original, filteredProperties));try {// 回调Loader类中的Consumer方法operation.accept(original);}finally {// 替换PropertySourcepropertySources.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<>();// 初始化profileinitializeProfiles();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的profilethis.profiles.add(null);// 获取spring.profiles.activeSet<Profile> activatedViaProperty = getProfilesFromProperty(ACTIVE_PROFILES_PROPERTY);// 获取spring.profiles.includeSet<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.activethis.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对应的profilethis.profiles.addAll(includedViaProperty);// 4. 添加spring.profiles.active对应的profileaddActiveProfiles(activatedViaProperty);// 5. 如果没有spring.profiles.active和spring.profiles.include,那么就使用spring.profiles.defaultif (this.profiles.size() == 1) { // only has null profilefor (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 processedfor (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 fileload(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;}// 读取配置文件中配置,转换成DocumentString 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的profilefor (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源码研究。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/9021.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第十篇:数字堡垒:操作系统安全深度解析与实战指南

数字堡垒&#xff1a;操作系统安全深度解析与实战指南 1 *引言 1.1 数字世界的守护者 在遥远的比特海中&#xff0c;有一座名为“操作系统”的数字堡垒&#xff0c;它守护着我们的数据宝藏&#xff0c;确保每一次计算的航行都能安全抵达彼岸。然而&#xff0c;这片海域并非风…

2024年最新【SpringBoot2】开发实用篇-测试_springboot2 test(1),2024年最新2024春招BAT面试真题详解

既有适合小白学习的零基础资料&#xff0c;也有适合3年以上经验的小伙伴深入学习提升的进阶课程&#xff0c;涵盖了95%以上软件测试知识点&#xff0c;真正体系化&#xff01; 由于文件比较多&#xff0c;这里只是将部分目录截图出来&#xff0c;全套包含大厂面经、学习笔记、…

YOLOv5,YOLOv7改进之结合​SOCA

1.SOCA moudle结构图 2,YOLOv5,YOLOv7改进之结合​SOCA 1.配置common.py文件 #SOCA moudle 单幅图像超分辨率 class Covpool(Function):@staticmethoddef forward(ctx, input):x = inputbatchSize = x.data.shape[0]dim = x.data.shape[1]h = x.data.shape[2]w = x.data.sha…

docker Harbor私有仓库部署管理

搭建本地私有仓库&#xff0c;但是本地私有仓库的管理和使用比较麻烦&#xff0c;这个原生的私有仓库并不好用&#xff0c;所以我们采用harbor私有仓库&#xff0c;也叫私服&#xff0c;更加人性化。 一、什么是Harbor Harbor是VWware 公司开源的企业级Docker Registry项…

安卓开发--按键跳转页面,按键按下变色

前面已经介绍了一个空白按键工程的建立以及响应方式&#xff0c;可以参考这里&#xff1a;安卓开发–新建工程&#xff0c;新建虚拟手机&#xff0c;按键事件响应。 安卓开发是页面跳转是基础&#xff01;&#xff01;&#xff01;所以本篇博客介绍利用按键实现页面跳转&#…

C语言自定义数据类型

一.结构体 1.结构体 I.基本格式 struct tag {member-list; }variable-list; II.结构体声明 struct PERSON // 结构体声明 {int age; // 声明成员类型long ss;float weight;char name[25]; } family_member; // 定义结构体变量fa…

day1_slidingWindow

一、滑动窗口模板 // 注意&#xff1a;java 代码由 chatGPT&#x1f916; 根据我的 cpp 代码翻译&#xff0c;旨在帮助不同背景的读者理解算法逻辑。 // 本代码不保证正确性&#xff0c;仅供参考。如有疑惑&#xff0c;可以参照我写的 cpp 代码对比查看。import java.util.Has…

【算法】滑动窗口——水果成篮

本篇博客是我对“水果成篮”这道题由暴力解法到滑动窗口思路的具体思路&#xff0c;有需要借鉴即可。 目录 1.题目2.暴力求解3.暴力优化3.1每次right不用回退3.2有些left长度一定不如前一个&#xff0c;不用走&#xff0c;left不回退 4.滑动窗口算法5.总结 1.题目 题目链接&am…

SpringBoot+vue实现右侧登录昵称展示

目录 1. 定义User数据 1.1.在created方法获取数据 1.2.头部导航栏绑定User数据 1.3.在data中定义User数据 2. 获取数据 2.1.接收父组件传递的值 2.2.展示数据 3. 页面效果 在SpringBoot和 Vue.js 结合的项目中实现右侧登录昵称展示&#xff0c;通常涉及到前端的用户界面…

Linux进程——Linux环境变量

前言&#xff1a;在结束完上一篇的命令行参数时&#xff0c;我们简单的了解了一下Linux中的环境变量PATH&#xff0c;而环境变量不只有PATH&#xff0c;关于更多环境变量的知识我们将在本篇展开&#xff01; 本篇主要内容&#xff1a; 常见的环境变量 获取环境变量的三种方式 本…

新能源汽车动力电池热管理方案直冷方案原理简介

前言 随着新能源汽车的快速发展&#xff0c;动力电池作为其核心部件之一&#xff0c;对于其性能和寿命具有重要影响。动力电池在工作过程中会产生大量的热量&#xff0c;如果不能有效地进行热管理&#xff0c;将会导致电池温度升高、性能下降甚至损坏。因此&#xff0c;热管理…

力扣138. 随机链表的复制

Problem: 138. 随机链表的复制 文章目录 题目描述思路及解法复杂度Code 题目描述 思路及解法 1.创建Map集合Map<Node, Node> map;创建指针cur指向head&#xff1b; 2.遍历链表将cur作为键&#xff0c;new Node(cur.val)作为值&#xff0c;存入map集合&#xff1b; 3.再次…

C语言例题30:将一个正整数分解质因数

#include <stdio.h>void main() {int i;int x;printf("请输入一个正整数&#xff1a;");scanf("%d", &x);printf("%d ", x);//方法一&#xff1a;for (i 2; i < x; i) { //除数&#xff0c;从质数2开始while (x % i 0) { //能…

小程序如何注销

随着移动互联网的深入发展&#xff0c;管控也越来越严格。现在小程序都要求进行ICP备案&#xff0c;不管是新注册的还是以往注册的。很多商家的小程序本身处于无运营状态&#xff0c;现在要求备案&#xff0c;还不如直接注销。下面&#xff0c;将详细介绍小程序注销的步骤和注意…

挖掘线下潜力:Xinstall为App推广开辟新渠道

在移动互联网时代&#xff0c;App的推广成为了企业营销的重要环节。然而&#xff0c;线上推广渠道日益拥堵&#xff0c;成本不断攀升&#xff0c;让许多开发者开始寻找线下推广的新机会。此时&#xff0c;Xinstall作为国内专业的App全渠道统计服务商&#xff0c;为开发者提供了…

中国4月进口以美元计同比增长8.4%,出口同比增长1.5%

中国按美元计4月进出口同比增速均转负为正&#xff0c;双双超预期。 5月9日周四&#xff0c;海关总署公布数据显示&#xff0c;以美元计价&#xff0c;中国2024年4月进口同比增长8.4%至2201亿美元&#xff0c;前值同比下降1.9%&#xff0c;出口同比增长1.5%至2924.5亿美元&…

javaWeb快速部署到tomcat阿里云服务器

目录 准备 关闭防火墙 配置阿里云安全组 点击控制台 点击导航栏按钮 点击云服务器ECS 点击安全组 点击管理规则 点击手动添加 设置完成 配置web服务 使用yum安装heepd服务 启动httpd服务 查看信息 部署java通过Maven打包好的war包项目 Maven打包项目 上传项目 …

西汉两个韩信,结局怎么如此相似

西汉军事家、“汉初三杰”韩信是家喻户晓的人物&#xff0c;同时期还有一个韩信&#xff0c;也是战功赫赫&#xff0c;也被封王&#xff0c;史书为了区别&#xff0c;在后一个韩信名字之间加上一个“王”&#xff0c;称为韩王信。韩信是个光芒万丈的人物&#xff0c;韩王信也是…

WPF容器控件之WrapPanel、布局控件

WrapPanel: 换行panel 子元素进行换行&#xff0c;当子元素的宽度或者高度超出了父元素&#xff0c;才进行换行。高度超出父元素的高度 也会另起一列 属性 Orientation布局方式 实例 <WrapPanel Orientation"Horizontal"><Label>C# 是从 C/C 衍生出来的…

活动回顾 |观测云 AI Agent 探索实践

亚马逊云科技“构建全球化软件和互联网新生态——ISV 行业”论坛上&#xff0c;观测云产品架构师刘锐发表了题为“AI Agent 可观测性探索与实践”的主题演讲&#xff0c;不仅展示了观测云在人工智能领域的前沿技术&#xff0c;更强调了在日益复杂的系统环境中&#xff0c;实现有…