Spring Boot源码分析-配置文件加载原理

4,194 阅读7分钟

Spring Boot框架提供了很多的默认配置,不需要我们再去逐一配置,极大地简化了开发流程,但是还是有部分配置是无法提供默认值得,这时候就需要我们自己手动配置。一般情况下,我们的配置都是写在application.propertiesapplication.yml中,本文就让我们一起来探讨一下Spring Boot如何加载配置文件中的内容。

本文针对有一定Spring Boot使用基础的同学,才能更好地理解后面叙述的内容。

首先让我们了解一下Spring Boot

Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。

  • Spring Boot特性
  1. 约定优于配置,大多数的配置直接使用默认配置即可
  2. 快速搭建项目,脱离繁杂的XML配置
  3. 内嵌Servlet容器,不依赖外部容器
  4. 与云计算天然集成,提供主流框架一键集成方式

接下来,进入正题通过源码分析理解Spring Boot加载配置文件内容的过程(部分方法内容没有贴出,可以根据源码一步一步跟踪下去)。

所有的Spring Boot项目都是由SpringApplication.run()这个静态方法开始执行的,源码分析也从这里开始进行。

public static ConfigurableApplicationContext run(Object source, String... args) {
	return run(new Object[] { source }, args);
}

run()方法内部实际上构造了一个SpringApplication对象并执行对象的run()方法(非静态方法)

public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
	return new SpringApplication(sources).run(args);
}

可以看到构造方法里面执行了initialize()方法

private void initialize(Object[] sources) {
	if (sources != null && sources.length > 0) {
		this.sources.addAll(Arrays.asList(sources));
	}
	// 判断当前是否是web环境
	this.webEnvironment = deduceWebEnvironment();
	// 初始化ApplicationContextInitializer对象
	setInitializers((Collection) getSpringFactoriesInstances(
			ApplicationContextInitializer.class));
	// 初始化ApplicationListener对象
	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
	this.mainApplicationClass = deduceMainApplicationClass();
}

这里可以看到是通过getSpringFactoriesInstances()初始化相关对象,进入方法内部

private <T> Collection<? extends T> getSpringFactoriesInstances(Class<T> type,
		Class<?>[] parameterTypes, Object... args) {
	ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
	// 加载类名称
	Set<String> names = new LinkedHashSet<String>(
			SpringFactoriesLoader.loadFactoryNames(type, classLoader));
	// 创建对象
	List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
			classLoader, args, names);
	AnnotationAwareOrderComparator.sort(instances);
	return instances;
}

进入SpringFactoriesLoader.loadFactoryNames()方法,看加载哪些类

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
	String factoryClassName = factoryClass.getName();
	try {
		Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
				ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
		List<String> result = new ArrayList<String>();
		while (urls.hasMoreElements()) {
			URL url = urls.nextElement();
			Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
			String factoryClassNames = properties.getProperty(factoryClassName);
			result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
		}
		return result;
	}
	catch (IOException ex) {
		throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +
				"] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
	}
}

可以看到实际上是加载META-INF/spring.factories文件夹下的内容,读取到的内容放在Properties中,通过类的名称去获取对应的值。值就是实现类的名称,然后再调用createSpringFactoriesInstances创建相关类的实例,这样就完成了对ApplicationContextInitializer对象的实例化工作。同理ApplicationListener

那么META-INF/spring.factories这个文件放在哪里呢,我们的项目下一般是不存在这个文件的。

实际上这个文件存在于Spring Boot项目中,这里就体现了Spring Boot的默认配置,文件内容如下:

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.context.embedded.ServerPortInfoApplicationContextInitializer

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener,\
org.springframework.boot.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.logging.LoggingApplicationListener

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor

# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ConnectorStartFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoSuchMethodFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer

# FailureAnalysisReporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter

接下来,继续看run()方法

public ConfigurableApplicationContext run(String... args) {
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
	ConfigurableApplicationContext context = null;
	FailureAnalyzers analyzers = null;
	configureHeadlessProperty();
	// 实例化SpringApplicationRunListener对象
	SpringApplicationRunListeners listeners = getRunListeners(args);
	// 广播ApplicationStartedEvent事件
	listeners.starting();
	try {
		ApplicationArguments applicationArguments = new DefaultApplicationArguments(
				args);
		ConfigurableEnvironment environment = prepareEnvironment(listeners,
				applicationArguments);
		Banner printedBanner = printBanner(environment);
		context = createApplicationContext();
		analyzers = new FailureAnalyzers(context);
		prepareContext(context, environment, listeners, applicationArguments,
				printedBanner);
		refreshContext(context);
		afterRefresh(context, applicationArguments);
		listeners.finished(context, null);
		stopWatch.stop();
		if (this.logStartupInfo) {
			new StartupInfoLogger(this.mainApplicationClass)
					.logStarted(getApplicationLog(), stopWatch);
		}
		return context;
	}
	catch (Throwable ex) {
		handleRunFailure(context, listeners, analyzers, ex);
		throw new IllegalStateException(ex);
	}
}

