springboot源码解读之配置从哪来

目录

概述

配置初始化

本地配置文件加载

本地配置文件location

本地配置文件名字

本地配置文件后缀名

Profile对文件名的影响

Json配置解析

@PropertySource注解处理

总结



概述

用了一段时间springboot,发现springboot启动及运行的过程中不断的使用配置,作为用户我们可以通过命令行、本地配置文件等方式设置配置项。但是springboot都可以从哪里加载配置?他们的顺序是怎样的?带着这些疑问学习一下源码。

本文使用的springboot版本为2.2.6

配置初始化

配置伴随着springboot的整个生命周期,在context创建之前就已经创建了environment(管理所有配置),看SpringApplication中关于

springboot启动的代码:

public ConfigurableApplicationContext run(String... args) {
	//......省略无关代码......
	try {
		ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
		ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
		configureIgnoreBeanInfo(environment);
		//......省略无关代码......
		prepareContext(context, environment, listeners, applicationArguments, printedBanner);
		refreshContext(context);
		afterRefresh(context, applicationArguments);
		//......省略无关代码......
		listeners.started(context);
		callRunners(context, applicationArguments);
	}
	//......省略无关代码......

	try {
		listeners.running(context);
	}
	catch (Throwable ex) {
		handleRunFailure(context, ex, exceptionReporters, null);
		throw new IllegalStateException(ex);
	}
	return context;
}

prepareEnvironment方法完成了environment的初始化以及部分配置的加载,代码如下:

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

可以看到prepareEnvironment方法完成了一系列关于配置的处理,我们对environment相关的操作进行逐个的跟踪。首先

根据classpath下的jar包判断当前服务的环境,决定创建什么类型的environment。具体的代码为

switch (this.webApplicationType) {
		case SERVLET:
			return new StandardServletEnvironment();
		case REACTIVE:
			return new StandardReactiveWebEnvironment();
		default:
			return new StandardEnvironment();
		}

environment有三种,当服务为servlet类型时创建StandardServletEnvironment,服务为reactive类型时创建StandardReactiveWebEnvironment,如果都不是则创建默认的StandardEnvironment。

StandardReactiveWebEnvironment和StandardServletEnvironment都是StandardEnvironment的子类,而StandardServletEnvironment加载的配置要比其他两个更加全面,多出两个servlet相关的配置,我们以它为目标看看都有哪些配置被加载进来。

第一次添加配置来自于StandardServletEnvironment的customizePropertySources方法

protected void customizePropertySources(MutablePropertySources propertySources) {
	propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
	propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
	if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
		propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
	}
	super.customizePropertySources(propertySources);
}

这里共添加了三个配置,前两个是名字分别为servletConfigInitParams、servletContextInitParams的StubPropertySource类型配置。其实这里只是起到占位的作用,后面servlet模块启动了才会替换当前这个配置。另外一个jndi配置,我们的服务不适用它,把它跳过。

现在environment一共有了两个配置,但实际没有任何配置项加载进来。

StandardServletEnvironment执行完配置的加载之后是StandardEnvironment

protected void customizePropertySources(MutablePropertySources propertySources) {
    propertySources.addLast(new PropertiesPropertySource("systemProperties", this.getSystemProperties()));
    propertySources.addLast(new SystemEnvironmentPropertySource("systemEnvironment", this.getSystemEnvironment()));
}

StandardEnvironment自己加载了两个配置,分别是systemProperties和systemEvniroment。

systemProperties实际上就是通过System.getProperties()获取了所有的系统属性,包括java版本、当前用户名等基本属性以及通过命令行-D方式传入的用户自定义属性。

systemEvniroment实际上就是通过System.getenv()获取了所有的的环境变量,如PATH、JAVA_HOME等。

目前为止,environment一共有了4个配置,但实际上只有systemProperties和systemEvniroment加载了进来。

 

回到SpringApplication的prepareEnvironment方法,执行完getOrCreateEnvironment完成了上述配置的加载,然后开始执行configureEnvironment方法。

protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
	if (this.addConversionService) {
		ConversionService conversionService = ApplicationConversionService.getSharedInstance();
		environment.setConversionService((ConfigurableConversionService) conversionService);
	}
	configurePropertySources(environment, args);
	configureProfiles(environment, args);
	}

