spring boot配置文件application.properties加载原理解析

本文基于spring boot 2.2.0 release版本

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
  • 二、事件触发加载配置文件
  • 三、Load类

一、创建ConfigFileApplicationListener

ConfigFileApplicationListener是监听器,实现ApplicationListener接口。我们使用spring boot,需要先创建SpringApplication对象,那么先来看一下SpringApplication类的构造方法:

	public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
		//加载ApplicationListener
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		this.mainApplicationClass = deduceMainApplicationClass();
	}

getSpringFactoriesInstances方法用于从spring.factories文件中加载ApplicationListener实现类。ConfigFileApplicationListener就配置在spring.factories文件中。
spring boot配置文件application.properties加载原理解析_第1张图片

二、事件触发加载配置文件

对配置文件加载是通过事件触发的。
spring boot启动过程会发布ApplicationEnvironmentPreparedEvent事件,然后调用ConfigFileApplicationListener.onApplicationEvent方法处理该事件。
下面我们看一下onApplicationEvent方法:

	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationEnvironmentPreparedEvent) {
		    //处理ApplicationEnvironmentPreparedEvent事件
			onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
		}
		if (event instanceof ApplicationPreparedEvent) {
			onApplicationPreparedEvent(event);
		}
	}
	private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
		//从spring.factories文件加载EnvironmentPostProcessor对象
		List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
		//ConfigFileApplicationListener也实现了EnvironmentPostProcessor
		postProcessors.add(this);
		//对EnvironmentPostProcessor实现类排序
		AnnotationAwareOrderComparator.sort(postProcessors);
		for (EnvironmentPostProcessor postProcessor : postProcessors) {
		    //调用postProcessEnvironment
			postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
		}
	}	

从onApplicationEnvironmentPreparedEvent中可以看到接下来将继续调用ConfigFileApplicationListener.postProcessEnvironment方法。

	public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
		addPropertySources(environment, application.getResourceLoader());
	}
	protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
	    //添加与随机数相关的配置源
		RandomValuePropertySource.addToEnvironment(environment);
		//Load类最终负责加载配置文件
		new Loader(environment, resourceLoader).load();
	}

postProcessEnvironment在最后调用了Load类的load方法,该方法便是完成对配置文件的加载。

三、Load类

Loader类是ConfigFileApplicationListener的内部私有类,只有ConfigFileApplicationListener可以创建。下面我们先来看一下该类的构造方法。

	Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
		this.environment = environment;
		this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
		this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
		//从spring.factories文件中加载PropertySourceLoader
		//PropertySourceLoader有两个实现类:PropertiesPropertySourceLoader和
		//YamlPropertySourceLoader,分别用于加载文件名后缀为properties和yaml的文件
		this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
					getClass().getClassLoader());
	}

