Springboot中PropertySource的结构与加载过程逐步分析讲解

 更新时间:2023年01月06日 11:06:25   作者:起风哥  
本文重点讲解一下Spring中@PropertySource注解的使用,PropertySource主要是对属性源的抽象,包含属性源名称name和属性源内容对象source。其方法主要是对这两个字段进行操作

记得之前写过一篇文章分析spring BeanFactory的时候说过的spring当中设计很经典的一个点就是 “读写分离” 模式。使用这个模式可以很好的区分开框架与业务的使用上的侧重点。业务层不应该具有修改框架的特性。

所以讲Propertysource我们从Environment开始讲。我们知道我们平时在项目中拿到的Environment对象是只读,但是它可以被转换成可写的对象。

在springboot中当我们启动一个servlet应用的时候在prepareEnvironment 阶段实际上是new了一个StandardServletEnvironment

此时调用构造函数放了四个propertysource进去

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

super.customizePropertySources(propertySources)

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

对应的名称分别为

  • servletContextInitParams
  • servletConfigInitParams
  • jndiProperties 可选
  • systemEnvironment
  • systemProperties

对早期项目熟悉的同学可能,通过这几个参数能立马知道他们是如何演变过来的。

早期的servlet项目中有个web.xml配置(那么springboot是如何让它消失的呢?思考下)。这个配置中有这样的标签

 <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/springMVC-servlet.xml</param-value>
 </context-param>
<servlet>
        <servlet-name>springMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
                 <param-name>home-page</param-name>
                 <param-value>home.jsp</param-value>
        </init-param>
  </servlet>

这些参数是被servlet容器所解析的,同时也对spring进行了映射,包括jndi配置,即你在容器层面做的配置最终也会被映射到environment中。此处不是我们当前的重点不展开。

现在我们先来看看PropertySource这个类

PropertySource是个抽象类代表name/value键值对的一个资源,使用了泛型可以代表任意对象类型,例如可以是java.util.Properties,也可以是java.util.Map等

PropertySource对象通常不单独使用,而是通过对象聚合资源属性,结合PropertyResolver实现来解析资源对象,并根据优先级进行搜索。

可以使用@PropertySource 注解将对应的PropertySource 加入到Enviroment

public abstract class PropertySource<T> {
	protected final Log logger = LogFactory.getLog(getClass());
	protected final String name;
	protected final T source;
	public PropertySource(String name, T source) {
		Assert.hasText(name, "Property source name must contain at least one character");
		Assert.notNull(source, "Property source must not be null");
		this.name = name;
		this.source = source;
	}
	@SuppressWarnings("unchecked")
	public PropertySource(String name) {
		this(name, (T) new Object());
	}
	public String getName() {
		return this.name;
	}
	public T getSource() {
		return this.source;
	}
	public boolean containsProperty(String name) {
		return (getProperty(name) != null);
	}
	@Nullable
	public abstract Object getProperty(String name);
	@Override
	public boolean equals(Object other) {
		return (this == other || (other instanceof PropertySource &&
				ObjectUtils.nullSafeEquals(this.name, ((PropertySource<?>) other).name)));
	}
	@Override
	public int hashCode() {
		return ObjectUtils.nullSafeHashCode(this.name);
	}
	@Override
	public String toString() {
		if (logger.isDebugEnabled()) {
			return getClass().getSimpleName() + "@" + System.identityHashCode(this) +
					" {name='" + this.name + "', properties=" + this.source + "}";
		}
		else {
			return getClass().getSimpleName() + " {name='" + this.name + "'}";
		}
	}
	public static PropertySource<?> named(String name) {
		return new ComparisonPropertySource(name);
	}
	//StubPropertySource内部类,存根PropertySouece 目的是为了,延迟加载。
	//即有些propertysource使用了一些占位符号,不能早于application context 加载,此时需要进行存根。
	//等到对应的资源加载之后再加载当前的propertysource。占位符会在容器的refresh阶段被替换,
	//具体解析可以查看AbstractApplicationContext#initPropertySources()
	public static class StubPropertySource extends PropertySource<Object> {
		public StubPropertySource(String name) {
			super(name, new Object());
		}
		/**
		 * Always returns {@code null}.
		 */
		@Override
		@Nullable
		public String getProperty(String name) {
			return null;
		}
	}
	//静态内部类为了named方法使用,仅仅用户比较,调用其它方法会报异常,这里是个适配器模式。
	static class ComparisonPropertySource extends StubPropertySource {
		private static final String USAGE_ERROR =
				"ComparisonPropertySource instances are for use with collection comparison only";
		public ComparisonPropertySource(String name) {
			super(name);
		}
		@Override
		public Object getSource() {
			throw new UnsupportedOperationException(USAGE_ERROR);
		}
		@Override
		public boolean containsProperty(String name) {
			throw new UnsupportedOperationException(USAGE_ERROR);
		}
		@Override
		@Nullable
		public String getProperty(String name) {
			throw new UnsupportedOperationException(USAGE_ERROR);
		}
	}
}

