SpringBoot2 | SpringBoot Environment源碼分析(四)

微信公衆號:吉姆餐廳ak 學習更多源碼知識,歡迎關注。 web

在這裏插入圖片描述


SpringBoot2 | SpringBoot啓動流程源碼分析(一)spring

SpringBoot2 | SpringBoot啓動流程源碼分析(二)bootstrap

SpringBoot2 | @SpringBootApplication註解 自動化配置流程源碼分析(三)springboot

SpringBoot2 | SpringBoot Environment源碼分析(四)bash

SpringBoot2 | SpringBoot自定義AutoConfiguration | SpringBoot自定義starter(五)微信

SpringBoot2 | SpringBoot監聽器源碼分析 | 自定義ApplicationListener(六)app

SpringBoot2 | 條件註解@ConditionalOnBean原理源碼深度解析(七)ide

SpringBoot2 | Spring AOP 原理源碼深度剖析(八)工具

SpringBoot2 | SpingBoot FilterRegistrationBean 註冊組件 | FilterChain 責任鏈源碼分析(九)源碼分析

SpringBoot2 | BeanDefinition 註冊核心類 ImportBeanDefinitionRegistrar (十)

SpringBoot2 | Spring 核心擴展接口 | 核心擴展方法總結(十一)


本篇文章主要介紹springBoot2.x配置文件解析流程,另外會涉及SpringBoot2.xenvironment處理邏輯上相對於SpringBoot1.x的變更。 springCloud的配置文件解析,則是在此基礎上作了擴展。在springBoot解析邏輯以前,添加了bootstrap配置,經過監聽器BootstrapApplicationListener實現。後續有詳細介紹。


1、概述

Environment是 spring 爲運行環境提供的高度抽象接口,項目運行中的全部相關配置都基於此接口。 springBoot對此接口作了擴展。 先來看一個簡單的SpringBoot應用。

@org.springframework.boot.autoconfigure.SpringBootApplication
@RestController
public class SpringBootApplication {

	@Autowired
	Environment environment;

	@RequestMapping(value = "/environment", method = RequestMethod.GET)
	public String environment() {
		//輸出environment 類型
		System.out.println(environment.getClass());
		return JSON.toJSONString(environment);
	}

}
複製代碼

上述代碼注入的environment具體對象是什麼呢? 跟着源碼,搜尋答案。


2、源碼分析

前面springBoot啓動流程中,咱們提到了有個prepareEnvironment方法:

ConfigurableEnvironment environment = prepareEnvironment(listeners,
					applicationArguments);
複製代碼
private ConfigurableEnvironment prepareEnvironment(
			SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// Create and configure the environment
		//一、初始化environment
		ConfigurableEnvironment environment = getOrCreateEnvironment();
		//二、加載默認配置
		configureEnvironment(environment, applicationArguments.getSourceArgs());
		//三、通知環境監聽器,加載項目中的配置文件
		listeners.environmentPrepared(environment);
		bindToSpringApplication(environment);
		if (this.webApplicationType == WebApplicationType.NONE) {
			environment = new EnvironmentConverter(getClassLoader())
					.convertToStandardEnvironmentIfNecessary(environment);
		}
		ConfigurationPropertySources.attach(environment);
		return environment;
	}
複製代碼

接下來對上面三步進行詳細分析:

一、初始化environment

private ConfigurableEnvironment getOrCreateEnvironment() {
		if (this.environment != null) {
			return this.environment;
		}
		//springboot應用返回的environment
		if (this.webApplicationType == WebApplicationType.SERVLET) {
			return new StandardServletEnvironment();
		}
		return new StandardEnvironment();
	}
複製代碼

能夠看到根據類型進行匹配 environment,獲取到StandardServletEnvironment,該實例直接注入到spring容器,因此上面示例代碼的輸出的類型就是StandardServletEnvironment

StandardServletEnvironment是整個springboot應用運行環境的實現類,後面全部關於配置和環境的操做都基於此類。看一下該類的結構:

這裏寫圖片描述
首先, StandardServletEnvironment的初始化一定會致使父類方法的初始化: AbstractEnvironment:

public AbstractEnvironment() {
		//從名字能夠看出加載咱們的自定義配置文件
		customizePropertySources(this.propertySources);
		if (logger.isDebugEnabled()) {
			logger.debug("Initialized " + getClass().getSimpleName() + " with PropertySources " + this.propertySources);
		}
	}
複製代碼

在構造方法中調用自定義配置文件,spring的一向作法,模板模式,調用的是實例對象的自定義邏輯:

@Override
	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);
	}
複製代碼

由於該配置類是基於web環境,因此先加載和 servlet有關的參數,addLast放在最後:

