`
tanliwei
  • 浏览: 49412 次
  • 性别: Icon_minigender_1
  • 来自: 中国
社区版块
存档分类
最新评论

从SpringBoot源码分析 配置文件的加载和优先级

阅读更多
RT.
  跟入源码之前,先提一个问题
  SpringBoot 既可以加载指定目录下的配置文件获取配置项,也可以通过启动参数(VM Options)传入配置项,为什么通过启动参数传入的配置项会“顶掉”配置文件中的配置?
 
示例
application.yml 
server.port: 8888
spring.profiles.active: dev
 
application-dev.yml 
spring.think: hello
 
在IDEA中使用命令行配置项 
VM Options 
-Dserver.port=5555
 
 
启动结果:
Tomcat started on port(s): 5555 (http) with context path ''
  同时在application.yml 和 启动参数(VM options)中设置 server.port, 最终采用了 启动参数 中的值。
 
  下面开始从main函数启动处,跟入SpringBoot源码,看看SpringBoot是如何处理的。
 
系统说明
JDK:1.8
SpringBoot 版本: 2.0.2.RELEASE
IDE: IntelliJ IDEA 2017
 
跟入源码正文 
#ApplicationConfigLoadFlow.java
    public static void main(String[] args) {
        SpringApplication.run(ApplicationConfigLoadFlow.class, args);
}
 
   从SpringApplication.run 函数开始,一个方法一个方法的点进去。需要跟入的方法给与注释。
 
IDEA 快捷键:
进入方法:  Ctrl + 鼠标左键
光标前进/后退: Ctrl + Shirt + 右方向键/左方向键
 
  依次跟入:
#SpringApplication.java
return run(new Class<?>[] { primarySource }, args)
 
#SpringApplication.java
return new SpringApplication(primarySources).run(args);
 
#SpringApplication.java
	public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(
					args);
			//跟入
			ConfigurableEnvironment environment = prepareEnvironment(listeners,
					applicationArguments);
			configureIgnoreBeanInfo(environment);
			configureIgnoreBeanInfo(environment);
  进入public ConfigurableApplicationContext run(String... args) 方法后,我们重点看 prepareEnvironment这个方法,这个方法之前的源码的从类名和源码注释上知道stopWatch用于计时,上下文context还未初始化,listeners监听器存储了EventPushlingRunListener。
  通过IDEA 一行行debug可以看到是在 prepareEnvironment方法执行后,server.port 配置项才被加载入 environment 环境配置中。如下图所示。注意:配置文件中的配置还未载入,请先接着往后看。


  因此,我们重新打断点跟入prepareEnvironment方法。
#SpringApplication.java
	private ConfigurableEnvironment prepareEnvironment(
			SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// Create and configure the environment
		//跟入
		ConfigurableEnvironment environment = getOrCreateEnvironment();
		configureEnvironment(environment, applicationArguments.getSourceArgs());
     同样的套路,通过debug发现实在getOrCreateEnvironment方法执行后得到server.port的值 
#SpringApplication.java
	private ConfigurableEnvironment getOrCreateEnvironment() {
		if (this.environment != null) {
			return this.environment;
		}
		if (this.webApplicationType == WebApplicationType.SERVLET) {
			//跟入 
			return new StandardServletEnvironment();
		}
  虚拟机启动参数的加载 是在StandardServletEnvironment 的实例化过程也就是构造函数中完成的。
  跟入StandardServletEnvironment的构造函数之前,大家需要先了解 Java模板模式
  先看一下StandardServletEnvironment的类继承关系图(通过IDEA 右键 类名 --> Diagrams --> Show Diagrams Popup 即可显示下图)


  抽象父类AbstractEnvironment的实例化方法中,调用了可由子类继承的customizePropertySources方法。
#AbstractEnvironment.java
	public AbstractEnvironment() {
		//跟入
		customizePropertySources(this.propertySources);
		if (logger.isDebugEnabled()) {
			logger.debug("Initialized " + getClass().getSimpleName() + " with PropertySources " + this.propertySources);
		}
	}
  实体化的过程中回过头来调用了子类StandardServletEnvironment的customizePropertySources方法
#StandardServletEnvironment.java
	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);
	}
  又调用了父类StandardEnvironment的customizePropertySources方法
#StandardEnvironment.java
	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()));
	}
 

  通过IDEA 的变量监听功能,可以看到正是StandardEnvironment类的getSystemProperties()方法获取到了之前设置的虚拟机启动参数server.port的值。
继续跟进去
#AbstractEnvironment.java
	public Map<String, Object> getSystemProperties() {
		try {
			//跟入
			return (Map) System.getProperties();
 
#System.java
    public static Properties getProperties() {
        SecurityManager sm = getSecurityManager();
        if (sm != null) {
            sm.checkPropertiesAccess();
        }

        return props;
  我们搜索一下有没有什么地方初始化 props
#System.java
    private static Properties props;
    private static native Properties initProperties(Properties props);
  发现了静态方法 initProperties,从方法名上即可知道在类被加载的时候 就初始化了 props, 这是个本地方法,继续跟的话需要看对应的C++代码。
  回到StandardEnvironment类的customizePropertySources方法
#StandardEnvironment.java
	protected void customizePropertySources(MutablePropertySources propertySources) {
		//SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME: systemProperties
		//跟入
		propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
		propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
	}
 
#MutablePropertySources.java
	/**
	 * Add the given property source object with lowest precedence.
	 * 添加属性源,并使其优先级最低
	 */
	public void addLast(PropertySource<?> propertySource) {
 再看一下MutablePropertySources的注释
 * <p>Where <em>precedence</em> is mentioned in methods such as {@link #addFirst}
 * and {@link #addLast}, this is with regard to the order in which property sources
 * will be searched when resolving a given property with a {@link PropertyResolver}.
 *
 * addFist 和 add Last 会设置属性源的优先级,
 * PropertyResolver解析配置时会根据优先级使用配置源
 *
 * @author Chris Beams
 * @author Juergen Hoeller
 * @since 3.1
 * @see PropertySourcesPropertyResolver
 */
public class MutablePropertySources implements PropertySources {
 
问题2:
  此时我们已经看到虚拟机的启动参数先添加到系统当中,那么后面添加进来的Property Source属性源的优先级是否比 SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME(systemProperties) 属性源的优先级高呢?
  回到SpringApplication的prepareEnvironment方法


  同样的debug套路发现listeners.environmentPrepared执行后,application.yml 和 application-dev.yml 两个配置文件的配置项都被加载完成,所以我们继续跟入environmentPrepared方法
  在跟入environmentPrepared方法之前,需要了解 Java事件监听机制
  跟入environmentPrepared中的源码
#SpringApplicationRunListeners.java
	public void environmentPrepared(ConfigurableEnvironment environment) {
		for (SpringApplicationRunListener listener : this.listeners) {
			//跟入
			listener.environmentPrepared(environment);
		}
	}
 
#EventPublishingRunListener.java
	public void environmentPrepared(ConfigurableEnvironment environment) {
		//广播ApplicationEnvrionmentPreparedEvnet事件
		//跟入
		this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(
				this.application, this.args, environment));
	}
 
#SimpleApplicationEventMulticaster.java
	public void multicastEvent(ApplicationEvent event) {
		//跟入
		multicastEvent(event, resolveDefaultEventType(event));
	}

	@Override
	public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
		ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
		//注意此时 getApplicationListeners(event, type) 返回结果
		//包含 监听器 *ConfigFileApplicationListener*
                for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
			Executor executor = getTaskExecutor();
			if (executor != null) {
				executor.execute(() -> invokeListener(listener, event));
			}
			else {
				//跟入
				invokeListener(listener, event);
			}
		}
	}
 
#SimpleApplicationEventMulticaster.java
	/**
	 * Invoke the given listener with the given event.
	 * 调用对应事件的监听者
	 * @param listener the ApplicationListener to invoke
	 * @param event the current event to propagate
	 * @since 4.1
	 */
	protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
		ErrorHandler errorHandler = getErrorHandler();
		if (errorHandler != null) {
			try {
				doInvokeListener(listener, event);
			}
			catch (Throwable err) {
				errorHandler.handleError(err);
			}
		}
		else {
			//跟入
			doInvokeListener(listener, event);
		}
	}

	private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
		try {
			//跟入
			listener.onApplicationEvent(event);
		}
 
#ApplicationListener.java
	//实现接口的监听器当中,有并跟入ConfigFileApplicationListener的实现
	void onApplicationEvent(E event);
 
#ConfigFileApplicationListener.java
	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 为 ConfigFileApplicationListener
			postProcessor.postProcessEnvironment(event.getEnvironment(),
					event.getSpringApplication());
		}
	}
 
#ConfigFileApplicationListener.java
	public void postProcessEnvironment(ConfigurableEnvironment environment,
			SpringApplication application) {
		//跟入
		addPropertySources(environment, application.getResourceLoader());
	}

	protected void addPropertySources(ConfigurableEnvironment environment,
			ResourceLoader resourceLoader) {
		//environment的属性源中包含 systemProperties 属性源 即包含 server.port启动参数
		RandomValuePropertySource.addToEnvironment(environment);
		//跟入 load()方法
		new Loader(environment, resourceLoader).load();
	}
  跟入load之前,需要了解  java lambda表达式
#ConfigFileApplicationListener.java
		public void load() {
			this.profiles = new LinkedList<>();
			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();
		}
 
#ConfigFileApplicationListener.java
		private void load(Profile profile, DocumentFilterFactory filterFactory,
				DocumentConsumer consumer) {
			//getSearchLocations()默认返回:
			//[./config/, file:./, classpath:/config/, classpath:/]
			//即搜索这些路径下的文件
			getSearchLocations().forEach((location) -> {
				boolean isFolder = location.endsWith("/");
				//getSearchNames()返回:application
				Set<String> names = (isFolder ? getSearchNames() : NO_SEARCH_NAMES);
				//跟入load(.....)
				names.forEach(
						(name) -> load(location, name, profile, filterFactory, consumer));
			});
		}
 
#ConfigFileApplicationListener.java
		private void load(String location, String name, Profile profile,
				DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
			//name默认为:application,所以这个if分支略过
			if (!StringUtils.hasText(name)) {
				for (PropertySourceLoader loader : this.propertySourceLoaders) {
					if (canLoadFileExtension(loader, location)) {
						load(loader, location, profile,
								filterFactory.getDocumentFilter(profile), consumer);
					}
				}
			}
			//this.propertySourceLoaders: PropertiesPropertySourceLoader,YamlPropertySourceLoader
			for (PropertySourceLoader loader : this.propertySourceLoaders) {
				//PropertiesPropertySourceLoader.getFileExtensions(): properties, xml
				//YamlPropertySourceLoader.getFileExtensions(): yml, yaml
				for (String fileExtension : loader.getFileExtensions()) {
					//location: [./config/, file:./, classpath:/config/, classpath:/]
					//name: application
					String prefix = location + name;
					fileExtension = "." + fileExtension;
					//profile: null, dev
					//相当于对(location, fileExtension, profile)做笛卡尔积,
					//遍历每一种可能,然后加载
					//加载文件的细节在loadForFileExtension中完成
					loadForFileExtension(loader, prefix, fileExtension, profile,
							filterFactory, consumer);
				}
			}
		}
  继续跟入 loadForFileExtension 方法,可以了解载入一个配置文件的更多细节。
  回到之前的load()方法
#ConfigFileApplicationListener.java
		public void load() {
			this.profiles = new LinkedList<>();
			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();
 
#ConfigFileApplicationListener.java
		private void addLoadedPropertySources() {
			//destination: 进入ConfigFileApplicationListener监听器前已有的配置 
			//即destination中包含 systemProperties 配置源
			MutablePropertySources destination = this.environment.getPropertySources();
			String lastAdded = null;
			//loaded: 此次监听通过扫描文件加载进来的配置源
			//loaded: application.yml, appcalition-dev.yml
			List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
			//倒序后 loaded: application-dev.yml, application.yml
			Collections.reverse(loaded);
			//先处理 application-dev.yml
			for (MutablePropertySources sources : loaded) {
				for (PropertySource<?> source : sources) {
					//第一次进入: lastAdded:null
					if (lastAdded == null) {
						if (destination.contains(DEFAULT_PROPERTIES)) {
							destination.addBefore(DEFAULT_PROPERTIES, source);
						}
						else {
							//第一次进入: 把application-dev.yml至于最低优先级
							destination.addLast(source);
						}
					}
					else {
						//第二次进入:
						//让 application.yml 优先级比 application-dev.yml 低
						destination.addAfter(lastAdded, source);
					}
					//第一次遍历结束: lastAdded: application-dev
					lastAdded = source.getName();
				}
			}
		}
 执行后得到各自的优先级:

   systemProperties优先级高,解析器会优先使用 systemProperties中的 server.port 配置项即 5555 所以最终Tomcat 启动端口是 5555
  从中也可以看出,如果application.yml 和 application-dev.yml中有相同的配置项,会优先采用application-dev.yml中的配置项。
  
参考:
0.《Spring源码深度解析》
  • 大小: 12.8 KB
  • 大小: 54.6 KB
  • 大小: 9.8 KB
  • 大小: 87.3 KB
  • 大小: 44.9 KB
  • 大小: 34.6 KB
分享到:
评论

相关推荐

    springboot入门实例2:springboot配置文件的位置和加载优先级

    springboot入门实例2:springboot配置文件的位置和加载优先级 博客地址:https://blog.csdn.net/u010476739/article/details/98380095

    SpringBoot内部外部配置文件加载顺序解析

    首先,SpringBoot会从内部和外部两个方面加载配置文件。内部配置文件是指项目内部的配置文件,而外部配置文件是指项目外部的配置文件。 内部配置文件加载顺序: 1. file:./config/下的application.properties或...

    SpringBoot-2.7.6读取配置文件的方式

    配置文件的加载遵循以下优先级(从高到低): 1. 命令行参数(`--key=value`) 2. `spring.config.location`指定的文件 3. `@SpringBootTest`注解中的`value`属性 4. 环境变量 5. 系统属性(`System.getProperties...

    springboot配置文件的加载顺序解析

    在SpringBoot中,配置文件的加载顺序是按照优先级从高到低的顺序进行加载的。下面是按照优先级从高到低的顺序列出的配置文件加载顺序: 1. file:./config/ 2. file:./classpath:/config/ 3. classpath:/config/ ...

    SpringBoot第 5 讲:SpringBoot+properties配置文件读取

    在SpringBoot框架中,`properties`文件的读取和管理变得更加简便,这得益于SpringBoot对自动化配置的强大支持。 1. **SpringBoot与Properties配置文件** SpringBoot鼓励使用`application.properties`或`...

    【SpringBoot】YAML 配置文件.doc

    在本文中,我们将详细介绍 YAML 配置文件的基础知识、优点、语法规则、格式、数组数据、配置文件类型、优先级、代码提示功能和读取 YAML 配置文件中的数据。 一、YAML 简介 YAML(YAML Ain’t Markup Language)是...

    Springboot引用外部配置文件的方法步骤

    Springboot 引用外部配置文件的方法步骤是指在 Springboot 项目中如何从外部配置文件中读取配置信息的方法。这种方法可以让我们在不修改项目代码的情况下对配置信息进行修改。 首先,我们可以通过命令行参数的方式...

    Spring Boot多模块配置文件读取

    - 当项目规模进一步扩大时,可以考虑使用Spring Cloud Config Server集中管理所有模块的配置,实现配置的远程加载和统一管理。 总之,Spring Boot的多模块配置文件读取策略旨在提高项目的可扩展性和可维护性。通过...

    web.config详解(配置文件的查找优先级)

    本文旨在深入解析`web.config`文件及其查找优先级的细节,帮助开发者更好地理解和掌握该配置文件的应用场景及原理。 #### 一、web.config 文件概述 `web.config`文件是ASP.NET应用程序的核心配置文件,它是一种XML...

    详解Spring Boot读取配置文件与配置文件优先级

    在加载配置文件时, Spring Boot 会按照顺序加载配置文件,如果多个配置文件中出现相同的配置项,那么后加载的配置文件将覆盖前面的配置文件。 Spring Boot 提供了多种方式来读取配置文件,包括通过注入 ...

    springBoot+jsp源码实例

    在Spring Boot中,配置文件的优先级高于默认配置,可以覆盖默认值。 关于"压缩包子文件的文件名称列表":springboot_jsp可能是一个目录或者压缩文件的名字,这通常包含整个项目的所有源代码、资源文件、配置文件等...

    springboot:springboot源码分析

    配置文件的加载顺序和优先级也是分析的重点。 5. **条件注解(Conditional Annotation)** - 如`@ConditionalOnClass`, `@ConditionalOnMissingBean`等,这些注解允许SpringBoot在满足特定条件时才进行配置,增强...

    SpringBoot源码解析——BeanDefinitionLoader 源码分析.docx

    在源码分析中,我们首先看到`prepareContext`方法被调用,这是`SpringApplication`类中的一个重要方法,它负责初始化`ApplicationContext`(应用上下文)并为后续的bean加载做准备。 `prepareContext`方法做了以下...

    SpringBoot源码初学者(一):SpringBoot功能扩展接口的使用与源码分析

    这个抽象类提供了加载工厂类的机制,是SpringBoot实现自动配置和扩展的关键。 ### 三、系统初始化器源码解析 系统初始化器通常通过`spring.factories`文件来注册,SpringApplication会遍历并调用这些初始化器的`...

    SpringBoot常见错误及解决方法1

    解决 Spring Boot 中的常见错误需要了解 Spring Boot 的配置加载过程和优先级问题。 配置加载过程 Spring Boot 的配置加载有着约定俗成的步骤: 1. 从 resources 目录下加载 application.properties/application....

    SpringBoot配置文件的加载位置实例详解

    这篇内容将深入探讨SpringBoot配置文件的加载位置和规则。 首先,SpringBoot默认会查找`application.yml`或`application.properties`作为主配置文件。这些文件可以在以下几个位置被加载: 1. 当前工作目录的`./...

    Springboot为什么加载不上application.yml的配置文件

    综上所述,排查Spring Boot无法加载`application.yml`的问题需要从多个角度入手,包括检查配置文件的位置、类路径设置、IDE配置、打包设置以及日志输出。通过细致的分析和调试,通常可以找到问题所在并解决。

    SpringBoot自动配置原理.docx

    总结,Spring Boot的自动配置原理基于对类路径、配置文件和条件注解的智能分析,实现了高度自动化和灵活的配置管理。理解这些核心概念有助于我们更好地利用Spring Boot,提高开发效率,并且便于维护和扩展我们的应用...

    让spring解决控制springboot中bean的加载顺序的问题.docx

    Spring Boot遵循"约定优于配置"的理念,简化了配置,但同时也带来了需要手动干预Bean加载顺序的问题。本文将探讨为什么需要控制Bean加载顺序、常见的误解,以及如何有效地实现Bean的加载顺序控制。 1. 为什么需要...

    SpringBoot 50道面试题和答案.docx

    SpringBoot 的自动配置原理主要依赖于`@EnableAutoConfiguration`注解,它会从`META-INF/spring.factories`文件中加载自动配置类。这些类会根据类路径中是否存在某些类(通过`@ConditionalOnClass`注解判断)来决定...

Global site tag (gtag.js) - Google Analytics