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不僅可以加載本地配置文件,還可以利用它加載遠程配置,並且可以做到配置變更時動態刷新。後續將繼續從源碼中追尋其中的原理,敬請期待!

 

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