spring boot配置文件application.properties加载原理解析

spring boot配置文件加载是通过ConfigFileApplicationListener监听器完成的。
先来看一下该类的注释:

复制代码
 * {@link EnvironmentPostProcessor} that configures the context environment by loading
 * properties from well known file locations. By default properties will be loaded from
 * 'application.properties' and/or 'application.yml' files in the following locations:
 * file:./config/
 * file:./
 * classpath:config/
 * classpath:
 * The list is ordered by precedence (properties defined in locations higher in the list
 * override those defined in lower locations).
 * Alternative search locations and names can be specified using
 * {@link #setSearchLocations(String)} and {@link #setSearchNames(String)}.
 * Additional files will also be loaded based on active profiles. For example if a 'web'
 * profile is active 'application-web.properties' and 'application-web.yml' will be
 * considered.
 * The 'spring.config.name' property can be used to specify an alternative name to load
 * and the 'spring.config.location' property can be used to specify alternative search
 * locations or specific files.
复制代码

上面注释的大概意思是说,该类默认加载file: ./config/、file:./、classpath:config/、classpath:路径下的’application.properties’和’application.yml’文件,且这些路径是按照优先级排序的,前面路径下的文件会覆盖后面路径的。可以调用setSearchLocations方法修改上述路径位置,该类也会根据激活的profile加载对应环境的配置文件,属性spring.config.name和spring.config.location也可以用来设置加载配置文件的文件名和路径。
下面详细分析该类加载配置文件的原理。

一、创建ConfigFileApplicationListener

ConfigFileApplicationListener是监听器,实现ApplicationListener接口。我们使用spring boot,需要先创建SpringApplication对象,那么先来看一下SpringApplication类的构造方法:

复制代码
 1     public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
 2         this.resourceLoader = resourceLoader;
 3         Assert.notNull(primarySources, "PrimarySources must not be null");
 4         this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
 5         this.webApplicationType = WebApplicationType.deduceFromClasspath();
 6         setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
 7         //加载ApplicationListener
 8         setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
 9         this.mainApplicationClass = deduceMainApplicationClass();
10     }
复制代码

getSpringFactoriesInstances方法用于从spring.factories文件中加载ApplicationListener实现类。ConfigFileApplicationListener就配置在spring.factories文件中。

二、事件触发加载配置文件

对配置文件加载是通过事件触发的。
spring boot启动过程会发布ApplicationEnvironmentPreparedEvent事件,然后调用ConfigFileApplicationListener.onApplicationEvent方法处理该事件。
下面我们看一下onApplicationEvent方法:

复制代码
 1     public void onApplicationEvent(ApplicationEvent event) {
 2         if (event instanceof ApplicationEnvironmentPreparedEvent) {
 3             //处理ApplicationEnvironmentPreparedEvent事件
 4             onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
 5         }
 6         if (event instanceof ApplicationPreparedEvent) {
 7             onApplicationPreparedEvent(event);
 8         }
 9     }
10     private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
11         //从spring.factories文件加载EnvironmentPostProcessor对象
12         List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
13         //ConfigFileApplicationListener也实现了EnvironmentPostProcessor
14         postProcessors.add(this);
15         //对EnvironmentPostProcessor实现类排序
16         AnnotationAwareOrderComparator.sort(postProcessors);
17         for (EnvironmentPostProcessor postProcessor : postProcessors) {
18             //调用postProcessEnvironment
19             postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
20         }
21     }    
复制代码

从onApplicationEnvironmentPreparedEvent中可以看到接下来将继续调用ConfigFileApplicationListener.postProcessEnvironment方法。

复制代码
1     public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
2         addPropertySources(environment, application.getResourceLoader());
3     }
4     protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
5         //添加与随机数相关的配置源
6         RandomValuePropertySource.addToEnvironment(environment);
7         //Load类最终负责加载配置文件
8         new Loader(environment, resourceLoader).load();
9     }
复制代码

postProcessEnvironment在最后调用了Load类的load方法,该方法便是完成对配置文件的加载。

三、Load类

Loader类是ConfigFileApplicationListener的内部私有类,只有ConfigFileApplicationListener可以创建。下面我们先来看一下该类的构造方法。

复制代码
 1     Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
 2         this.environment = environment;
 3         this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
 4         this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
 5         //从spring.factories文件中加载PropertySourceLoader
 6         //PropertySourceLoader有两个实现类:PropertiesPropertySourceLoader和
 7         //YamlPropertySourceLoader,分别用于加载文件名后缀为properties和yaml的文件
 8         this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
 9                     getClass().getClassLoader());
