前言
这一小节,来学习 Spring Profiles。
Profiles 在我们日常工作中,很常用。一般我们用它来区分开发、测试、线上环境。
那么,Spring Boot 是怎么加载它的呢?
Profiles
默认情况
我们最常使用的就是application.yml
了,我们知道 Spring Boot 会自动加载它。
其实,除了 application.yml
之外,Spring Boot 还自动会加载application-default.yml
。
yml
与 properties
格式,Spring Boot 都是支持的。但是在相同情况下,properties
优先于yml
。
另外,我们一般会使用active
属性去激活某个配置文件,在此情况下,application-default
就失效了。
加载过程
我们还是来关注在 SpringApplication#run()
方法中,被调用的 prepareEnvironment
方法。
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// Create and configure the 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;
}
复制代码
经过我们 debug,在listeners.environmentPrepared(environment);
之前,加载了这些属性源:
发送事件之后的属性源:
破案了破案了,我们的配置文件,就是在事件监听机制中被加载到内存中的。
还是经过 debug,我们找到了监听器 ConfigFileApplicationListener
。下面我们主要就看监听器中怎么加载的配置文件。
ConfigFileApplicationListener
// ConfigFileApplicationListener # 172
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());
}
}
复制代码
这里主要就是循环调用postProcessor.postProcessEnvironment()
方法:
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
addPropertySources(environment, application.getResourceLoader());
}
复制代码
继续调用addPropertySources()
方法:
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
RandomValuePropertySource.addToEnvironment(environment);
new Loader(environment, resourceLoader).load();
}
复制代码
激动了,我们看到了关键字 Loader,我们继续进load()
方法:
public void load() {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
initializeProfiles();
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
if (profile != null && !profile.isDefaultProfile()) {
addProfileToEnvironment(profile.getName());
}
load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
resetEnvironmentProfiles(this.processedProfiles);
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
addLoadedPropertySources();
}
复制代码
有点东西,估计这个就是加载配置文件的主要逻辑了。
前面几行逻辑都是初始化,我们从initializeProfiles()
方法开始:
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.
// 这里先添加一个null,和后面的 this.profiles.size() == 1 呼应。
this.profiles.add(null);
// 获取激活的配置属性
Set<Profile> activatedViaProperty = getProfilesActivatedViaProperty();
this.profiles.addAll(getOtherActiveProfiles(activatedViaProperty));
// Any pre-existing active profiles set via property sources (e.g.
// System properties) take precedence over those added in config files.
addActiveProfiles(activatedViaProperty);
if (this.profiles.size() == 1) { // only has null profile
for (String defaultProfileName : this.environment.getDefaultProfiles()) {
Profile defaultProfile = new Profile(defaultProfileName, true);
this.profiles.add(defaultProfile);
}
}
}
复制代码
有一点需要注意,当程序进行到这里的时候,我们配置文件还没被加载进来:
所以这里,我们必然会进入到 if (this.profiles.size() == 1)
的逻辑中,然后在此,加载了default Property
。这就是为什么,Spring Boot 会自动加载 application-default.yml
。
然后,程序继续执行:
while (!this.profiles.isEmpty()) {
// 出栈
Profile profile = this.profiles.poll();
// null && default,profile.get(0) == null 不会进入这里
if (profile != null && !profile.isDefaultProfile()) {
addProfileToEnvironment(profile.getName());
}
// 这里开始是主要逻辑,这里需要注意 addToLoaded(MutablePropertySources::addLast, false) 这个是 Consumer 函数参数。
load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
复制代码
我们进入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));
});
}
复制代码
getSearchLocaltion()
就是加载配置文件存放的地点,默认的有四个:classpath:/,classpath:/config/,file:./,file:./config/ ;另外还会加载spring.config.location
和spring.config.additional-location
的值。
getSearchNames(),则是加载配置文件的前缀,我们使用的都是默认的application
。此外还会加载spring.config.name
属性的值,覆盖掉默认的application
。
最后会执行 names.forEach((name) -> load(location, name, profile, filterFactory, consumer))
:
private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) { ........ 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); } } } }
复制代码
this.propertySourceLoaders
就有意思了:
这就是properties
和yml
文件的加载器。
然后做了一下路径拼接,进入了loadForFileExtension()
方法。我们定位到以prefix=classpath:/application
、fileExtension=.yml
参数进入此方法体的逻辑:
private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
....
.....
// Also try the profile-specific section (if any) of the normal file
// 我们会进入到这个方法中
load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}
复制代码
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
DocumentConsumer consumer) {
try {
// 加载文件,我们的配置文件被加载到内存中了
Resource resource = this.resourceLoader.getResource(location);
.......
.......
// 把配置文件(application.yml)包装成 Document
String name = "applicationConfig: [" + location + "]";
List<Document> documents = loadDocuments(loader, name, resource);
......
List<Document> loaded = new ArrayList<>();
for (Document document : documents) {
if (filter.match(document)) {
// 加载配置文件中配置的 active、include,添加到profiles中
addActiveProfiles(document.getActiveProfiles());
addIncludedProfiles(document.getIncludeProfiles());
loaded.add(document);
}
}
Collections.reverse(loaded);
if (!loaded.isEmpty()) {
// 最后走到这个方法
loaded.forEach((document) -> consumer.accept(profile, document));
.......
}
}
......
}
复制代码
loaded.forEach((document) -> consumer.accept(profile, document))
中的 consumer.accept(profile, document)
,会执行下面这段逻辑:
private DocumentConsumer addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,
boolean checkForExisting) {
return (profile, document) -> {
......
// 添加到 loaded (Map<Profile, MutablePropertySources>) 属性源中
MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
(k) -> new MutablePropertySources());
// 把解析到的属性添加到 MutablePropertySources(属性源) 的最后面
addMethod.accept(merged, document.getPropertySource());
};
}
复制代码
以上就是profiles.get(0)=null
时,所做的事情:在指定的位置,加载指定的配置文件,然后读取其中的属性,包括active、include,然后在把active、include的值,添加到 profiles 中,继续遍历。
全部遍历完成后,就是执行addLoadedPropertySources()
方法了:
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();
}
}
}
}
复制代码
这个方法就比较好理解了,把我们加载的配置文件属性源,都添加都属性源集合中:
如此一来,我们配置的激活某环境下的配置文件,就生效了。
总结
这一小节,学习了我们日常开发中 profiles 的加载原理。
基本上每个项目我们都会用到 profiles ,但是却从来没有探究过。也算了小小地解惑了一次。