介绍
configFileApplicationListener是spring中对于配置文件解析的监听器,主要是负责对于配置文件的解析,并且加入到environment中去。
源码
在上一章中prepareEnvironment方法,会发送广播ApplicationEnvironmentPreparedEvent事件到广播,然后各个监听器接收处理。对于如何设别到监听器就不多解释了,之前也介绍过。别的监听器暂时先不看,先看最重要的configFileApplicationListener。直接看ConfigFileApplicationListener的源码吧。
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());
}
}
List<EnvironmentPostProcessor> loadPostProcessors() {
return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class,
getClass().getClassLoader());
}
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
addPropertySources(environment, application.getResourceLoader());
}
private void onApplicationPreparedEvent(ApplicationEvent event) {
this.logger.replayTo(ConfigFileApplicationListener.class);
addPostProcessors(((ApplicationPreparedEvent) event).getApplicationContext());
}
protected void addPropertySources(ConfigurableEnvironment environment,
ResourceLoader resourceLoader) {
RandomValuePropertySource.addToEnvironment(environment);
new Loader(environment, resourceLoader).load();
}
先是监听到了事件,然后根据事件来执行相应的逻辑。准备环境变量时候是ApplicationEnvironmentPreparedEvent事件,就看这个事件的onApplicationEnvironmentPreparedEvent逻辑。
先看loadPostProcessors方法,这个方法调用了SpringFactoriesLoader类,对于这个类第一章初始化时候就说过,是从META-INF/spring.factories配置文件中读取配置的类名,然后进行处理。这里呢就是从配置文件中读取实现EnvironmentPostProcessor的类。
然后把当前ConfigFileApplicationListener加入到postProcessors中。因为ConfigFileApplicationListener也实现了EnvironmentPostProcessor接口。接下来排序,然后调用postProcessEnvironment方法。
对于ConfigFileApplicationListener的处理逻辑就一个方法,addPropertySources。这个方法主要有两个:一个把RandomValuePropertySource放入environment中去,另一个就是对配置文件的解析。
整体逻辑比较清晰,也没什么好说的,接下来说ConfigFileApplicationListener如何处理的逻辑。
ConfigFileApplicationListener.Loader
Loader是ConfigFileApplicationListener中的内部核心类,按照上面的调用来看下去。
private final Log logger = ConfigFileApplicationListener.this.logger;
private final ConfigurableEnvironment environment;
private final ResourceLoader resourceLoader;
// 对于配置文件的解析类
private final List<PropertySourceLoader> propertySourceLoaders;
// 一个先进后出的队列,根据profile去解析指定的文件
private Queue<Profile> profiles;
// 已经处理的profile
private List<Profile> processedProfiles;
// 维护一个是否有在profiles中是否activeProfile的状态
private boolean activatedProfiles;
// 解析出来的数据都会以PropertySource的方式存到loaded中
private Map<Profile, MutablePropertySources> loaded;
// 缓存作用
private Map<DocumentsCacheKey, List<Document>> loadDocumentsCache = new HashMap<>();
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
this.environment = environment;
this.resourceLoader = resourceLoader == null ? new DefaultResourceLoader()
: resourceLoader;
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(
PropertySourceLoader.class, getClass().getClassLoader());
}
public void load() {
// 先进后出队列
this.profiles = Collections.asLifoQueue(new LinkedList<Profile>());
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
// 初始化
initializeProfiles();
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
// 解析配置文件加到loaded中
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
load(null, this::getNegativeProfileFilter,
addToLoaded(MutablePropertySources::addFirst, true));
// 加到environment中
addLoadedPropertySources();
}
看下构造函数:就是environment和resourceLoader,其中resourceLoader是spring对于资源的封装,其中就是resource和classloader;propertySourceLoaders的方法调用也是从META-INF/spring.factories调用实现PropertySourceLoader的类。是PropertiesPropertySourceLoader和YamlPropertySourceLoader主要负责对于properties,xml和yml,yaml的解析。
继续看load方法
profiles是一个先进后出的队列。然后初始化profiles。看看初始化的实现。
initializeProfiles
private void initializeProfiles() {
// 初始化activeProfiles
Set<Profile> initialActiveProfiles = initializeActiveProfiles();
// 加入到profiles中
this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
// 如果没有activeProfiles,则加入一个default的profile
if (this.profiles.isEmpty()) {
for (String defaultProfileName : this.environment.getDefaultProfiles()) {
Profile defaultProfile = new Profile(defaultProfileName, true);
if (!this.profiles.contains(defaultProfile)) {
this.profiles.add(defaultProfile);
}
}
}
this.profiles.add(null);
}
private Set<Profile> initializeActiveProfiles() {
if (!this.environment.containsProperty(ACTIVE_PROFILES_PROPERTY)
&& !this.environment.containsProperty(INCLUDE_PROFILES_PROPERTY)) {
return Collections.emptySet();
}
Binder binder = Binder.get(this.environment);
Set<Profile> activeProfiles = new LinkedHashSet<>();
activeProfiles.addAll(getProfiles(binder, ACTIVE_PROFILES_PROPERTY));
activeProfiles.addAll(getProfiles(binder, INCLUDE_PROFILES_PROPERTY));
// 如果有activePorfile,加到profiles中,移除default的profile
maybeActivateProfiles(activeProfiles);
return activeProfiles;
}
private void maybeActivateProfiles(Set<Profile> profiles) {
if (profiles.isEmpty()) {
return;
}
// 如果有activatedProfiles,就不会再加到profiles队列中了,这样就不会再去解析了
if (this.activatedProfiles) {
this.logger.debug("Profiles already activated, '" + profiles
+ "' will not be applied");
return;
}
addProfiles(profiles);
this.logger.debug("Activated profiles "
+ StringUtils.collectionToCommaDelimitedString(profiles));
// 维护一个状态
this.activatedProfiles = true;
removeUnprocessedDefaultProfiles();
}
初始化的流程
- 先去查看environment中有没有active的profile,也就是key是spring.profiles.active的值,如果有就加入到profiles的队列中,并且移除default的profile,因为已经指定了profile了,就没有必要再去解析default的了。这边就会用到我们在命令行启动的参数 --spring.profiles.active了。
- 如果没有则加入一个default的profile到profiles的队列中
- 再加入一个null到队列中,这个null的作用主要是先出,然后在没有任何配置的时候,先去加载application的文件。
这边需要注意的maybeActivateProfiles方法,这里面维护了一个activatedProfiles 状态,在有activeProfile时加入到profiles队列时,这边就不会再加了,因为已经指定了active,其他的就不需要解析了。
接着看上面的load方法,从profiles中拿出一个profile,由于是先进后出,所以此时拿出来是null的profile。然后逻辑在多态的另一个load方法中。接下来看了load方法。
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));
});
}
在这里使用的是函数式编程的方法,就是在传参时候,传的是匿名函数。这样好处就是在调用是时候才会调用,并且对于数据的处理并不限定。意思就是数据我拿到了,但是我并不知道如何处理,处理的逻辑完全交给调用方,你只要传入你的函数,然后按照你的方法来处理数据。所以等下我们还是得回过头去再去看看具体的处理逻辑,现在继续向下看。
getSearchLocations方法就是去查找路径,然后再去getSearchNames查找名称。然后再去调用load方法。先去看看查找路径和名称方法。
getSearchLocations,getSearchNames
public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";
public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";
private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
public static final String CONFIG_NAME_PROPERTY = "spring.config.name";
private static final String DEFAULT_NAMES = "application";
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;
}
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);
}
可以看到如果没有配置spring.active.location那么就会直接从DEFAULT_SEARCH_LOCATIONS中获取,也就是在classpath获取。名称如果没有配置spring.config.name的话,就默认取application。
这也就是为什么我们一般都在resource中设置application中的原因。因为在springboot中主张的是约定大于配置,这边就是配置的约定了。如果我们想改变配置文件的位置或者配置文件的名称,可以去修改spring.active.location和spring.config.name。在启动的参数或者在springApplication的参数中都可以。
接着继续看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);
}
}
}
for (PropertySourceLoader loader : this.propertySourceLoaders) {
for (String fileExtension : loader.getFileExtensions()) {
String prefix = location + name;
fileExtension = "." + fileExtension;
loadForFileExtension(loader, prefix, fileExtension, profile,
filterFactory, consumer);
}
}
}
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) {
// 根据profile不同拼接名称
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 processed
for (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 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);
String description = getDescription(location, resource);
if (profile != null) {
description = description + " for profile " + profile;
}
if (resource == null || !resource.exists()) {
this.logger.trace("Skipped missing config " + description);
return;
}
if (!StringUtils.hasText(
StringUtils.getFilenameExtension(resource.getFilename()))) {
this.logger.trace("Skipped empty config extension " + description);
return;
}
String name = "applicationConfig: [" + location + "]";
List<Document> documents = loadDocuments(loader, name, resource);
if (CollectionUtils.isEmpty(documents)) {
this.logger.trace("Skipped unloaded config " + description);
return;
}
List<Document> loaded = new ArrayList<>();
for (Document document : documents) {
// 如果能匹配
if (filter.match(document)) {
maybeActivateProfiles(document.getActiveProfiles());
addProfiles(document.getIncludeProfiles());
loaded.add(document);
}
}
Collections.reverse(loaded);
if (!loaded.isEmpty()) {
// 处理解析出来的数据
loaded.forEach((document) -> consumer.accept(profile, document));
this.logger.debug("Loaded config file " + description);
}
}
catch (Exception ex) {
throw new IllegalStateException("Failed to load property "
+ "source from location '" + location + "'", ex);
}
}
这边在就是根据propertySourceLoaders解析配置文件了。propertySourceLoaders就是对于properties,xml,yml,yaml的解析。在各自的类PropertiesPropertySourceLoader和YamlPropertySourceLoader中就能看到各自能解析文件的类型了。
接下来就是根据profile不同去解析对应的配置文件了,如果profile是dev,那么就会拼接成application-dev去解析。这边也就是我们常常看到的配置文件的类型。在解析完成之后如果能匹配就加到loaded中。然后就到了我们前面说的函数式编程的地方,这边就开始调用了,回过头看下调用方法。
// 入口loaded方法的调用
addToLoaded(MutablePropertySources::addLast, false)
private DocumentConsumer addToLoaded(
BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,
boolean checkForExisting) {
return (profile, document) -> {
if (checkForExisting) {
for (MutablePropertySources merged : this.loaded.values()) {
if (merged.contains(document.getPropertySource().getName())) {
return;
}
}
}
MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
(k) -> new MutablePropertySources());
addMethod.accept(merged, document.getPropertySource());
};
}
这边调用方法就是如果需要检验,就检验之前的loaded有没有存在需要加载的配置。如果不需要检验,则把profile放到根据名称放到loaded的MutablePropertySources中,如果有就拿到MutablePropertySources加入进去,没有就创建新增。对于MutablePropertySources之前就说过了,propertySource的集合。
然后看最后的加入到environment中去的方法了。
addLoadedPropertySources
private void addLoadedPropertySources() {
MutablePropertySources destination = this.environment.getPropertySources();
String lastAdded = null;
List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
Collections.reverse(loaded);
for (MutablePropertySources sources : loaded) {
for (PropertySource<?> source : sources) {
if (lastAdded == null) {
if (destination.contains(DEFAULT_PROPERTIES)) {
destination.addBefore(DEFAULT_PROPERTIES, source);
}
else {
destination.addLast(source);
}
}
else {
destination.addAfter(lastAdded, source);
}
lastAdded = source.getName();
}
}
}
}
这边要注意一点,有个 Collections.reverse(loaded),它会把之前获取的值翻转下加入到environment中去。这样的话之前先读取的是默认application,然后再读取application-dev就会把翻转下,也就是指定的active会在前面了,这也就是为什么指定的activePorfile会覆盖默认的配置的原因了。
总结
ConfigFileApplicationListener这个类在spring中还是非常重要的,主要就是解析配置文件数据放到environment中去使用,但是在springboot2.0.0中用了很多的函数式编程方式,看起来还是有点难度的,多看看就可以了,记住它核心是干嘛用的,以及我们平时为什么要写application,然后指定spring.profiles.active就可以了。