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;
}
-
getOrCreateEnvironment创建StandardServletEnvironment,所有的启动参数和配置文件信息都会保存到environment中。environment中默认创建了4个propertySource,分别用来存放系统属性和servlet属性。
-
configureEnvironment配置环境信息,此时配置文件还没解析。
-
listeners.environmentPrepared,调用监听器ConfigFileApplicationListener解析配置文件。最终回调了ConfigFileApplicationListener#onApplicationEvent,这里是解析文件的核心逻辑。
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
}
监听器发布的是ApplicationEnvironmentPreparedEvent类型的事件。
- 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);
}
- 第二个参数args为SpringBoot启动参数。
- 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);});
}
- (defaultProperties) -> { … } 是一个函数接口 Consumer,所以需要先看 FilteredPropertySource#apply方法,在apply方法内部回调这个Consumer。
- initializeProfiles:初始化profile。
- profiles.poll先入先出,依次加载profile,后续的配置文件中有profile,也会放入到profiles中。
- 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加载的顺序,先进先出
- 添加一个为null的profile。因为就算用户配置了spring.profiles.active=dev,不仅要加载application-dev.yml文件,application.yml文件也需要被加载。
- 添加不在当前spring.profiles.active和spring.profiles.include范围内的,之前获取到的spring.profiles.active。前期在SpringBoot启动的时候在SpringApplication#configureProfiles方法中就已经获取到了启动参数中的spring.profiles.active。
- 添加spring.profiles.include对应的profile。
- 添加spring.profiles.active对应的profile。
- 如果没有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;
}
获取文件父路径
- spring.config.location强制指定文件路径,只能从这个路径下面寻找文件
- spring.config.additional-location额外的文件查找路径
- 默认了四个路径:classpath:/,classpath:/config/,file:./,file:./config/,在asResolvedSet方法中将顺序颠倒了。
- 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));});
}
- 先遍历所有的locations,如果location为文件,那么就直接通过location加载配置。如果为文件夹,那么就查询获取文件名称,通过文件夹+文件名称去加载文件。
- 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);
}
- prefix为文件夹路径+文件名称,比如:file:./config/application
- 如果profile不为空,那么就使用prefix + “-” + profile + fileExtension加载,比如:file:./config/application-dev.yml。
- 最后,不管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);}
}
- 通过一系列的校验,比如文件资源、文件扩展名是否存在。当校验都通过了,才会去加载资源。
- 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。
- 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);}}
}
- 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));
配置文件加载顺序总结
- 遍历profile。假如启动脚本中有spring.profiles.active、spring.profiles.include的profile为dev,则遍历null、dev。现进先出,先加载为null的profile。越先加载的profile,优先级越低。
- 遍历location。相关配置:spring.config.location、spring.config.additional-location,默认配置为classpath:/,classpath:/config/,file:./,file:./config/。注意:会先将location的顺序颠倒,再去加载。
- 遍历文件名称。相关配置spring.config.name。默认application。
- 判断文件名称是否为空。如果location不是文件夹(不以“/”结尾,那么就认为不是文件夹),则使用location去加载文件。否则就拼接文件名称加载。
- 遍历文件扩展名。先遍历资源加载器,每一个资源加载器都支持不同的文件扩展名。PropertiesPropertySourceLoader支持properties、xml,YamlPropertySourceLoader支持yml、yaml。
- 最终将加载到的所有信息放入到ConfigFileApplicationListener.Loader#loaded。loaded是LinkedHashMap,key为profile,value为对应的文件名称加载是所有资源propertySource。最终会颠倒profile加载的顺序,将propertySource放入到environment中。
- 放入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的值。解析占位符、转换值类型之后,就返回了。
遗留问题:
- bootstrap.yml加载逻辑BootstrapApplicationListener。
- nacos中的配置文件如何加载到的?getSearchLocations中使用URL协议吗?nacos源码研究。
相关文章:
SpringBoot 扩展篇:ConfigFileApplicationListener源码解析
SpringBoot 扩展篇:ConfigFileApplicationListener源码解析 1.概述2. ConfigFileApplicationListener定义3. ConfigFileApplicationListener回调链路3.1 SpringApplication#run3.2 SpringApplication#prepareEnvironment3.3 配置environment 4. 环境准备事件 Config…...
蓝桥杯省三爆改省二,省一到底做错了什么?
到底怎么个事 这届蓝桥杯选的软件测试赛道,都说选择大于努力,软件测试一不卷二不难。省赛结束,自己就感觉稳啦,全部都稳啦。没想到一出结果,省三,g了。说落差,是真的有一点,就感觉和自己预期的…...
Unity EventSystem入门
概述 相信在学习Unity中,一定有被UI事件困扰的时候把,当添加UICanvas的时候,Unity会为我们自动添加EventSystem,这个是为什么呢,Unity的UI事件是如何处理的呢,在使用各个UI组件的时候,一定有不…...
第4章 Vim编辑器与Shell命令脚本
第4章 Vim编辑器与Shell命令脚本 1. Vim文本编辑器2. 编写Shell脚本2.2 接收用户的参数2.3 判断用户的参数 3. 流程控制语句3.1 if条件测试语句3.2 for条件循环语句3.3 while条件循环语句3.4 case条件测试语句 4. 计划任务服务程序复习题 1. Vim文本编辑器 Vim编辑器中设置了三…...
javaWeb快速部署到tomcat阿里云服务器
目录 准备 关闭防火墙 配置阿里云安全组 点击控制台 点击导航栏按钮 点击云服务器ECS 点击安全组 点击管理规则 点击手动添加 设置完成 配置web服务 使用yum安装heepd服务 启动httpd服务 查看信息 部署java通过Maven打包好的war包项目 Maven打包项目 上传项目 …...
[MQTT]Mosquitto的內網連接(intranet)和使用者/密碼權限設置
[MQTT | Raspberry Pi]Publish and Subscribe with RSSI Data of Esp32 on Intranet 延續[MQTT]Mosquitto的簡介、安裝與連接測試文章,接著將繼續測試在內網的兩台機器是否也可以完成發佈和訂閱作業。 同一網段的兩台電腦測試: 假設兩台電腦的配置如下: A電腦為發…...
某盾BLACKBOX逆向关键点
需要准备的东西: 1、原JS码 2、AST解混淆码 3、token(来源于JSON) 一、原JS码很好获取,每次页面刷新,混淆的代码都会变,这是正常,以下为部分代码 while (Qooo0) {switch (Qooo0) {case 110 14 - 55: {function O0…...
【2024全国青少年信息素养大赛初赛时间以及模拟题】
2024全国青少年信息素养大赛时间已经出来了 目录 全国青少年信息素养大赛智能算法挑战赛初中模拟卷 全国青少年信息素养大赛智能算法挑战赛初中模拟卷 1、比赛时间和考试内容: 算法创意实践挑战赛初中组于5月19日举行,检录时间为10:30-11:00…...
2024年软件测试最全jmeter做接口压力测试_jmeter接口性能测试_jmeter压测接口(3),【大牛疯狂教学
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上软件测试知识点,真正体系化! 由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、…...
LLM——用于微调预训练大型语言模型(LLM)的GPU内存优化与微调
前言 GPT-4、Bloom 和 LLaMA 等大型语言模型(LLM)通过扩展至数十亿参数,实现了卓越的性能。然而,这些模型因其庞大的内存需求,在部署进行推理或微调时面临挑战。这里将探讨关于内存的优化技术,旨在估计并优…...
Telnet协议:远程控制的基石
目录 1. 概述 2. 工作机制 3. 网络虚拟终端 4. 选项协商 5. 操作方式 6. 用户接口命令 7. 验证的过程 1. 概述 Telnet(Telecommunication Network)是一种用于在互联网上远程登录到计算机系统的标准协议。它早期被广泛用于远程终端连接࿰…...
网络工程师必备:静态路由实验指南
大家好,这里是G-LAB IT实验室。今天带大家学习一下华为静态路由实验配置 01、实验拓扑 02、实验需求 1.R1环回口11,1,1.1模拟PC1 2.R2建立2个环回口模拟Server server-1: 22,1,1.1 server-2: 44.1.1.1 3.要求使用静态路由实现全网互通 PC1去往server-1从R3走…...
springboot利用切面保存操作日志(支持Spring表达式语言(简称SpEL))
springboot利用切面保存操作日志(支持Spring表达式语言(简称SpEL)) 文章目录 springboot利用切面保存操作日志(支持Spring表达式语言(简称SpEL))前言一、Spring EL是什么?…...
遂宁专业知识付费系统报价,免费网课平台怎么开通?需要哪些条件?
其实,不少的大咖老师都不愿意在大平台上开课,因为学员的留存并不是自己的,所以,很多人也考虑自己开通网课平台,那免费的平台怎么开通?这就是我们今天要跟老师们分享的内容了。 需要哪些条件? 大家如果想要开通免费的…...
【linuxC语言】fcntl和ioctl函数
文章目录 前言一、功能介绍二、具体使用2.1 fcntl函数2.2 ioctl函数三、拓展:填写arg总结前言 在Linux系统编程中,经常会涉及到对文件描述符、套接字以及设备的控制操作。fcntl和ioctl函数就是用来进行这些控制操作的两个重要的系统调用。它们提供了对文件、设备和套接字进行…...
java——继承(一)
一:匿名对象 只能使用一次,每一次使用都会创建一个新的对象,默认值和数组的默认值的规则相同。所以适用于调用一次对象的情况: public class ClassAnonymous {String name;public void show(){System.out.println(name"真厉…...
【Linux】进程间通信方式之管道
🤖个人主页:晚风相伴-CSDN博客 💖如果觉得内容对你有帮助的话,还请给博主一键三连(点赞💜、收藏🧡、关注💚)吧 🙏如果内容有误的话,还望指出&…...
【Linux】yum与vim
文章目录 软件包管理器:yumLinux安装和卸载软件包Linux中的编辑器:vimvim下的底行模式vim下的正常模式vim下的替换模式vim下的视图模式vim下的多线程 软件包管理器:yum yum其实就是一个软件,也可以叫商店 和你手机上的应用商店或app store一…...
苍穹外卖Day06笔记
疯玩了一个月,效率好低,今天开始捡起来苍穹外卖~ 1. 为什么不需要单独引入HttpClient的dependency? 因为我们在sky-common的pom.xml中已经引入了aliyun-sdk-oss的依赖,而这个依赖低层就引入了httpclinet的依赖,根据依…...
Maximo 使用 REST API 创建 Cron Task
接前面几篇文章,我没有了 automation script 以后,有时候需要让其定期自动执行,这时候就可以通过 Cron Task 来实现了。 通过Maximo REST API 来创建 Cron Task request: POST {{base_url}}/api/os/mxapicrontaskdef?apikey{{…...
【镜像仿真篇】磁盘镜像仿真常见错误
【镜像仿真篇】磁盘镜像仿真常见错误 记系统镜像仿真常见错误集—【蘇小沐】 1、实验环境 2023AFS39.E01(Windows11系统镜像)Arsenal Image Mounter,[v3.10.262]Vmware Workstation 17 Pro,[v17.5.1]Windows 11 专业工作站版…...
代码随想录算法训练营DAY45|C++动态规划Part7|70.爬楼梯(进阶版)、322. 零钱兑换、279.完全平方数
文章目录 70.爬楼梯(进阶版)⭐️322. 零钱兑换思路CPP代码总结 279.完全平方数思路CPP代码 70.爬楼梯(进阶版) 卡码网:57. 爬楼梯 文章讲解:70.爬楼梯(进阶版) 本题就是典型了完全背包排列问题,…...
Linux(openEuler、CentOS8)企业内网DHCP服务器搭建(固定Mac获取指定IP)
----本实验环境为openEuler系统<以server方式安装>(CentOS8基本一致,可参考本文)---- 目录 一、知识点二、实验(一)为服务器配置网卡和IP(二)为服务器安装DHCP服务软件(三&a…...
c#读取hex文件方法,相对来说比较清楚
Hex文件解读_c#读取hex文件-CSDN博客 https://wenku.csdn.net/answer/d67f30cf834c435ca37c3d1ef5e78a62?ops_request_misc%257B%2522request%255Fid%2522%253A%2522171498156816800227423661%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&…...
【ytb数据采集器】按关键词批量爬取视频数据,界面软件更适合文科生!
一、背景介绍 1.1 爬取目标 用Python独立开发的爬虫工具,作用是:通过搜索关键词采集油管的搜索结果,包含14个关键字段:关键词,页码,视频标题,视频id,视频链接,发布时间,视频时长,频道名称,频道id,频道链接,播放数,点赞数,评论数…...
三条命令快速配置Hugging Face
大家好啊,我是董董灿。 本文给出一个配置Hugging Face的方法,让你在国内可快速从Hugging Face上下在模型和各种文件。 1. 什么是 Hugging Face Hugging Face 本身是一家科技公司,专注于自然语言处理(NLP)和机器学习…...
Python网络编程 03 实验:FTP详解
文章目录 一、小实验FTP程序需求二、项目文件架构三、服务端1、conf/settings.py2、conf/accounts.cgf3、conf/STATUS_CODE.py4、启动文件 bin/ftp_server.py5、core/main.py6、core/server.py 四、客户端1、conf/STATUS_CODE.py2、bin/ftp_client.py 五、在终端操作示例 一、小…...
个人银行账户管理程序(2)
在(1)的基础上进行改进 1:增加一个静态成员函数total,记录账户总金额和静态成员函数getTotal 2对不需要改变的对象进行const修饰 3多文件实现 account。h文件 #ifndef _ACCOUNT_ #define _ACCOUNT_ class SavingAccount {pri…...
2024.04.19校招 实习 内推 面经
绿*泡*泡VX: neituijunsir 交流*裙 ,内推/实习/校招汇总表格 1、校招&转正实习 | 美团无人机业务部招聘(内推) 校招&转正实习 | 美团无人机业务部招聘(内推) 2、校招&实习 | 快手 这些岗位…...
Python并发编程 04 进程与线程基础
文章目录 一、操作系统简介二、进程三、线程四、线程的调用1、示例2、join方法3、setDaemon方法4、继承式调用(不推荐)5、其他方法 一、操作系统简介 ①操作系统是一个用来协调、管理和控制计算机硬件和软件资源的系统程序,它位于硬件和应用…...
网站开发的标准/seo排名推广工具
去年五六月在一台很老的mac机(2014)上安装过as和flutter环境,对于一个长期用win的安卓开发来说,第一次配置和使用mac真的痛苦。 今天,终于2014的8g机子报废了,公司配了一台2020新版mbp,32gi71t…...
不断完善政府网站建设/谷歌浏览器搜索入口
android下的单元測试在AndroidManifest.xml文件里配置下面信息:在manifest节点下加入:<!-- 指定測试信息和要測试的包 --><instrumentationandroid:name"android.test.InstrumentationTestRunner"android:targetPackage"com.jxn…...
新乡交友网站开发公司/上海发布微信公众号
满意答案uahyiw2020.02.11采纳率:51% 等级:11已帮助:6110人一.关机、睡眠的快捷键:1.先按左下角“windows“键,再按字母“U“键,再按字母“U”或“S”键。2.在桌面状态下按 ALTF4 再按“U”键或者“S”键…...
邢台做网站哪个网络公司好/百度在线使用
sys.ps1、sys.ps2:字符串,指定解释器的首要和次要提示符。仅当解释器处于交互模式时,它们才有定义。这种情况下,它们的初值为 >>> 和 ... 。如果赋给其中某个变量的是非字符串对象,则每次解释器准备读取新的交互式命令时…...
wordpress部署https/廊坊首页霸屏优化
集群配置:1个nsqlookupd, 1个nsqadmin,3个nsqd 分区:1个order-topic,分区数为100,副本数为3 扩容时,新增一个nsqd-4。刚开始,nsqd-4没有任何分区副本。 接下来通过nsqadmin页面发现ÿ…...
网站开发公司属于什么行业/在线资源搜索引擎
版权声明:欢迎转载,请注明沉默王二原创。 https://blog.csdn.net/qing_gee/article/details/43447681 init 6 重启Linux。free -m查看内存大小,已M为单位。df -h查看硬盘情况,包括大小和分区。cat /proc/cpuinfo查看CPU信息。file…...