通过上面代码看到,configureEnvironment可能加载了一些配置,逐个跟进去看看到底发生了什么。

protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
	MutablePropertySources sources = environment.getPropertySources();
	if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
		sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties));
	}
	if (this.addCommandLineProperties && args.length > 0) {
		String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
		if (sources.contains(name)) {
			PropertySource<?> source = sources.get(name);
			CompositePropertySource composite = new CompositePropertySource(name);
			composite.addPropertySource(
					new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
			composite.addPropertySource(source);
			sources.replace(name, composite);
		}
		else {
			sources.addFirst(new SimpleCommandLinePropertySource(args));
		}
	}
}

这里加载默认配置defaultProperties,如果没设置则不加载,可以通过SpringApplication的setDefaultProperties方法设置。

另外还加载命令行配置commandLineArgs,如果没有设置则不加载,这里的args就是调用SpringApplication的run方法传进来的参数,可以通过--mainArgKey=mainArgValue这样的形式设置。注意一点,commandLineArgs是添加到最前面的,优先级最高。

这里还设置了profile,后面会用到。

目前为止,environment一共有了6个配置

 

本地配置文件加载

再次回到SpringApplication的prepareEnvironment方法,现在environment的以及创建好并完成了最基本配置的加载,下面将发布environment已经准备好的消息。这里是springboot提供的一个扩展点,用户可以自定义ApplicationListener监听ApplicationEnvironmentPreparedEvent事件,扩展方法这里不做展开,埋一个伏笔

 

ConfigFileApplicationListener就是上面的listener之一,springboot用它实现了本地配置文件加载的逻辑。

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

ConfigFileApplicationListener里面又提供了扩展点,用户可以自定义EnvironmentPostProcessor,默认有如下几个

ConfigFileApplicationListener通过addPropertySources加载配置

加载本地配置文件之前,先添加了RandomValuePropertySource用于支持随机参数的数据。然后开始查找加载本地配置文件,
文件的定位需要确认三个变量,路径位置、文件名、文件后缀名,对应springboot中为location、name、ext。另外springboot还支持加载不同profile的配置,所有还需要查找profile对应的文件名。

最终的加载由一系列重载的load方法完成,省略中间过程下面直接看定位文件的几个参数是怎么来的。

本地配置文件location

本地配置文件的location可以由用户自己通过spring.config.location(对应代码中CONFIG_LOCATION_PROPERTY)指定,可以配置在前面已经加载进来的任意配置里,如启动时-D设置的参数、调用SpringApplication的run方法传进来等。如果用户指定了位置,则只会查找指定的位置。

如果用户想在系统默认的位置之外加载配置文件,则可以通过spring.config.additional-location(对应代码中CONFIG_ADDITIONAL_LOCATION_PROPERTY)指定

系统的默认路径为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 Set<String> getSearchLocations(String propertyName) {
	Set<String> locations = new LinkedHashSet<>();
	if (this.environment.containsProperty(propertyName)) {
		for (String path : asResolvedSet(this.environment.getProperty(propertyName), null)) {
			if (!path.contains("$")) {
				path = StringUtils.cleanPath(path);
				if (!ResourceUtils.isUrl(path)) {
					path = ResourceUtils.FILE_URL_PREFIX + path;
				}
			}
			locations.add(path);
		}
	}
	return locations;
}

本地配置文件名字

配置文件的名字同样可以由用户自己指定,通过设置spring.config.name(对应代码中CONFIG_NAME_PROPERTY)。

默认的文件名字为application

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

本地配置文件后缀名

后缀名还是可以由用户自定义,只是要通过实现PropertySourceLoader的方式,因为自定的文件格式需要用户自己解析。

系统默认的PropertySourceLoader为PropertiesPropertySourceLoader和YamlPropertySourceLoader,他们支持xml、properties 、yaml、yml格式配置文件的加载。

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

Profile对文件名的影响

profile对配置文件加载的影响就是在文件名的后面拼接上“-”+ profile。

系统中有两个默认的profile分别是null和default

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) {
		// Try profile-specific file & profile section in profile file (gh-340)
		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);
}

Json配置解析