我们可以看到它预留了一个抽象方法getProperty 给子类实现,而此方法就是如何获取每个propertysource中的属性的value,此时就可以有各种各样的实现方式

  • 例如:在SystemEnvironmentPropertySource 中 调用父类MapPropertySource 的getProperty方法实际是调用map.get方法获取对应的属性值
  • 例如:CommandLinePropertySource中实际是调用CommandLineArgs的getNonOptionArgs()与getOptionValues(name)方法获取对应的属性值
  • 例如:apollo实现的ConfigPropertySource实际上是调用System.getProperty(key);

以及Properties对象的get方法获取的属性值。

了解完这个结构之后我们后面再去看配置中心的实现,看起来就容易理解多了,此处按下不表。

其它的不多介绍,具体的类层次结构大家自行观察。大体上最后的数据结构基本上都是从hash表中获取对应的键值对。

了解完propertysource的数据结构之后,那么问题来了springboot什么时候加载了配置文件呢?又是如何解析成对应的propertysource呢?带着这个问题我们将整个流程贯穿起来看看就知道了。

所以我们先来看看ConfigFileApplicationListener这个类,如果你问我为什么看这个类,我会告诉你你可以全局内容搜索application.properties,当然最好是你有初略过了一遍springboot源码在来看会比较好。

ConfigFileApplicationListener实现了EnvironmentPostProcessor以及SmartApplicationListener这两个接口。我们知道实现了ApplicationListener接口的类会在spring启动阶段接收到各个环节的事件,所以我们直接查看onApplicationEvent方法

public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationEnvironmentPreparedEvent) {
			onApplicationEnvironmentPreparedEvent(
					(ApplicationEnvironmentPreparedEvent) event);
		}
		if (event instanceof ApplicationPreparedEvent) {
			onApplicationPreparedEvent(event);
		}
	}

我们发现做了两个环节的处理,一个是环境装备完成的时候处理了一次,一个是容器准备完成时处理了一次,这两次的事件的执行时机分别如下

ApplicationEnvironmentPreparedEvent:prepareEnvironment

onApplicationPreparedEvent:prepareContext

在启动过程中prepareEnvironment 先执行所以这个事件的执行顺序为代码的逻辑顺序,先进第一个if条件再进第二个条件。

具体来看这两个方法

先看onApplicationEnvironmentPreparedEvent,遍历调用了一轮postProcessor.postProcessEnvironment

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

当前类的postPorcessEnvironment方法添加了个RandomValuePropertySource,并并且new Loader 调用load方法在load方法中加载了application.properties文件,其它的逻辑就是如何找到这个文件以及如何加载这个文件具体细节自行研究,不多解释,加载的时候用到了PropertySourceLoader,对应的PropertySourceLoader有不同的实现,扩展名properties,xml使用PropertiesPropertySourceLoader 解析,而

“yml”, "yaml"使用YamlPropertySourceLoader加载,加载完成后就包装成MapPropertySource子类。并且将其设置给Environment。

public void load() {
			this.profiles = new LinkedList<>();
			this.processedProfiles = new LinkedList<>();
			this.activatedProfiles = false;
			this.loaded = new LinkedHashMap<>();
			//初始化默认的profile=default
			initializeProfiles();
			while (!this.profiles.isEmpty()) {
				Profile profile = this.profiles.poll();
				if (profile != null && !profile.isDefaultProfile()) {
					addProfileToEnvironment(profile.getName());
				}
				load(profile, this::getPositiveProfileFilter,
						addToLoaded(MutablePropertySources::addLast, false));
				this.processedProfiles.add(profile);
			}
			resetEnvironmentProfiles(this.processedProfiles);
			load(null, this::getNegativeProfileFilter,
					addToLoaded(MutablePropertySources::addFirst, true));
			addLoadedPropertySources();
		}