这里需要关注getRunListeners()这个方法

private SpringApplicationRunListeners getRunListeners(String[] args) {
	Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
	return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(
			SpringApplicationRunListener.class, types, this, args));
}

根据前面分析的代码可以看出,这里构造了SpringApplicationRunListener对象,对应的实现类即是EventPublishingRunListener,这个对象提供了广播Spring事件的能力。

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

再看listeners.starting(),实际上就是执行了EventPublishingRunListener.starting()方法,这个方法广播了一个ApplicationEnvironmentPreparedEvent事件。


到这里才真正开始进入读取配置的环节,前面的都是铺垫。 通过观察可以看出ConfigFileApplicationListener监听了ApplicationEnvironmentPreparedEvent事件。那么在接收到这个事件的通知之后,又做了什么呢?

继续往下看...

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

这里可以看到执行了onApplicationEnvironmentPreparedEvent()方法。首先加载通过SpringFactoriesLoader.loadFactories()加载对应的Processor,然后将ConfigFileApplicationListener也添加到了postProcessors中。原来它也实现了EnvironmentPostProcessor接口。

public class ConfigFileApplicationListener
		implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
}

紧接着执行每个ProcessorpostProcessEnvironment()方法。这里我们主要关注ConfigFileApplicationListener.postProcessEnvironment()方法。

public void postProcessEnvironment(ConfigurableEnvironment environment,
		SpringApplication application) {
	addPropertySources(environment, application.getResourceLoader());
	configureIgnoreBeanInfo(environment);
	bindToSpringApplication(environment, application);
}
protected void addPropertySources(ConfigurableEnvironment environment,
		ResourceLoader resourceLoader) {
	RandomValuePropertySource.addToEnvironment(environment);
	// load方法才真正实现加载配置文件内容
	new Loader(environment, resourceLoader).load();
}

继续跟踪Loader.load()方法

public void load() {
	this.propertiesLoader = new PropertySourcesLoader();
	this.activatedProfiles = false;
	this.profiles = Collections.asLifoQueue(new LinkedList<Profile>());
	this.processedProfiles = new LinkedList<Profile>();

	Set<Profile> initialActiveProfiles = initializeActiveProfiles();
	this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
	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);

	while (!this.profiles.isEmpty()) {
		Profile profile = this.profiles.poll();
		for (String location : getSearchLocations()) {
			if (!location.endsWith("/")) {
				load(location, null, profile);
			}
			else {
				for (String name : getSearchNames()) {
					load(location, name, profile);
				}
			}
		}
		this.processedProfiles.add(profile);
	}

	addConfigurationProperties(this.propertiesLoader.getPropertySources());
}

这里先不考虑profile的影响,直接看while循环里面且套的foreach循环,进入getSearchLocations()

public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";
private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
private Set<String> getSearchLocations() {
	Set<String> locations = new LinkedHashSet<String>();
	// 判断是否修改了配置文件的默认位置
	if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
		for (String path : asResolvedSet(
				this.environment.getProperty(CONFIG_LOCATION_PROPERTY), null)) {
			if (!path.contains("$")) {
				path = StringUtils.cleanPath(path);
				if (!ResourceUtils.isUrl(path)) {
					path = ResourceUtils.FILE_URL_PREFIX + path;
				}
			}
			locations.add(path);
		}
	}
	locations.addAll(
			asResolvedSet(ConfigFileApplicationListener.this.searchLocations,
					DEFAULT_SEARCH_LOCATIONS));
	return locations;
}

DEFAULT_SEARCH_LOCATIONS可以看出,通常情况下,这个四个位置的配置文件会被默认加载。在回到load()方法里面的foreach循环,会执行

for (String name : getSearchNames()) {
	load(location, name, profile);
}

在进入getSearchNames()方法