10     }
复制代码

下面看一下load方法:

复制代码
  1         void load() {
  2             FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
  3                     (defaultProperties) -> {
  4                         this.profiles = new LinkedList<>();
  5                         this.processedProfiles = new LinkedList<>();
  6                         this.activatedProfiles = false;
  7                         this.loaded = new LinkedHashMap<>();
  8                         //initializeProfiles从多个配置源加载设置的profile,
  9                         //配置源可以是:环境变量、启动参数"--"设置、Environment对象设置等
 10                         //可以通过属性名spring.profiles.include或者spring.profiles.active指定profile
 11                         //无论上述配置源没有设置profile,都会在profiles属性中增加null,
 12                         //这是为了保证能首先处理默认的配置文件
 13                         initializeProfiles();
 14                         //遍历profiles
 15                         while (!this.profiles.isEmpty()) {
 16                             Profile profile = this.profiles.poll();
 17                             if (isDefaultProfile(profile)) {
 18                                 addProfileToEnvironment(profile.getName());
 19                             }
 20                             //读取配置文件,下面分析该方法
 21                             load(profile, this::getPositiveProfileFilter,
 22                                     addToLoaded(MutablePropertySources::addLast, false));
 23                             this.processedProfiles.add(profile);
 24                         }
 25                         //读取application.properties配置文件
 26                         //如果application.properties中没有配置spring.profiles属性,那么下面这个方法不会加载任何内容
 27                         load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
 28                         //将配置文件作为配置源添加到Environment对象中
 29                         //以后获取配置可以通过Environment获取
 30                         addLoadedPropertySources();
 31                         //将profile设置到Environment对象中
 32                         applyActiveProfiles(defaultProperties);
 33                     });
 34         }
 35         private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
 36             //getSearchLocations方法获得加载配置文件的路径
 37             //然后遍历这些路径
 38             getSearchLocations().forEach((location) -> {
 39                 boolean isFolder = location.endsWith("/");
 40                 //查找配置文件名,可以通过spring.config.name指定文件名
 41                 //如果没有设置,使用默认名application
 42                 Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
 43                 //下面介绍load方法
 44                 names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
 45             });
 46         }
 47         //获得加载配置文件的路径
 48         //可以通过spring.config.location配置设置路径,如果没有配置,则使用默认
 49         //默认路径由DEFAULT_SEARCH_LOCATIONS指定:
 50         //String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/"
 51         private Set<String> getSearchLocations() {
 52             if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
 53                 return getSearchLocations(CONFIG_LOCATION_PROPERTY);
 54             }
 55             Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
 56             locations.addAll(
 57                     asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
 58             return locations;
 59         }
 60         private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
 61                 DocumentConsumer consumer) {
 62             //下面的if分支默认是不走的,除非我们设置spring.config.name为空或者null
 63             //或者是spring.config.location指定了配置文件的完整路径,也就是入参location的值
 64             if (!StringUtils.hasText(name)) {
 65                 for (PropertySourceLoader loader : this.propertySourceLoaders) {
 66                     //检查配置文件名的后缀是否符合要求,
 67                     //文件名后缀要求是properties、xml、yml或者yaml
 68                     if (canLoadFileExtension(loader, location)) {
 69                         //加载location指定的文件,下面的load方法不做介绍,
 70                         //其原理和下面将要调用的loadForFileExtension方法类似
 71                         load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
 72                         return;
 73                     }
 74                 }
 75                 throw new IllegalStateException("File extension of config file location '" + location
 76                         + "' is not known to any PropertySourceLoader. If the location is meant to reference "
 77                         + "a directory, it must end in '/'");
 78             }
 79             Set<String> processed = new HashSet<>();
 80             //propertySourceLoaders属性是在Load类的构造方法中设置的,可以加载文件后缀为properties、xml、yml或者yaml的文件
 81             for (PropertySourceLoader loader : this.propertySourceLoaders) {
 82                 //fileExtension表示文件名后缀
 83                 for (String fileExtension : loader.getFileExtensions()) {
 84                     if (processed.add(fileExtension)) {
 85                         //将路径、文件名、后缀组合起来形成完成文件名
 86                         loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
 87                                 consumer);
 88                     }
 89                 }
 90             }
 91         }
 92         private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
 93                 Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
 94             DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
 95             DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
 96             if (profile != null) {
 97                 //在文件名上加上profile值,之后调用load方法加载配置文件,入参带有过滤器,可以防止重复加载
 98                 String profileSpecificFile = prefix + "-" + profile + fileExtension;
 99                 load(loader, profileSpecificFile, profile, defaultFilter, consumer);
100                 load(loader, profileSpecificFile, profile, profileFilter, consumer);
101                 for (Profile processedProfile : this.processedProfiles) {
102                     if (processedProfile != null) {
103                         String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
104                         load(loader, previouslyLoaded, profile, profileFilter, consumer);
105                     }
106                 }
107             }
108             //加载不带profile的配置文件
109             load(loader, prefix + fileExtension, profile, profileFilter, consumer);
110         }
111         //加载配置文件
112         private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
113                 DocumentConsumer consumer) {
114             try {
115                 //调用Resource类加载配置文件
116                 Resource resource = this.resourceLoader.getResource(location);
117                 if (resource == null || !resource.exists()) {
118                     if (this.logger.isTraceEnabled()) {
119                         StringBuilder description = getDescription("Skipped missing config ", location, resource,
120                                 profile);
121                         this.logger.trace(description);
122                     }
123                     return;
124                 }
125                 if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
126                     if (this.logger.isTraceEnabled()) {
127                         StringBuilder description = getDescription("Skipped empty config extension ", location,
128                                 resource, profile);
129                         this.logger.trace(description);
130                     }
131                     return;
132                 }
133                 String name = "applicationConfig: [" + location + "]";
134                 //读取配置文件内容,将其封装到Document类中,解析文件内容主要是找到
135                 //配置spring.profiles.active和spring.profiles.include的值
136                 List<Document> documents = loadDocuments(loader, name, resource);
137                 //如果文件没有配置数据,则跳过
138                 if (CollectionUtils.isEmpty(documents)) {
139                     if (this.logger.isTraceEnabled()) {
140                         StringBuilder description = getDescription("Skipped unloaded config ", location, resource,
141                                 profile);
142                         this.logger.trace(description);
143                     }
144                     return;
145                 }
146                 List<Document> loaded = new ArrayList<>();
147                 //遍历配置文件,处理里面配置的profile
148                 for (Document document : documents) {
149                     if (filter.match(document)) {
150                         //将配置文件中配置的spring.profiles.active和
151                         //spring.profiles.include的值写入集合profiles中,
152                         //上层调用方法会读取profiles集合中的值,并读取对应的配置文件
153                         //addActiveProfiles方法只在第一次调用时会起作用,里面有判断
154                         addActiveProfiles(document.getActiveProfiles());
155                         addIncludedProfiles(document.getIncludeProfiles());
156                         loaded.add(document);
157                     }
158                 }
159                 Collections.reverse(loaded);
160                 if (!loaded.isEmpty()) {
161                     loaded.forEach((document) -> consumer.accept(profile, document));
162                     if (this.logger.isDebugEnabled()) {
163                         StringBuilder description = getDescription("Loaded config file ", location, resource, profile);
164                         this.logger.debug(description);
165                     }
166                 }
167             }
168             catch (Exception ex) {
169                 throw new IllegalStateException("Failed to load property source from location '" + location + "'", ex);
170             }
171         }
复制代码