/** System environment property source name: {@value} */
	public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";

	/** JVM system properties property source name: {@value} */
	public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
複製代碼

該類又是對StandardEnvironment的擴展,這裏會調用super.customizePropertySources(propertySources);

@Override
	protected void customizePropertySources(MutablePropertySources propertySources) {
		propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
		propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
	}
複製代碼
/** System environment property source name: {@value} */
	public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";

	/** JVM system properties property source name: {@value} */
	public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
複製代碼

能夠看到放入順序是永遠放在最後面,也就是先加入的在前面。systemEnvironment是在systemProperties前面,這點很重要。由於前面的配置會覆蓋後面的配置,也就是說系統變量中的配置比系統環境變量中的配置優先級更高。以下:

這裏寫圖片描述


2)加載默認配置:

protected void configureEnvironment(ConfigurableEnvironment environment,
			String[] args) {
		//加載啓動命令行配置屬性
		configurePropertySources(environment, args);
		//設置active屬性
		configureProfiles(environment, args);
	}
複製代碼

這裏接收的參數是ConfigurableEnvironment,也就是StandardServletEnvironment的父類。 繼續跟進configurePropertySources方法:

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));
			}
		}
	}	
複製代碼

上述代碼主要作兩件事: 一是判斷SpringBootApplication是否指定了默認配置, 二是加載默認的命令行配置。

上面有個核心關鍵類出現了,MutablePropertySources,mutable中文是可變的意思,該類封裝了屬性資源集合:

public class MutablePropertySources implements PropertySources {
	private final Log logger;
	private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
}
複製代碼

該類又是如何使用的呢?

這裏寫圖片描述
這裏的設計很巧妙,將MutablePropertySources傳遞到文件解析器propertyResolver中,同時AbstractEnvironment又實現了文件解析接口ConfigurablePropertyResolver,因此AbstractEnvironment就有了文件解析的功能。因此StandardServletEnvironment文件解析功能實際委託給了PropertySourcesPropertyResolver來實現

繼續看一下configureProfiles(environment, args);方法:

protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
		environment.getActiveProfiles(); // ensure they are initialized
		// But these ones should go first (last wins in a property key clash)
		Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
		profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
		environment.setActiveProfiles(StringUtils.toStringArray(profiles));
	}
複製代碼

該方法主要將SpringBootApplication中指定的additionalProfiles文件加載到environment中,通常默認爲空。 該變量的用法,在項目啓動類中,須要顯示建立SpringApplication實例,以下:

SpringApplication springApplication = new SpringApplication(MyApplication.class);
		//設置profile變量
        springApplication.setAdditionalProfiles("prd");
        springApplication.run(MyApplication.class,args);
複製代碼

三、通知環境監聽器,加載項目中的配置文件

觸發監聽器:

listeners.environmentPrepared(environment);
複製代碼

SpringBoot2 | SpringBoot啓動流程源碼分析(一)中提到了該方法通知的監聽器,和配置文件有關的監聽器類型爲ConfigFileApplicationListener,監聽到事件時執行的方法:

@Override
	public void postProcessEnvironment(ConfigurableEnvironment environment,
			SpringApplication application) {
		//加載項目中的配置文件
		addPropertySources(environment, application.getResourceLoader());
		configureIgnoreBeanInfo(environment);
		bindToSpringApplication(environment, application);
	}
複製代碼

繼續跟進去,會發現一個核心內部類 Loader ,配置文件加載也就委託給該內部類來處理:

private class Loader {

		private final Log logger = ConfigFileApplicationListener.this.logger;
		//當前環境
		private final ConfigurableEnvironment environment;
		//類加載器,能夠在項目啓動時經過 SpringApplication 構造方法指定,默認採用 Launcher.AppClassLoader加載器
		private final ResourceLoader resourceLoader;
		//資源加載工具類
		private final List<PropertySourceLoader> propertySourceLoaders;
		//LIFO隊列
		private Queue<String> profiles;
		//已處理過的文件
		private List<String> processedProfiles;
		private boolean activatedProfiles;

		Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
			this.environment = environment;
			//獲取類加載器
			this.resourceLoader = resourceLoader == null ? new DefaultResourceLoader()
					: resourceLoader;
			//獲取propertySourceLoaders
			this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(
					PropertySourceLoader.class, getClass().getClassLoader());
		}
		//......
}		
複製代碼

上面propertySourceLoaders經過 SpringFactoriesLoader 獲取當前項目中類型爲 PropertySourceLoader 的全部實現類,默認有兩個實現類,以下圖:

這裏寫圖片描述

