【Spring】从 PropertyResolver Environment 等类谈一种设计思维

116 阅读6分钟

【Spring】从 PropertyResolver Environment 等类谈一种设计思维

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

我喜欢 Spring 最大的一个原因就是它的设计思维,回归了编程的本质而不是功能的堆砌。同时,精巧的接口、类设计为 Spring 的拓展提供了无限的可能

一直以来,我都尽可能地试图将这些设计思维用在自己的代码中,并加以理解与拓展

本文就 PropertyResolver Environment 相关类谈一种 Spring 的接口设计原则

PropertyResolver

public interface PropertyResolver {

	// 属性是否存在
	boolean containsProperty(String key);

	// 获取属性相关
	@Nullable
	String getProperty(String key);
	String getProperty(String key, String defaultValue);
	@Nullable
	<T> T getProperty(String key, Class<T> targetType);
	<T> T getProperty(String key, Class<T> targetType, T defaultValue);
	String getRequiredProperty(String key) throws IllegalStateException;
	<T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;

	// 占位符解析
	String resolvePlaceholders(String text);
	String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;

}

PropertyResolver,属性解析相关,顶层接口

  • 首先作为顶层接口,它提供了这个 属性解析 最基本的方法:判断属性是否存在、属性获取、占位符解析
  • 不难猜测,暴露出这么多的 属性获取 相关方法,最终实现肯定是收口到一个方法中的,这便是一种 调用者友好 的设计原则

ConfigurablePropertyResolver

public interface ConfigurablePropertyResolver extends PropertyResolver {

	// 转换服务相关
	ConfigurableConversionService getConversionService();
	void setConversionService(ConfigurableConversionService conversionService);

	// 前缀 后缀 默认值分隔符
	void setPlaceholderPrefix(String placeholderPrefix);
	void setPlaceholderSuffix(String placeholderSuffix);
	void setValueSeparator(@Nullable String valueSeparator);

	// 是否允许占位符解析失败
	void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders);

	// 必选项配置
	void setRequiredProperties(String... requiredProperties);

	// 必选项校验
	void validateRequiredProperties() throws MissingRequiredPropertiesException;

}
Configurable:可配置的
  • 这是 Spring 最常用的接口设计模式之一:ConfigurableXXX
  • 可以看到,PropertyResolver 作为顶层接口只提供了最核心的基础 API,而对于整个 PropertyResolver 组件的配置相关方法就被抽到子接口 ConfigurablePropertyResolver 中,职责分明
  • 该接口允许配置 ConversionService 相关,因为属性获取涉及类型转换(从 PropertyResolver 提供的方法也能看出来)
  • 允许配置 占位符 相关属性,因为 PropertyResolver 支持占位符解析
关于 ConversionService,可以下文了解

【源码】Spring —— Convert 解读

AbstractPropertyResolver

这里也是 Spring 常用的一种设计原则:当对应的接口抽象出来后,Spring 通常会提供一个抽象类,将所有共有的部分实现出来,对于 “规范”实现 和 “默认”实现 一般会以 publicprotected 语义标识

个人理解:
“规范”实现:原则上的最佳实践,一般情况下并不认为需要子类复写
“默认”实现:Spring 仅仅提供一种缺省实现,通常主张或允许子类去复写

比如该抽象实现中

  • 首先定义了所有可配置属性,比如 ConfigurableConversionService 占位符前后缀 等,同时实现对应的配置方法
  • 正如前面提到的,所有 getProperty 方法(包括 containsProperty)被最终收口到 <T> T getProperty(String key, Class<T> targetType) 中,不难理解,毕竟这是功能最强大的方法(同时也是调用最不“友好”的方法)。同时,该类并不实现该方法,而是由子类个性化实现:这一下子,该“死”的地方“死”,该“活”的地方“活”,有没有体会到这种设计的美感
  • 占位符的解析收口到方法 doResolvePlaceholders,其本质是委托给 PropertyPlaceholderHelper,并暴露抽象方法 getPropertyAsRawString 供子类自定义指定占位符解析数据源
  • 此类额外提供方法 convertValueIfNecessary,即属性类型转换的默认实现:交给 ConversionService 实现,允许子类复写