下面看一下load方法:

		void load() {
			FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
					(defaultProperties) -> {
						this.profiles = new LinkedList<>();
						this.processedProfiles = new LinkedList<>();
						this.activatedProfiles = false;
						this.loaded = new LinkedHashMap<>();
						//initializeProfiles从多个配置源加载设置的profile,
						//配置源可以是:环境变量、启动参数"--"设置、Environment对象设置等
						//可以通过属性名spring.profiles.include或者spring.profiles.active指定profile
						//无论上述配置源没有设置profile,都会在profiles属性中增加null,
						//这是为了保证能首先处理默认的配置文件
						initializeProfiles();
						//遍历profiles
						while (!this.profiles.isEmpty()) {
							Profile profile = this.profiles.poll();
							if (isDefaultProfile(profile)) {
								addProfileToEnvironment(profile.getName());
							}
							//读取配置文件,下面分析该方法
							load(profile, this::getPositiveProfileFilter,
									addToLoaded(MutablePropertySources::addLast, false));
							this.processedProfiles.add(profile);
						}
						//读取application.properties配置文件
						//如果application.properties中没有配置spring.profiles属性,那么下面这个方法不会加载任何内容
						load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
						//将配置文件作为配置源添加到Environment对象中
						//以后获取配置可以通过Environment获取
						addLoadedPropertySources();
						//将profile设置到Environment对象中
						applyActiveProfiles(defaultProperties);
					});
		}
		private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
		    //getSearchLocations方法获得加载配置文件的路径
		    //然后遍历这些路径
			getSearchLocations().forEach((location) -> {
				boolean isFolder = location.endsWith("/");
				//查找配置文件名,可以通过spring.config.name指定文件名
				//如果没有设置,使用默认名application
				Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
				//下面介绍load方法
				names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
			});
		}
		//获得加载配置文件的路径
		//可以通过spring.config.location配置设置路径,如果没有配置,则使用默认
		//默认路径由DEFAULT_SEARCH_LOCATIONS指定:
		//String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/"
		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 void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
				DocumentConsumer consumer) {
			//下面的if分支默认是不走的,除非我们设置spring.config.name为空或者null
			//或者是spring.config.location指定了配置文件的完整路径,也就是入参location的值
			if (!StringUtils.hasText(name)) {
				for (PropertySourceLoader loader : this.propertySourceLoaders) {
					//检查配置文件名的后缀是否符合要求,
					//文件名后缀要求是properties、xml、yml或者yaml
					if (canLoadFileExtension(loader, location)) {
					    //加载location指定的文件,下面的load方法不做介绍,
					    //其原理和下面将要调用的loadForFileExtension方法类似
						load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
						return;
					}
				}
				throw new IllegalStateException("File extension of config file location '" + location
						+ "' is not known to any PropertySourceLoader. If the location is meant to reference "
						+ "a directory, it must end in '/'");
			}
			Set<String> processed = new HashSet<>();
			//propertySourceLoaders属性是在Load类的构造方法中设置的,可以加载文件后缀为properties、xml、yml或者yaml的文件
			for (PropertySourceLoader loader : this.propertySourceLoaders) {
			    //fileExtension表示文件名后缀
				for (String fileExtension : loader.getFileExtensions()) {
					if (processed.add(fileExtension)) {
					    //将路径、文件名、后缀组合起来形成完成文件名
						loadForFileExtension(loader, location + name, "." + 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值,之后调用load方法加载配置文件,入参带有过滤器,可以防止重复加载
				String profileSpecificFile = prefix + "-" + profile + fileExtension;
				load(loader, profileSpecificFile, profile, defaultFilter, consumer);
				load(loader, profileSpecificFile, profile, profileFilter, consumer);
				for (Profile processedProfile : this.processedProfiles) {
					if (processedProfile != null) {
						String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
						load(loader, previouslyLoaded, profile, profileFilter, consumer);
					}
				}
			}
			//加载不带profile的配置文件
			load(loader, prefix + fileExtension, profile, profileFilter, consumer);
		}
		//加载配置文件
		private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
				DocumentConsumer consumer) {
			try {
			    //调用Resource类加载配置文件
				Resource resource = this.resourceLoader.getResource(location);
				if (resource == null || !resource.exists()) {
					if (this.logger.isTraceEnabled()) {
						StringBuilder description = getDescription("Skipped missing config ", location, resource,
								profile);
						this.logger.trace(description);
					}
					return;
				}
				if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
					if (this.logger.isTraceEnabled()) {
						StringBuilder description = getDescription("Skipped empty config extension ", location,
								resource, profile);
						this.logger.trace(description);
					}
					return;
				}
				String name = "applicationConfig: [" + location + "]";
				//读取配置文件内容,将其封装到Document类中,解析文件内容主要是找到
				//配置spring.profiles.active和spring.profiles.include的值
				List<Document> documents = loadDocuments(loader, name, resource);
				//如果文件没有配置数据,则跳过
				if (CollectionUtils.isEmpty(documents)) {
					if (this.logger.isTraceEnabled()) {
						StringBuilder description = getDescription("Skipped unloaded config ", location, resource,
								profile);
						this.logger.trace(description);
					}
					return;
				}
				List<Document> loaded = new ArrayList<>();
				//遍历配置文件,处理里面配置的profile
				for (Document document : documents) {
					if (filter.match(document)) {
					    //将配置文件中配置的spring.profiles.active和
					    //spring.profiles.include的值写入集合profiles中,
					    //上层调用方法会读取profiles集合中的值,并读取对应的配置文件
					    //addActiveProfiles方法只在第一次调用时会起作用,里面有判断
						addActiveProfiles(document.getActiveProfiles());
						addIncludedProfiles(document.getIncludeProfiles());
						loaded.add(document);
					}
				}
				Collections.reverse(loaded);
				if (!loaded.isEmpty()) {
					loaded.forEach((document) -> consumer.accept(profile, document));
					if (this.logger.isDebugEnabled()) {
						StringBuilder description = getDescription("Loaded config file ", location, resource, profile);
						this.logger.debug(description);
					}
				}
			}
			catch (Exception ex) {
				throw new IllegalStateException("Failed to load property source from location '" + location + "'", ex);
			}
		}

在最后一个load方法中可以看到spring boot通过Resource类加载了配置文件。
用下图梳理一下整个加载流程:
spring boot配置文件application.properties加载原理解析_第2张图片

还有一点要注意,如果定义了多个环境文件,同时也通过spring.profiles.active激活了多个环境,那么spring将加载所有激活环境的配置文件,最后加载配置文件的配置会覆盖前面加载的配置。

你可能感兴趣的:(spring,boot,spring,boot,配置文件,java)