繼續來看主要解析方法:load()

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();
				load(profile, this::getPositiveProfileFilter,
						addToLoaded(MutablePropertySources::addLast, false));
				this.processedProfiles.add(profile);
			}
			//對加載過的配置文件進行排序
			load(null, this::getNegativeProfileFilter,
					addToLoaded(MutablePropertySources::addFirst, true));
			addLoadedPropertySources();
		}
複製代碼

跟進去上面初始化方法:

private void initializeProfiles() {
			Set<Profile> initialActiveProfiles = initializeActiveProfiles();
			this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
			//若是爲空,添加默認的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);
					}
				}
			}
			// The default profile for these purposes is represented as null. We add it
			// last so that it is first out of the queue (active profiles will then
			// override any settings in the defaults when the list is reversed later).
			//這裏添加一個爲null的profile,主要是加載默認的配置文件
			this.profiles.add(null);
		}
複製代碼

上面主要作了兩件事情:

1)判斷是否指定了profile,若是沒有,添加默認環境:default。後面的解析流程會解析default文件,好比:application-default.yml、application-default.properties

注意:在第2步中咱們提到了additionalProfiles屬性,若是咱們經過該屬性指定了profile,這裏就不會加載默認的配置文件,根據咱們指定的profile進行匹配。

2)添加一個null的profile,主要用來加載沒有指定profile的配置文件,好比:application.properties 由於 profiles 採用了 LIFO 隊列,後進先出。因此會先加載profile爲null的配置文件,也就是匹配application.properties、application.yml

繼續跟進解析方法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));
			});
		}
複製代碼

能夠看到springBoot2.0底層的新的改動都是基於lambda表達式實現。

繼續跟進getSearchLocations()方法:

這裏寫圖片描述

獲取路徑以後,會拼接配置文件名稱,選擇合適的yml或者properties解析器進行解析: (name) -> load(location, name, profile, filterFactory, consumer) 具體的解析邏輯比較簡單,咱們來梳理一下:

1)獲取默認的配置文件路徑,有4種。 2)遍歷全部的路徑,拼裝配置文件名稱。 3)再遍歷解析器,選擇yml或者properties解析,將解析結果添加到集合MutablePropertySources當中。

最後解析的結果以下:

在這裏插入圖片描述

至此,springBoot中的資源文件加載完畢,解析順序從上到下,因此前面的配置文件會覆蓋後面的配置文件。能夠看到application.properties的優先級最低,系統變量和環境變量的優先級相對較高。


SpringBoot2.x和SpringBoot1.x不一樣點

主要有兩處大的變化:

1、在SpringBoot1.x版本中,若是咱們定義了application-default.properties文件,優先級順序:

在這裏插入圖片描述

application-default.properties > application-dev.properties > application.properties
複製代碼

在SpringBoot2.x版本中,若是咱們定義了application-default.properties文件,會有兩種狀況:

1)沒有配置application-dev.properties文件,優先級順序:

application-default.properties > application.properties
複製代碼

2)同時配置了application-dev.properties文件,優先級順序:

application-dev.properties > application.properties
複製代碼

這是由於在2.x版本中,若是定義了application-dev.properties文件,application-default.properties文件將會刪除。

代碼中,在解析application.properties時會有以下判斷,判斷是否有 active 文件,若是有,則會刪除默認的profile

private void maybeActivateProfiles(Set<Profile> profiles) {
            if (profiles.isEmpty()) {
                return;
            }
            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;
            //刪除默認的配置文件,即application-default.* 文件
            removeUnprocessedDefaultProfiles();
        }

        private void removeUnprocessedDefaultProfiles() {
            this.profiles.removeIf(Profile::isDefaultProfile);
        }
複製代碼

2、SpringBoot 1.x版本中,項目的配置文件統一被封裝在內部類ConfigFileApplicationListener$ConfigurationPropertySources對象中,存入environment。而在2.x版本中,配置文件則是分別被包裝成OriginTrackedMapPropertySource存入environment

springboot1.x升級到springboot2.x的時候,還請注意以上兩點。


SpringBoot2 | SpringBoot啓動流程源碼分析(一)

SpringBoot2 | SpringBoot啓動流程源碼分析(二)

SpringBoot2 | @SpringBootApplication註解 自動化配置流程源碼分析(三)

SpringBoot2 | SpringBoot Environment源碼分析(四)

SpringBoot2 | SpringBoot自定義AutoConfiguration | SpringBoot自定義starter(五)

SpringBoot2 | SpringBoot監聽器源碼分析 | 自定義ApplicationListener(六)

SpringBoot2 | 條件註解@ConditionalOnBean原理源碼深度解析(七)

相關文章
相關標籤/搜索