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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 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]在终端启动程序关闭终端不退出的方法