上面已经提到ConfigFileApplicationListener里面提供了扩展点,用户可以实现EnvironmentPostProcessor完成一些额外的工作。spring boot内部的SpringApplicationJsonEnvironmentPostProcessor就通过这个扩展点完成了Json配置的解析。

public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
	MutablePropertySources propertySources = environment.getPropertySources();
	propertySources.stream().map(JsonPropertyValue::get).filter(Objects::nonNull).findFirst()
			.ifPresent((v) -> processJson(environment, v));
}

private void processJson(ConfigurableEnvironment environment, JsonPropertyValue propertyValue) {
	JsonParser parser = JsonParserFactory.getJsonParser();
	Map<String, Object> map = parser.parseMap(propertyValue.getJson());
	if (!map.isEmpty()) {
		addJsonPropertySource(environment, new JsonPropertySource(propertyValue, flatten(map)));
	}
}

这里不是加载新的配置,而是在原来的配置里查找spring.application.json,SPRING_APPLICATION_JSON,如果存在则将其中的配置项解析出来。

static JsonPropertyValue get(PropertySource<?> propertySource) {
	for (String candidate : CANDIDATES) {
		Object value = propertySource.getProperty(candidate);
		if (value instanceof String && StringUtils.hasLength((String) value)) {
			return new JsonPropertyValue(propertySource, candidate, (String) value);
		}
	}
	return null;
}

实际上就是为我们提供了一种json格式配置解析的能力,个人认为不如类型properties或者yaml格式的本地文件形式方便,因为json字符串得通过直接已经加载的命令行或者run方法参数等方式传入。

@PropertySource注解处理

@PropertySource是spring框架本身就支持的机制,所以这里不做过多展开,简单介绍一下。在任何bean上加上这个注解,spring都可以把相关的配置加载到environment中。ConfigurationClassParser负责加载应用内通过注解生命的bean的定义,它的doProcessConfigurationClass方法完成了@PropertySource注解的处理

protected final SourceClass doProcessConfigurationClass(
		ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
		throws IOException {

	if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
		// Recursively process any member (nested) classes first
		processMemberClasses(configClass, sourceClass, filter);
	}

	// Process any @PropertySource annotations
	for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
			sourceClass.getMetadata(), PropertySources.class,
			org.springframework.context.annotation.PropertySource.class)) {
		if (this.environment instanceof ConfigurableEnvironment) {
			processPropertySource(propertySource);
		}
		else {
			logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
					"]. Reason: Environment must implement ConfigurableEnvironment");
		}
	}

	// Process any @ComponentScan annotations
	Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
			sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
	// ComponentScans相关处理,省略...

	// Process any @Import annotations
	processImports(configClass, sourceClass, getImports(sourceClass), filter, true);

	// Process any @ImportResource annotations
	AnnotationAttributes importResource =
			AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
	// ImportResource相关处理,省略...

	// 其他逻辑,省略...		
	return null;
}

在注解中可以指定文件的路径,加载文件的factory(默认properties方式)。其中文件的路径里可以使用占位符,在读取文件之前spring会先替换占位符。如@PropertySource(name = "myPropertySource", value = {"${property.source.path}"}),真正读取的时候实际上会读取property.source.path配置的路径。这个路径可以通过已经加载到environment中的任何配置方式进行配置。

private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
	String name = propertySource.getString("name");
	if (!StringUtils.hasLength(name)) {
		name = null;
	}
	String encoding = propertySource.getString("encoding");
	if (!StringUtils.hasLength(encoding)) {
		encoding = null;
	}
	String[] locations = propertySource.getStringArray("value");
	Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
	boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");

	Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
	PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
			DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));

	for (String location : locations) {
		try {
			String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
			Resource resource = this.resourceLoader.getResource(resolvedLocation);
			addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
		}
		// 省略...
	}
}

总结

在加载上面的全部配置后,系统中的environment包含了如下的PropertySource。

仔细观察可以发现图中的第9、10、11项配置在本文中并未提及,实际上是因为引入了spring-cloud项目加载了相关的配置。使用spring-cloud不仅可以加载本地配置文件,还可以利用它加载远程配置,并且可以做到配置变更时动态刷新。后续将继续从源码中追寻其中的原理,敬请期待!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章