接着我们来看第二个事件的方法

第二个事件的方法添加了一个BeanFactoryPostProcessor 为PropertySourceOrderingPostProcessor,而BeanFactoryPostProcessor 是再refresh的InvokeBeanFactoryPostProcessor 阶段执行的。我们先看看它是如何执行的,postProcessBeanFactory调用了如下方法

private void reorderSources(ConfigurableEnvironment environment) {
			PropertySource<?> defaultProperties = environment.getPropertySources()
					.remove(DEFAULT_PROPERTIES);
			if (defaultProperties != null) {
				environment.getPropertySources().addLast(defaultProperties);
			}
		}

这个方法做了一件神奇的事情,因为默认配置是最先被放到环境容器中的,所以它在最前面,所以后续往里又添加了很多其它的propertysource之后,需要将它移动到最后,做一个兜底策略,最终就是取不到配置了再去取默认配置。

在结合开始的时候的数据结构,大概我们就可以总结出如下过程

1、环境准备阶段,广播了环境准备完成事件

2、调用listener方法onApplicationEvent去初始化了application.properties文件

3、使用PropertySourceLoader解析对应的文件并包装成propertysource

4、将propertysource设置给environment

5、容器准备阶段,广播了容器准备完成事件

6、调用listener方法onApplicationEvent去设置了一个BeanfactoryPostProcessor

7、在refresh阶段调用了这个postProcessor,调整了下默认配置文件的顺序。

具体的文件解析和占位符替换等等这些动作这里先不介绍了。

到此这篇关于Springboot中PropertySource的结构与加载过程逐步分析讲解的文章就介绍到这了,更多相关Springboot PropertySource内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java 创建线程的几种方式

    java 创建线程的几种方式

    本文主要介绍了java中创建线程的几种方式。具有很好的参考价值,下面跟着小编一起来看下吧
    2017-02-02
  • Java数据结构与算法之树(动力节点java学院整理)

    Java数据结构与算法之树(动力节点java学院整理)

    这篇文章主要介绍了Java数据结构与算法之树的相关知识,最主要的是二叉树中的二叉搜索树,需要的朋友可以参考下
    2017-04-04
  • 浅谈java反射和自定义注解的综合应用实例

    浅谈java反射和自定义注解的综合应用实例

    本篇文章主要介绍了java反射和自定义注解的综合应用,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • springboot整合vue2-uploader实现文件分片上传、秒传、断点续传功能

    springboot整合vue2-uploader实现文件分片上传、秒传、断点续传功能

    对于大文件的处理,无论是用户端还是服务端,如果一次性进行读取发送、接收都是不可取,很容易导致内存问题,下面这篇文章主要给大家介绍了关于springboot整合vue2-uploader实现文件分片上传、秒传、断点续传功能的相关资料,需要的朋友可以参考下
    2023-06-06
  • SpringBoot实现服务接入nacos注册中心流程详解

    SpringBoot实现服务接入nacos注册中心流程详解

    这篇文章主要介绍了SpringBoot实现服务接入nacos注册中心流程,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2023-01-01
  • SpringBoot集成Memcached的项目实践

    SpringBoot集成Memcached的项目实践

    Memcached是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载,本文主要介绍了SpringBoot集成Memcached的项目实践,具有一定的参考价值,感兴趣的可以了解一下
    2024-01-01
  • 如何通过Java打印Word文档

    如何通过Java打印Word文档

    这篇文章主要介绍了如何通过Java打印Word文档,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-02-02
  • 解决	Spring RestTemplate post传递参数时报错问题

    解决 Spring RestTemplate post传递参数时报错问题

    本文详解说明了RestTemplate post传递参数时报错的问题及其原由,需要的朋友可以参考下
    2020-02-02
  • Java多线程按指定顺序同步执行

    Java多线程按指定顺序同步执行

    这篇文章主要介绍了java多线程如何按指定顺序同步执行,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-10-10
  • Maven统一版本管理的实现

    Maven统一版本管理的实现

    在使用Maven多模块结构工程时,配置版本是一个比较头疼的事,本文主要介绍了Maven统一版本管理的实现,具有一定的参考价值,感兴趣的可以了解一下
    2024-03-03

最新评论