一个项目有不同的环境,比如开发测试环境、预发布环境、生产环境等,不同环境对应不同的配置,在SpringBoot中最直观的区别就是配置文件。当各监听器执行完应用启动事件之后(ApplicationStartingEvent),后面一步就是确定环境了。
初始化环境
SpringApplication->run():
// 构建环境
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
SpringApplication->prepareEnvironment():
// 构建标准Servlet环境
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
listeners.environmentPrepared(environment);
SpringApplication->configureEnvironment():
configurePropertySources(environment, args); // --1
configureProfiles(environment, args); // --2
SpringApplication->configurePropertySources(): // --1
// 如果有设置成员变量defaultProperties属性值,将其作为属性源添加到末尾
if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
......
}
// 如果有命令行参数配置,将其作为属性源添加到头部
if (this.addCommandLineProperties && args.length > 0) {
......
}
SpringApplication->configureProfiles(): // --2
// 如果有设置成员变量additionalProfiles属性值,将其添加上
Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
// 如果环境变量和系统变量中有设置spring.profiles.active,将其值添加上
profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
// 设置为环境的activeProfiles属性
environment.setActiveProfiles(StringUtils.toStringArray(profiles));
本文所讲的环境即为StandardServletEnvironment类。在实例化类的时候,会给予它默认的四个属性源,其中第三个和第四个分别是环境变量和系统环境变量,会给成员变量defaultProfiles添加默认配置值default。
![]()
图一
假设在没有其它属性源的情况下,获得属性是根据properySourceList的先后顺序来的,并且只要有值就返回,也就是说环境变量中的配置比系统环境变量中的配置优先级更高。
配置文件的加载
创建完环境后,触发监听器调用应用环境准备完毕事件(ApplicationEnvironmentPreparedEvent),重点关注ConfigFileApplicationListener这个监听器。
ConfigFileApplicationListener->addPropertySources():
// 又给属性源集合添加RandomValuePropertySource属性源
RandomValuePropertySource.addToEnvironment(environment);
// 实例化Loader的时候,将会添加下图两个属性源加载类
new Loader(environment, resourceLoader).load();
![]()
图二
ConfigFileApplicationListener->Loader->load():
this.profiles = new LinkedList<>();
// 已经解析过的配置文件
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
// 初始化,此处假设profiles一个是null,一个是default
initializeProfiles();
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
if (profile != null && !profile.isDefaultProfile()) {
// 如果不是default,就添加进环境的activeProfiles中
addProfileToEnvironment(profile.getName());
}
// --3
load(profile, this::getPositiveProfileFilter,
// --4
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
resetEnvironmentProfiles(this.processedProfiles);
load(null, this::getNegativeProfileFilter,
addToLoaded(MutablePropertySources::addFirst, true));
// 将解析到的属性源放到环境的propertySourceList最后
// 添加的时候会将集合倒序
addLoadedPropertySources();
ConfigFileApplicationListener->Loader->load(): // --3
getSearchLocations().forEach((location) -> {
boolean isFolder = location.endsWith("/");
// names默认是application
Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
names.forEach(
(name) -> load(location, name, profile, filterFactory, consumer));
});
ConfigFileApplicationListener->Loader->load():
Set<String> processed = new HashSet<>();
for (PropertySourceLoader loader : this.propertySourceLoaders) {
// properties的扩展名是properties, xml;yaml的扩展名是yml,yaml
for (String fileExtension : loader.getFileExtensions()) {
if (processed.add(fileExtension)) {
loadForFileExtension(loader, location + name, "." + fileExtension,
profile, filterFactory, consumer);
}
}
}
ConfigFileApplicationListener->Loader->loadForFileExtension():
if (profile != null) {
// 加上扩展名再读一次,类似application-file.yml这种
......
}
// 将文件位置+文件名+文件扩展名拼接形成最终的location
load(loader, prefix + fileExtension, profile, profileFilter, consumer);
getSearchLocations()为要查找的文件位置,如下图所示,资源查找时,file:./开头表示当前项目路径下的文件,classpath:/表示当前项目下target/classes路径下的文件
![]()
图三
ConfigFileApplicationListener->Loader->load():
Resource resource = this.resourceLoader.getResource(location);
// 如果resource为空或者resource不存在,说明文件不存在,直接返回
......
// 读取resource文件解析属性值,得到一个属性源
// 然后将文档中的spring.profiles.active给Document的activeProfiles成员变量赋值、spring.profiles.include给includeProfiles成员变量赋值,将属性源给propertySource成员变量赋值
// 最终返回单一的list集合
List<Document> documents = loadDocuments(loader, name, resource);
......
for (Document document : documents) {
if (filter.match(document)) {
// --5 给profiles添加activeProfiles
addActiveProfiles(document.getActiveProfiles());
// 将include的放在profiles最前面
addIncludedProfiles(document.getIncludeProfiles());
loaded.add(document);
}
}
if (!loaded.isEmpty()) {
// --4处的loader添加,key为当前profile,value为包含该属性源的多属性源类对象
loaded.forEach((document) -> consumer.accept(profile, document));
}
ConfigFileApplicationListener->Loader->addActiveProfiles(): // --5
// 没有则直接返回
if (profiles.isEmpty()) {
return;
}
// 第一次进来,activatedProfiles为false
if (this.activatedProfiles) {
return;
}
this.profiles.addAll(profiles);
this.activatedProfiles = true;
// 移除default
removeUnprocessedDefaultProfiles();
spring.profiles.active与spring.profiles.include的区别是,1. 前者只能添加一次,后者可以添加多次;2. 同属性文件下,先解析include,再解析active
![]()
图四
![]()
图五
假如环境配置如图一所示,并且配置文件application-file存在,那么环境变量最终如图五所示
总结
- 初始化时,加上环境变量和系统变量作为属性源,
defaultProfiles属性添加default - 启动前有设置
defaultProperties,将默认配置加到属性源最后,有设置命令行参数,将命令行参数作为属性源加到最前 - 找到属性源中的
spring.profiles.active,添加进环境的activeProfiles变量中 - 启动前有设置
additionalProfiles,添加进环境的activeProfiles变量中并放在最前面 - 监听器执行
ApplicationEnvironmentPreparedEvent事件,其中ConfigFileApplicationListener监听器:- 添加随机值属性源
- 监听器的profiles添加null,找属性源中的
spring.profiles.active和spring.profiles.include- 如果有的话,activatedProfiles设置成true,添加进监听器的profiles中
- 如果没有的话,profiles添加default
- profiles出栈,根据图三的文件位置顺序查找项目中是否有application.properties,application.xml,application.yml,application.yaml
- 文件存在的话,解析文件,查看是否有设置
spring.profiles.active或spring.profiles.include,处理完之后,将文件源属性添加进loaded变量中- 对于active的处理,如果activatedProfiles为true则跳过,否则添加进监听器的profiles中,删除default
- 对于include的处理,添加到监听器profiles最前面
- 文件存在的话,解析文件,查看是否有设置
- profiles出栈,添加进环境的activeProfiles中,查找application-xxx.properties,application-xxx.xml,application-xxx.yml,application-xxx.yaml,重复步骤3.1,直至所有profiles出栈
- 将解析出来的文件属性源倒序排序,属性源没有defaultProperties则添加进环境的属性源集合末尾,否则插入到defaultProperties(确保默认属性在最后)之前
- 其它监听器的操作
- 属性源集合首位添加
ConfigurationPropertySourcesPropertySource(代码见SpringApplication->prepareEnvironment():ConfigurationPropertySources.attach(environment)),将之前的所有属性源放入作为缓存,返回SpringBoot的run方法
举例:
- 属性源properySourceList=[环境变量,系统变量]
- 假设profiles为[null,default],null解析出active=[dev],include=[mail,message],此时profiles=[mail,message,dev],loaded=[application.yml]
- 假设
spring.profiles.active=dev,profiles为[null,dev],null解析出active=[release],include=[mail,message],此时profiles=[mail,message,dev],loaded=[application.yml]
- profiles=[message,dev],application-mail不存在
- profiles=[dev],application-message存在,此时loaded=[application.yml,application-message.yml]
- profiles=[],dev解析出active=[release],include=[db],此时profiles=[db],loaded=[application.yml,application-message.yml,application-dev.yml]
- profiles=[],application-db存在,此时loaded=[application.yml,application-message.yml,application-dev.yml,application-db.yml]
- 最终环境变量的activeProfiles=[mail,message,dev,db],属性源properySourceList=[环境变量,系统变量,application-db.yml,application-dev.yml,application-message.yml,application.yml,defaultProperties]
application.yml是公共配置文件,位于末尾,这也正解释了相同属性在不同文件中,环境配置文件优先的原因了
应用
环境准备完毕后,后面的很多功能到最后都是使用其成员变量或调用其方法,典型的有:
- IoC注入
environment,调用getProperty()方法 - IoC中
@Value给字段赋值,AutowiredAnnotationBeanPostProcessor后置处理器 - 注解
@ConfigurationProperties(prefix = "xxx")给Bean的成员变量赋值,CommonAnnotationBeanPostProcessor后置处理器,大致过程点击此处
附录1:命令行参数
还记得写的第一个Java程序:
![]()
图六
用命令javac Hello.java,然后再java Hello就可以输出了,java Hello是最简单的启动方式,如果复杂点,可以带上参数,如java -Dlang=java Test hello world,这里的-D就是环境变量,可以用System.getProperty(lang)得到,hello world就是命令行参数,此时args[0]=hello,args[1]=world,当然也可以在idea中设置。
![]()
图七 这里的VM options就是环境变量,Program arguments就是命令行参数
附录2:yml解析文件与properties解析文件
yml解析文件
![]()
图八
![]()
图九
以图八的配置为例,可以看到很明显的层级关系,解析yml的本质是一个递归的过程,层级的递进是父节点与子节点的关系。
我们可以把cache-enabled: false和executor-type: reuse当作是一个Pair结构,因此它们的key和value属性都是ScalarNode,configuration的value是一个Pair,因此它的value属性是MappingNode,mybatis-plus在第一层,所以它的key属性是一个KeyScalarNode。
![]()
图十
![]()
图十一
图九的层级在图十用Map结构表示了出来,它是与原来的配置文件最相似的。
深层的key用外层的name补充,最终被转换成图十一的单一key-value的Map集合,使用yml好处是不用重复写key,但是要注意yml书写规范。yml解析配置文件的源代码位于YamlPropertySourceLoader->load()。
properties解析文件
properties解析文件就太简单了,大致为一个字符一个字符读,遇到等于号就认为等于号前面的是key,然后跳过等于号,读到换行符之前就认为前面是value,赋值进map,直到文件读完。properties解析配置文件的源代码位于PropertiesPropertySourceLoader->load()。
![]()
图十二
除了单一value,解析过程中还有变量替换,集合解析等,但无论是yml,还是properties,虽然处理过程不同,最终结果都是一样的