关于 PropertyPlaceholderHelper,可以参考下文

【Spring】PropertyPlaceholderHelper —— 占位符解析工具类

关于上述细节的源码,此处不再张贴,可以自行查看

PropertySourcesPropertyResolver

再往下,职责就很清晰了,复写(实现)对应的方法,根据不同的需求实现不同的子类即可,对于此类,Spring 只提供了一个实现类 PropertySourcesPropertyResolver

  • 指定了属性源为 PropertySources,本质就是一组有序的 PropertySource
  • 对应的方法实现基于 PropertySources,见代码
	// 基于 getProperty 实现
	@Override
	@Nullable
	protected String getPropertyAsRawString(String key) {
		return getProperty(key, String.class, false);
	}

	@Nullable
	protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
		if (this.propertySources != null) {

			// 遍历 propertySources,返回第一个不为 null 的属性
			for (PropertySource<?> propertySource : this.propertySources) {
				if (logger.isTraceEnabled()) {
					logger.trace("Searching for key '" + key + "' in PropertySource '" +
							propertySource.getName() + "'");
				}

				// 委托给 PropertySource#getProperty
				Object value = propertySource.getProperty(key);
				if (value != null) {
					/**
					 * 对于解析的属性是支持 占位符 解析的
					 * 当然 getPropertyAsRawString 是获取占位符元素属性的
					 * 		不需要再次解析
					 */
					if (resolveNestedPlaceholders && value instanceof String) {
						value = resolveNestedPlaceholders((String) value);
					}
					logKeyFound(key, propertySource, value);
					
					// 最后对于类型转换由 convertValueIfNecessary 实现
					return convertValueIfNecessary(value, targetValueType);
				}
			}
		}
		if (logger.isTraceEnabled()) {
			logger.trace("Could not find key '" + key + "' in any property source");
		}
		return null;
	}
  • getProperty 方法本质是委托给 PropertySource#getProperty,基于 PropertySources 的有序性,返回第一个不为 null 的解析结果
  • PropertySources 中获取的属性,会进行 占位符 解析
  • 最后如果类型不匹配,由 convertValueIfNecessary 继续转换
  • 职责层层分离后,一个干净又强大的属性解析类 PropertySourcesPropertyResolver 就实现了,如果我们有需要,自然也可以自定义拓展,让整个框架富有生命力

Environment

public interface Environment extends PropertyResolver {

	String[] getActiveProfiles();
	String[] getDefaultProfiles();

	@Deprecated
	boolean acceptsProfiles(String... profiles);
	boolean acceptsProfiles(Profiles profiles);

}

Environment 也是 Spring 的一个核心分支,顶层接口在 PropertyResolver 的基础上拓展了 profile 相关方法

ConfigurableEnvironment

老套路了,Environment 的配置化拓展,其中 getPropertySources 值得一提:返回内部维护的 MutablePropertySources 属性集 ,因而我们可以自定义属性集,拓展 Spring 对应环境属性

AbstractEnvironment

再就是 Spring 提供的抽象实现类了,可以发现,这种设计模式是很实用的,因此 Spring 的应用不在少数

值得一提的是 AbstractEnvironment 的构造方法:

	public AbstractEnvironment() {
		customizePropertySources(this.propertySources);
	}

它允许所有的子类通过复写 customizePropertySources 方法来自定义拓展 propertySources,也是一个让人眼前一亮的设计。比如最常用的 systemEnvironment systemProperties 属性集,就是子类 StandardEnvironment 维护的

总结

总结一下提到的 设计思维

  • 抽象出来的方法尽可能的对 调用者友好,具体的细节可以内部消化
  • ConfigurableXXX 的层级设计,实现接口配置层的 “读写分离”
  • 提供合适的 抽象类,定义方法地规范实现,同时提供良好的 可拓展性
  • 职责分明,具体的行为委托给对应的类实现,也是一种 策略模式 的应用
抛砖引玉,欢迎讨论