在最后一个load方法中可以看到spring boot通过Resource类加载了配置文件。
用下图梳理一下整个加载流程:

还有一点要注意,如果定义了多个环境文件,同时也通过spring.profiles.active激活了多个环境,那么spring将加载所有激活环境的配置文件,最后加载配置文件的配置会覆盖前面加载的配置。

拓展:

PropertySource的原理

PropertySource 代表 name/value 属性对,常见的如命令行参数、环境变量、properties文件、yaml文件等最终都会转为PropertySource,再提供给应用使用。

由 @ConfigurationProperties 标记的类,其数据源就是PropertySource。当多个PropertySource中存在相同值时,默认从第一个PropertySource中获取。下面是PropertySource的部分常见子类:

下图是 Environment中PropertySources截图,其中OriginTrackedMapPropertySource来自于classpath下的application.yml文件。

如果PropertySource有更新,通过发布 EnvironmentChangeEvent 事件,ConfigurationPropertiesRebinder 会监听该事件,然后利用最新的数据将 @ConfigurationProperties 标记的bean重新绑定一定,从而达到动态更新的效果。

 

转自:https://blog.csdn.net/weixin_38308374/article/details/109566009

 

posted @   Boblim  阅读(2038)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
历史上的今天:
2018-10-17 git cherry-pick合并某个commit
2016-10-17 freetds简介、安装、配置及使用介绍
2016-10-17 [Linux]在终端启动程序关闭终端不退出的方法
点击右上角即可分享
微信分享提示