private static final String DEFAULT_NAMES = "application";
private Set<String> getSearchNames() {
	if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
		return asResolvedSet(this.environment.getProperty(CONFIG_NAME_PROPERTY),
			null);
	}
	return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
}

这里的ConfigFileApplicationListener.this.namesnull,所以返回的就是DEFAULT_NAMES。所以,这就是为什么默认配置文件名称都是application。 拿到文件名称后,在回到上面的forech方法,执行了load()方法,name即是文件名称

private void load(String location, String name, Profile profile) {
	String group = "profile=" + ((profile != null) ? profile : "");
	if (!StringUtils.hasText(name)) {
		loadIntoGroup(group, location, profile);
	}
	else {
		// 支持的文件类型是properties,xml,yml等文件格式
		for (String ext : this.propertiesLoader.getAllFileExtensions()) {
			if (profile != null) {
				loadIntoGroup(group, location + name + "-" + profile + "." + ext,
						null);
				for (Profile processedProfile : this.processedProfiles) {
					if (processedProfile != null) {
						loadIntoGroup(group, location + name + "-"
								+ processedProfile + "." + ext, profile);
					}
				}
				loadIntoGroup(group, location + name + "-" + profile + "." + ext,
						profile);
			}
			// 加载配置文件
			loadIntoGroup(group, location + name + "." + ext, profile);
		}
	}
}

再进入loadIntoGroup方法

private PropertySource<?> doLoadIntoGroup(String identifier, String location,
		Profile profile) throws IOException {
	Resource resource = this.resourceLoader.getResource(location);
	PropertySource<?> propertySource = null;
	StringBuilder msg = new StringBuilder();
	if (resource != null && resource.exists()) {
		String name = "applicationConfig: [" + location + "]";
		String group = "applicationConfig: [" + identifier + "]";
		propertySource = this.propertiesLoader.load(resource, group, name,
				(profile != null) ? profile.getName() : null);
		if (propertySource != null) {
			msg.append("Loaded ");
			handleProfileProperties(propertySource);
		}
		else {
			msg.append("Skipped (empty) ");
		}
	}
	else {
		msg.append("Skipped ");
	}
	msg.append("config file ");
	msg.append(getResourceDescription(location, resource));
	if (profile != null) {
		msg.append(" for profile ").append(profile);
	}
	if (resource == null || !resource.exists()) {
		msg.append(" resource not found");
		this.logger.trace(msg);
	}
	else {
		this.logger.debug(msg);
	}
	return propertySource;
}

可以看到这里是通过this.propertiesLoader.load()方法读取配置

public PropertySource<?> load(Resource resource, String group, String name,
		String profile) throws IOException {
	if (isFile(resource)) {
		String sourceName = generatePropertySourceName(name, profile);
		for (PropertySourceLoader loader : this.loaders) {
			// 判断当前loader是否能读取该类型文件
			if (canLoadFileExtension(loader, resource)) {
				PropertySource<?> specific = loader.load(sourceName, resource,
						profile);
				addPropertySource(group, specific);
				return specific;
			}
		}
	}
	return null;
}

然后又通过loader.load()方法执行具体的读取逻辑

public PropertySourcesLoader(MutablePropertySources propertySources) {
	Assert.notNull(propertySources, "PropertySources must not be null");
	this.propertySources = propertySources;
	this.loaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
			getClass().getClassLoader());
}

结合META-INF/spring.factories,可以看出loaders实际上PropertiesPropertySourceLoaderYamlPropertySourceLoader对象。

我们看下PropertiesPropertySourceLoaderload方法。

public PropertySource<?> load(String name, Resource resource, String profile)
		throws IOException {
	if (profile == null) {
		Properties properties = PropertiesLoaderUtils.loadProperties(resource);
		if (!properties.isEmpty()) {
			return new PropertiesPropertySource(name, properties);
		}
	}
	return null;
}

PropertiesLoaderUtils.loadProperties()方法将配置文件中的内容读取写入到Properties对象里,到这里已经执行了Spring Boot对配置文件内容的读取。

总结

整个源码分析的过程还是很长的,牵涉到很多的类,中间的关系还是有点复杂。不过如果第一遍看不懂的话,可以多看几遍,多跟踪几遍源码,每次你都会多明白一些原理,看到后面自然就明白其中的原理了。这里为了简化分析,忽略了其他一部分因素的影响,例如profile等。 虽然看源码的过程比较枯燥繁琐,且懂不懂源码对业务开发没有明显的影响,但是如果你想突破自己,提升自己,阅读优秀源码是一门必修的课程。