介绍

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();
		}

初始化的流程

  1. 先去查看environment中有没有active的profile,也就是key是spring.profiles.active的值,如果有就加入到profiles的队列中,并且移除default的profile,因为已经指定了profile了,就没有必要再去解析default的了。这边就会用到我们在命令行启动的参数               --spring.profiles.active了。
  2. 如果没有则加入一个default的profile到profiles的队列中
  3. 再加入一个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就可以了。