专栏首页BAT的乌托邦关于Spring属性处理器PropertyResolver以及应用运行环境Environment的深度分析,强大的StringValueResolver使用和解析【享学Spring】

关于Spring属性处理器PropertyResolver以及应用运行环境Environment的深度分析,强大的StringValueResolver使用和解析【享学Spring】

前言

若直接提PropertyResolver或者StringValueResolver可能很小伙伴会觉得非常的陌生,但是我若提EnvironmentEmbeddedValueResolverAware这个感知接口,相信大部分小伙伴就能感受到一种亲切感了~。

我们的任何一个Spring Bean若实现了EmbeddedValueResolverAware接口,Spring容器在启动的时候就会自动给我们我们的Bean注入进来一个StringValueResolver。然后我们借用这个resolver就能处理一系列字符串的逻辑比如:占位符解释、SpEL计算等等~

本文就是以StringValueResolver为引子,去剖析它的底层依赖逻辑:PropertyResolverEnvironment

PropertyResolver

org.springframework.core.env.PropertyResolver此接口用于在底层源之上解析一系列的属性值:例如properties文件,yaml文件,甚至是一些nosql(因为nosql也是k-v形式)。

接口中定义了一系列读取,解析,判断是否包含指定属性的方法:

// @since 3.1  出现得还是相对较晚的  毕竟SpEL也3.0之后才出来嘛~~~
public interface PropertyResolver {

	// 查看规定指定的key是否有对应的value   注意:若对应值是null的话 也是返回false
	boolean containsProperty(String key);
	// 如果没有则返回null
	@Nullable
	String getProperty(String key);
	// 如果没有则返回defaultValue
	String getProperty(String key, String defaultValue);
	// 返回指定key对应的value,会解析成指定类型。如果没有对应值则返回null(而不是抛错~)
	@Nullable
	<T> T getProperty(String key, Class<T> targetType);
	<T> T getProperty(String key, Class<T> targetType, T defaultValue);

	// 若不存在就不是返回null了  而是抛出异常~  所以不用担心返回值是null
	String getRequiredProperty(String key) throws IllegalStateException;
	<T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;

	// 解析${...}这种类型的占位符,把他们替换为使用getProperty方法返回的结果,解析不了并且没有默认值的占位符会被忽略(原样输出)
	String resolvePlaceholders(String text);
	// 解析不了就抛出异常~
	String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;

}

它的继承链可以描述如下:PropertyResolver -> ConfigurablePropertyResolver -> AbstractPropertyResolver -> PropertySourcesPropertyResolver

当然还有Environment分支,这里放在下面Environment章节继续解释说明~

ConfigurablePropertyResolver

顾名思义,它是一个可配置的处理器。这个方法不仅有父接口所有功能,还扩展定义类型转换、属性校验、前缀、后缀、分隔符等一些列的功能,这个在具体实现类里有所体现~

public interface ConfigurablePropertyResolver extends PropertyResolver {

	// 返回在解析属性时使用的ConfigurableConversionService。此方法的返回值可被用户定制化set
	// 例如可以移除或者添加Converter  cs.addConverter(new FooConverter());等等
	ConfigurableConversionService getConversionService();
	// 全部替换ConfigurableConversionService的操作(不常用)  一般还是get出来操作它内部的东东
	void setConversionService(ConfigurableConversionService conversionService);

	// 设置占位符的前缀  后缀    默认是${}
	void setPlaceholderPrefix(String placeholderPrefix);
	void setPlaceholderSuffix(String placeholderSuffix);
	// 默认值的分隔符   默认为冒号:
	void setValueSeparator(@Nullable String valueSeparator);
	// 是否忽略解析不了的占位符,默认是false  表示不忽略~~~(解析不了就抛出异常)
	void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders);

	/**
	 * Specify which properties must be present, to be verified by
	 * {@link #validateRequiredProperties()}.
	 */
	void setRequiredProperties(String... requiredProperties);
	void validateRequiredProperties() throws MissingRequiredPropertiesException;
}

ConfigurableXXX成了Spring的一种命名规范,或者说是一种设计模式。它表示课配置的,所以都会提供大量的set方法

Spring很多接口都是读写分离的,最顶层接口一般都只会提供只读方法,这是Spring框架设计的一般规律之一

AbstractPropertyResolver

它是对ConfigurablePropertyResolver的一个抽象实现,实现了了所有的接口方法,并且只提供一个抽象方法给子类去实现~~~

// @since 3.1
public abstract class AbstractPropertyResolver implements ConfigurablePropertyResolver {
	@Nullable
	private volatile ConfigurableConversionService conversionService;

	// PropertyPlaceholderHelper是一个极其独立的类,专门用来解析占位符  我们自己项目中可议拿来使用  因为它不依赖于任何其他类
	@Nullable
	private PropertyPlaceholderHelper nonStrictHelper;
	@Nullable
	private PropertyPlaceholderHelper strictHelper;
	
	private boolean ignoreUnresolvableNestedPlaceholders = false;
	private String placeholderPrefix = SystemPropertyUtils.PLACEHOLDER_PREFIX;
	private String placeholderSuffix = SystemPropertyUtils.PLACEHOLDER_SUFFIX;
	@Nullable
	private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR;
	private final Set<String> requiredProperties = new LinkedHashSet<>();

	// 默认值使用的DefaultConversionService
	@Override
	public ConfigurableConversionService getConversionService() {
		// Need to provide an independent DefaultConversionService, not the
		// shared DefaultConversionService used by PropertySourcesPropertyResolver.
		ConfigurableConversionService cs = this.conversionService;
		if (cs == null) {
			synchronized (this) {
				cs = this.conversionService;
				if (cs == null) {
					cs = new DefaultConversionService();
					this.conversionService = cs;
				}
			}
		}
		return cs;
	}
	... // 省略get/set
	@Override
	public void setRequiredProperties(String... requiredProperties) {
		for (String key : requiredProperties) {
			this.requiredProperties.add(key);
		}
	}
	// 校验这些key~
	@Override
	public void validateRequiredProperties() {
		MissingRequiredPropertiesException ex = new MissingRequiredPropertiesException();
		for (String key : this.requiredProperties) {
			if (this.getProperty(key) == null) {
				ex.addMissingRequiredProperty(key);
			}
		}
		if (!ex.getMissingRequiredProperties().isEmpty()) {
			throw ex;
		}
	}
	... //get/set property等方法省略  直接看处理占位符的方法即可
	@Override
	public String resolvePlaceholders(String text) {
		if (this.nonStrictHelper == null) {
			this.nonStrictHelper = createPlaceholderHelper(true);
		}
		return doResolvePlaceholders(text, this.nonStrictHelper);
	}
	private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) {
		return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix, this.valueSeparator, ignoreUnresolvablePlaceholders);
	}
	// 此处:最终都是委托给PropertyPlaceholderHelper去做  而getPropertyAsRawString是抽象方法  根据key返回一个字符串即可~
	private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
		return helper.replacePlaceholders(text, this::getPropertyAsRawString);
	}
}

最终,处理占位的核心逻辑在PropertyPlaceholderHelper身上,这个类不可小觑,是一个与业务无关非常强大的工具类,我们可以直接拿来主义~


PropertyPlaceholderHelper

将字符串里的占位符内容,用我们配置的properties里的替换。这个是一个单纯的类,没有继承没有实现,而且简单无依赖,没有依赖Spring框架其他的任何类。 个人感觉自己项目中可以拿来模仿或者直接使用。

// @since 3.0  Utility class for working with Strings that have placeholder values in them
public class PropertyPlaceholderHelper {

	// 这里保存着  通用的熟悉的 开闭的符号们~~~
	private static final Map<String, String> wellKnownSimplePrefixes = new HashMap<>(4);
	static {
		wellKnownSimplePrefixes.put("}", "{");
		wellKnownSimplePrefixes.put("]", "[");
		wellKnownSimplePrefixes.put(")", "(");
	}

	private final String placeholderPrefix;
	private final String placeholderSuffix;
	private final String simplePrefix;
	@Nullable
	private final String valueSeparator;
	private final boolean ignoreUnresolvablePlaceholders; // 是否采用严格模式~~

	// 从properties里取值  若你有就直接从Properties里取值了~~~
	public String replacePlaceholders(String value, final Properties properties)  {
		Assert.notNull(properties, "'properties' must not be null");
		return replacePlaceholders(value, properties::getProperty);
	}

	// @since 4.3.5   抽象类提供这个类型转换的方法~ 需要类型转换的会调用它
	// 显然它是委托给了ConversionService,而这个类在前面文章已经都重点分析过了~
	@Nullable
	protected <T> T convertValueIfNecessary(Object value, @Nullable Class<T> targetType) {
		if (targetType == null) {
			return (T) value;
		}
		ConversionService conversionServiceToUse = this.conversionService;
		if (conversionServiceToUse == null) {
			// Avoid initialization of shared DefaultConversionService if
			// no standard type conversion is needed in the first place...
			if (ClassUtils.isAssignableValue(targetType, value)) {
				return (T) value;
			}
			conversionServiceToUse = DefaultConversionService.getSharedInstance();
		}
		return conversionServiceToUse.convert(value, targetType);
	}

	// 这里会使用递归,根据传入的符号,默认值等来处理~~~~
	protected String parseStringValue(String value, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) { ... }
}

这个工具类不仅仅用在此处,在ServletContextPropertyUtilsSystemPropertyUtilsPropertyPlaceholderConfigurer里都是有使用到它的


PropertySourcesPropertyResolver

从上面知道AbstractPropertyResolver封装了解析占位符的具体实现。PropertySourcesPropertyResolver作为它的子类它只需要提供数据源,所以它主要是负责提供数据源。

// @since 3.1    PropertySource:就是我们所说的数据源,它是Spring一个非常重要的概念,比如可以来自Map,来自命令行、来自自定义等等~~~
public class PropertySourcesPropertyResolver extends AbstractPropertyResolver {

	// 数据源们~
	@Nullable
	private final PropertySources propertySources;

	// 唯一构造函数:必须制定数据源~
	public PropertySourcesPropertyResolver(@Nullable PropertySources propertySources) {
		this.propertySources = propertySources;
	}

	@Override
	public boolean containsProperty(String key) {
		if (this.propertySources != null) {
			for (PropertySource<?> propertySource : this.propertySources) {
				if (propertySource.containsProperty(key)) {
					return true;
				}
			}
		}
		return false;
	}
	...
	//最终依赖的都是propertySource.getProperty(key);方法拿到如果是字符串的话
	//就继续交给 value = resolveNestedPlaceholders((String) value);处理
	@Nullable
	protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
		if (this.propertySources != null) {
			for (PropertySource<?> propertySource : this.propertySources) {
				Object value = propertySource.getProperty(key);
				if (value != null) {
					// 若值是字符串,那就处理一下占位符~~~~~~  所以我们看到所有的PropertySource都是支持占位符的
					if (resolveNestedPlaceholders && value instanceof String) {
						value = resolveNestedPlaceholders((String) value);
					}
					logKeyFound(key, propertySource, value);
					return convertValueIfNecessary(value, targetValueType);
				}
			}
		}
		return null;
	}
	...
}

PropertySourcesPropertySource属性源是Spring里一个非常重要的概念设计,涉及到Spring属性配置的非常重要的优先级关系、以及它支持的配置类型。

Environment

这个接口代表了当前应用正在运行的环境,为应用的两个重要方面建立抽象模型 【profiles】和【properties】。关于属性访问的方法通过父接口PropertyResolver暴露给客户端使用,本接口主要是扩展出访问【profiles】相关的接口。

对于他俩,我愿意这么来翻译:

  • profiles:配置。它代表应用在一启动时注册到context中bean definitions的命名的逻辑分组。
  • properties:属性。几乎在所有应用中都扮演着重要角色,他可能源自多种源头。例如属性文件,JVM系统属性,系统环境变量,JNDI,servlet上下文参数,Map等等,Environment对象和其相关的对象一起提供给用户一个方便用来配置和解析属性的服务。
// @since 3.1   可见Spring3.x版本是Spirng一次极其重要的跨越、升级
// 它继承自PropertyResolver,所以是对属性的一个扩展~
public interface Environment extends PropertyResolver {
	
	// 就算被激活  也是支持同时激活多个profiles的~
	// 设置的key是:spring.profiles.active
	String[] getActiveProfiles();
	// 默认的也可以有多个  key为:spring.profiles.default
	String[] getDefaultProfiles();

	// 看看传入的profiles是否是激活的~~~~  支持!表示不激活
	@Deprecated
	boolean acceptsProfiles(String... profiles);
	// Spring5.1后提供的  用于替代上面方法   Profiles是Spring5.1才有的一个函数式接口~
	boolean acceptsProfiles(Profiles profiles);

}

我们可以通过实现接口EnvironmentAware或者直接@Autowired可以很方便的得到当前应用的环境:Environment

稍微解释一下:若你是普通的Spring MVC环境(非Boot)也只会有一个Environment的,因为Spring容器内部会判断若你已经实例化过Environment就不会再重复实例化了~

它有如下实现:

ConfigurableEnvironment

扩展出了修改和配置profiles的一系列方法,包括用户自定义的和系统相关的属性。所有的环境实现类也都是它的实现~

// @since 3.1
public interface ConfigurableEnvironment extends Environment, ConfigurablePropertyResolver {
	void setActiveProfiles(String... profiles);
	void addActiveProfile(String profile);
	void setDefaultProfiles(String... profiles);

	// 获取到所有的属性源~  	MutablePropertySources表示可变的属性源们~~~ 它是一个聚合的  持有List<PropertySource<?>>
	// 这样获取出来后,我们可以add或者remove我们自己自定义的属性源了~
	MutablePropertySources getPropertySources();

	// 这里两个哥们应该非常熟悉了吧~~~
	Map<String, Object> getSystemProperties();
	Map<String, Object> getSystemEnvironment();

	// 合并两个环境配置信息~  此方法唯一实现在AbstractEnvironment上
	void merge(ConfigurableEnvironment parent);
}

它会有两个分支:

  1. ConfigurableWebEnvironment:显然它和web环境有关,提供方法void initPropertySources(@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig)让web自己做资源初始化~
  2. AbstractEnvironment:这个是重点,如下~

AbstractEnvironment

它是对环境的一个抽象实现,很重要。

public abstract class AbstractEnvironment implements ConfigurableEnvironment {

	public static final String IGNORE_GETENV_PROPERTY_NAME = "spring.getenv.ignore";
	public static final String ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active";
	public static final String DEFAULT_PROFILES_PROPERTY_NAME = "spring.profiles.default";

	// 保留的默认的profile值   protected final属性,证明子类可以访问
	protected static final String RESERVED_DEFAULT_PROFILE_NAME = "default";


	private final Set<String> activeProfiles = new LinkedHashSet<>();
	// 显然这个里面的值 就是default这个profile了~~~~
	private final Set<String> defaultProfiles = new LinkedHashSet<>(getReservedDefaultProfiles());

	// 这个很关键,直接new了一个 MutablePropertySources来管理属性源们
	// 并且是用的PropertySourcesPropertyResolver来处理里面可能的占位符~~~~~
	private final MutablePropertySources propertySources = new MutablePropertySources();
	private final ConfigurablePropertyResolver propertyResolver = new PropertySourcesPropertyResolver(this.propertySources);

	// 唯一构造方法  customizePropertySources是空方法,交由子类去实现,对属性源进行定制~ 
	// Spring对属性配置分出这么多曾经,在SpringBoot中有着极其重要的意义~~~~
	public AbstractEnvironment() {
		customizePropertySources(this.propertySources);
	}
	// 该方法,StandardEnvironment实现类是有复写的~
	protected void customizePropertySources(MutablePropertySources propertySources) {
	}

	// 若你想改变默认default这个值,可以复写此方法~~~~
	protected Set<String> getReservedDefaultProfiles() {
		return Collections.singleton(RESERVED_DEFAULT_PROFILE_NAME);
	}
	
	//  下面开始实现接口的方法们~~~~~~~
	@Override
	public String[] getActiveProfiles() {
		return StringUtils.toStringArray(doGetActiveProfiles());
	}
	protected Set<String> doGetActiveProfiles() {
		synchronized (this.activeProfiles) {
			if (this.activeProfiles.isEmpty()) { 
		
				// 若目前是empty的,那就去获取:spring.profiles.active
				String profiles = getProperty(ACTIVE_PROFILES_PROPERTY_NAME);
				if (StringUtils.hasText(profiles)) {
					//支持,分隔表示多个~~~且空格啥的都无所谓
					setActiveProfiles(StringUtils.commaDelimitedListToStringArray(
							StringUtils.trimAllWhitespace(profiles)));
				}
			}
			return this.activeProfiles;
		}
	}


	@Override
	public void setActiveProfiles(String... profiles) {
		synchronized (this.activeProfiles) {
			this.activeProfiles.clear(); // 因为是set方法  所以情况已存在的吧
			for (String profile : profiles) {
				 // 简单的valid,不为空且不以!打头~~~~~~~~
				validateProfile(profile);
				this.activeProfiles.add(profile);
			}
		}
	}

	// default profiles逻辑类似,也是不能以!打头~
	@Override
	@Deprecated
	public boolean acceptsProfiles(String... profiles) {
		for (String profile : profiles) {

			// 此处:如果该profile以!开头,那就截断出来  把后半段拿出来看看   它是否在active行列里~~~ 
			// 此处稍微注意:若!表示一个相反的逻辑~~~~~请注意比如!dev表示若dev是active的,我反倒是不生效的
			if (StringUtils.hasLength(profile) && profile.charAt(0) == '!') {
				if (!isProfileActive(profile.substring(1))) {
					return true;
				}
			} else if (isProfileActive(profile)) {
				return true;
			}
		}
		return false;
	}

	// 采用函数式接口处理  就非常的优雅了~
	@Override
	public boolean acceptsProfiles(Profiles profiles) {
		Assert.notNull(profiles, "Profiles must not be null");
		return profiles.matches(this::isProfileActive);
	}

	// 简答的说要么active包含,要门是default  这个profile就被认为是激活的
	protected boolean isProfileActive(String profile) {
		validateProfile(profile);
		Set<String> currentActiveProfiles = doGetActiveProfiles();
		return (currentActiveProfiles.contains(profile) ||
				(currentActiveProfiles.isEmpty() && doGetDefaultProfiles().contains(profile)));
	}

	@Override
	public MutablePropertySources getPropertySources() {
		return this.propertySources;
	}

	public Map<String, Object> getSystemProperties() {
		return (Map) System.getProperties();
	}
	public Map<String, Object> getSystemEnvironment() {
		// 这个判断为:return SpringProperties.getFlag(IGNORE_GETENV_PROPERTY_NAME);
		// 所以我们是可以通过在`spring.properties`这个配置文件里spring.getenv.ignore=false关掉不暴露环境变量的~~~
		if (suppressGetenvAccess()) {
			return Collections.emptyMap();
		}
		return (Map) System.getenv();
	}

	// Append the given parent environment's active profiles, default profiles and property sources to this (child) environment's respective collections of each.
	// 把父环境的属性合并进来~~~~  
	// 在调用ApplicationContext.setParent方法时,会把父容器的环境合并进来  以保证父容器的属性对子容器都是可见的
	@Override
	public void merge(ConfigurableEnvironment parent) {
		for (PropertySource<?> ps : parent.getPropertySources()) {
			if (!this.propertySources.contains(ps.getName())) {
				this.propertySources.addLast(ps); // 父容器的属性都放在最末尾~~~~
			}
		}
		// 合并active
		String[] parentActiveProfiles = parent.getActiveProfiles();
		if (!ObjectUtils.isEmpty(parentActiveProfiles)) {
			synchronized (this.activeProfiles) {
				for (String profile : parentActiveProfiles) {
					this.activeProfiles.add(profile);
				}
			}
		}
		// 合并default
		String[] parentDefaultProfiles = parent.getDefaultProfiles();
		if (!ObjectUtils.isEmpty(parentDefaultProfiles)) {
			synchronized (this.defaultProfiles) {
				this.defaultProfiles.remove(RESERVED_DEFAULT_PROFILE_NAME);
				for (String profile : parentDefaultProfiles) {
					this.defaultProfiles.add(profile);
				}
			}
		}
	}

	// 其余方法全部委托给内置的propertyResolver属性,因为它就是个`PropertyResolver`
	...
}

该抽象类完成了对active、default等相关方法的复写处理。它内部持有一个MutablePropertySources引用来管理属性源。 So,留给子类的活就不多了:只需要把你的属性源注册给我就OK了

StandardEnvironment

这个是Spring应用在非web容器运行的环境。从名称上解释为:标准实现

public class StandardEnvironment extends AbstractEnvironment {

	// 这两个值定义着  就是在@Value注解要使用它们时的key~~~~~
	/** 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";


	// 注册MapPropertySource和SystemEnvironmentPropertySource
	// SystemEnvironmentPropertySource是MapPropertySource的子类~~~~
	@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()));
	}
}
StandardServletEnvironment

这是在web容器(servlet容器)时候的应用的标准环境。

public class StandardServletEnvironment extends StandardEnvironment implements ConfigurableWebEnvironment {
	public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";
	public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";
	public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties";

	// 放置三个web相关的配置源~  StubPropertySource是PropertySource的一个public静态内部类~~~
	@Override
	protected void customizePropertySources(MutablePropertySources propertySources) {
		propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
		propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
		
		// 可以通过spring.properties配置文件里面的spring.jndi.ignore=true关闭对jndi的暴露   默认是开启的
		if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
			propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
		}
		super.customizePropertySources(propertySources);
	}

	// 注册servletContextInitParams和servletConfigInitParams到属性配置源头里
	@Override
	public void initPropertySources(@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
		WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig);
	}
}

注意:这里addFirst和addLast等关系这顺序,进而都关乎着配置最终的生效的。因此下面对比一下web环境和非web环境下属性源们的配置,各位要有感官上的一个认识~~~

非web环境:

web环境:

可见web相关配置的属性源的优先级是高于system相关的。

需要注意的是:若使用@PropertySource导入自定义配置,它会位于最底端(优先级最低

另外附上SpringBoot的属性源们: 访问:http://localhost:8080/env得到如下



EnvironmentCapable、EnvironmentAware

实现了此接口的类都应该有一个Environment类型的环境,并且可以通过getEnvironment方法取得。 我们熟知的所有的Spring应用上下文都实现了这个接口,因为ApplictionContext就实现了这个接口,表示每个应用上下文都是有自己的运行时环境的

还有HttpServletBean、GenericFilterBean它们既实现了EnvironmentCapable也实现了EnvironmentAware用于获取到这个环境对象。

ClassPathScanningCandidateComponentProvider也实现了它如下代码:

public class ClassPathScanningCandidateComponentProvider implements EnvironmentCapable, ResourceLoaderAware {
	...
	@Override
	public final Environment getEnvironment() {
		if (this.environment == null) {
			this.environment = new StandardEnvironment();
		}
		return this.environment;
	}
	...
}

StringValueResolver

分析完了PropertyResolverEnvironment之后,来到我们今天的主菜StringValueResolver

先说说StringValueResolver本身,Spring对它的定义为:一个处理字符串的简单策略接口。

// @since 2.5  该接口非常简单,就是个函数式接口~
@FunctionalInterface
public interface StringValueResolver {
	@Nullable
	String resolveStringValue(String strVal);
}

唯一public实现类为:EmbeddedValueResolver

EmbeddedValueResolver

帮助ConfigurableBeanFactory处理placeholders占位符的。ConfigurableBeanFactory#resolveEmbeddedValue处理占位符真正干活的间接的就是它~~

// @since 4.3  这个类出现得还是蛮晚的   因为之前都是用内部类的方式实现的~~~~这个实现类是最为强大的  只是SpEL
public class EmbeddedValueResolver implements StringValueResolver {

	// BeanExpressionResolver之前有非常详细的讲解,简直不要太熟悉~  它支持的是SpEL  可以说非常的强大
	// 并且它有BeanExpressionContext就能拿到BeanFactory工厂,就能使用它的`resolveEmbeddedValue`来处理占位符~~~~
	// 双重功能都有了~~~拥有了和@Value一样的能力,非常强大~~~
	private final BeanExpressionContext exprContext;
	@Nullable
	private final BeanExpressionResolver exprResolver;

	public EmbeddedValueResolver(ConfigurableBeanFactory beanFactory) {
		this.exprContext = new BeanExpressionContext(beanFactory, null);
		this.exprResolver = beanFactory.getBeanExpressionResolver();
	}


	@Override
	@Nullable
	public String resolveStringValue(String strVal) {
		
		// 先使用Bean工厂处理占位符resolveEmbeddedValue
		String value = this.exprContext.getBeanFactory().resolveEmbeddedValue(strVal);
		// 再使用el表达式参与计算~~~~
		if (this.exprResolver != null && value != null) {
			Object evaluated = this.exprResolver.evaluate(value, this.exprContext);
			value = (evaluated != null ? evaluated.toString() : null);
		}
		return value;
	}

}

关于Bean工厂resolveEmbeddedValue的实现,我们这里也顺带看看:

public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
	...
	@Override
	@Nullable
	public String resolveEmbeddedValue(@Nullable String value) {
		if (value == null) {
			return null;
		}
		String result = value;

		// embeddedValueResolvers是个复数:因为我们可以自定义处理器添加到bean工厂来,增强它的能力
		for (StringValueResolver resolver : this.embeddedValueResolvers) {
			result = resolver.resolveStringValue(result);
			// 只要处理结果不为null,所以的处理器都会执行到~~~~
			if (result == null) {
				return null;
			}
		}
		return result;
	}
	...
}

而Bean工厂的处理器都怎么添加进去的呢????

public abstract class AbstractApplicationContext extends DefaultResourceLoader
		implements ConfigurableApplicationContext {
		
	protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
		...
		// 如果从来没有注册过,Spring容器默认会给注册一个这样的内部类
		// 可以看到,它最终还是委托给了Environment去干这件事~~~~~~
		// 显然它最终就是调用PropertyResolver#resolvePlaceholders
		if (!beanFactory.hasEmbeddedValueResolver()) {
			beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
		}
		...
	}	
			
}

由此可见,解析占位符最终都返璞归真,真正最终的处理类,处理方法方法是:AbstractPropertyResolver#resolvePlaceholders,这就是我们非常熟悉了,上面也有详细讲解,最终都是委托给了PropertyPlaceholderHelper去处理的~

由此可见,若我们通过实现感知接口EmbeddedValueResolverAware得到一个StringValueResolver来处理我们的占位符、SpEL计算。根本原因是:

class ApplicationContextAwareProcessor implements BeanPostProcessor {
	
	// 这里new的一个EmbeddedValueResolver,它持有对beanFactory的引用~~~
	// 所以调用者直接使用的是EmbeddedValueResolver:它支持解析占位符(依赖于Enviroment上面有说到)并且支持SpEL的解析  非常强的
	public ApplicationContextAwareProcessor(ConfigurableApplicationContext applicationContext) {
		this.applicationContext = applicationContext;
		this.embeddedValueResolver = new EmbeddedValueResolver(applicationContext.getBeanFactory());
	}
}

Spring对这个感知接口的命名也很实在,我们通过实现EmbeddedValueResolverAware这个接口得到的实际上是一个EmbeddedValueResolver,提供处理占位符和SpEL等高级功能。

另外StringValueResolver还有个实现类是PropertyPlaceholderConfigurer的private内部类实现,PlaceholderResolvingStringValueResolver逻辑也非常的简单,此处就不展开了。 关于PropertyPlaceholderConfigurer本身,我们在当初xml导入配置的时候经常看到如下配置:

<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
   <property name="location">
   		<!-- classpath:conf/jdbc.properties  -->
       <value>conf/jdbc.properties</value>
   </property>
    <property name="fileEncoding">
        <value>UTF-8</value>
    </property>
</bean>

而在注解时代,一般建议使用@PropertySource代替~

关于SpEL的讲解和BeanExpressionContext以及BeanExpressionResolver我之前文章有非常详细的讲解,强烈建议参考博文: 【小家Spring】Spring中@Value注解有多强大?从原理层面去剖析为何它有如此大的“能耐“


PropertyResolver的resolvePlaceholders()getProperty()的区别

这个区别其实很多人都并不能明白,举个例子:

public class Main {

    public static void main(String[] args) {
        StandardEnvironment environment = new StandardEnvironment();

        MutablePropertySources mutablePropertySources = environment.getPropertySources();
        MapPropertySource mapPropertySource = new MapPropertySource("diy", new HashMap<String, Object>() {{
            put("app.name", "fsx");
            put("app.key", "${user.home1}"); // 注意这里是user.home1 特意让系统属性里不存在的
            put("app.full", "${app.key} + ${app.name}");
        }});
        mutablePropertySources.addFirst(mapPropertySource);


        // 正常使用
        String s = environment.resolvePlaceholders("${app.full}");
        System.out.println(s);

        s = environment.getProperty("app.full");
        System.out.println(s);

    }

}

结果为:

${user.home1} + fsx
Exception in thread "main" java.lang.IllegalArgumentException: Could not resolve placeholder 'user.home1' in value "${user.home1}"
	at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:172)
	at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:160)

由此可以得出如下几点结论:

注意:他俩最终解析的都是${app.key} + ${app.name}这个字符串,只是两种方法get取值的方式不一样~

  1. properties里面可以书写通配符如${app.name},但需要注意: 1. properties里的内容都原封不动的被放进了PropertySource里(或者说是环境里),而是只有在需要用的时候才会解析它 2. 可以引用系统属性、环境变量等,设置引用被的配置文件里都是ok的(只要保证在同一Environment就成)
  2. resolvePlaceholders()它的入参是${}一起也包含进来的。它有如下特点: 1. 若${}里面的key不存在,就原样输出不报错。若存在就使用值替换 2. key必须用${}包着,否则原样输出~~ 3. 若是resolveRequiredPlaceholders()方法,那key不存在就会抛错~
  3. getProperty()指定的是key本身,并不需要包含${}, 1. 若key不存在返回null,但是若key的值里还有占位符,那就就继续解析。若出现占位符里的key不存在时,就抛错 2. getRequiredProperty()方法若key不存在就直接报错了~

注意:@Value注解我们一般这么使用@Value("${app.full}")来读取配置文件里的值,所以它即使出现了如上占位符不存在也原样输出不会报错(当然你的key必须存在啊),因为已经对@Value分析过多次:DefaultListableBeanFactory解析它的时候,最终会把表达式先交给StringValueResolver们去处理占位符,调用的就是resolver.resolveStringValue(result)方法。而最终执行它的见:

beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));

所以最终是委托给EnvironmentresolvePlaceholders()方法去处理的,所以由上结论可知,即是存在占位符的key不存在,原样输出即可嘛。

备注最终解析都是交给了PropertyPlaceholderHelper,它默认支持{}[]()等占位符。而我们最为常用的就是${},注意它的placeholderPrefix=${(而不是单单的{),后缀是}

占位符使用小技巧

例如一般我们的web程序的application.properties配置端口如下:

server.port=8080

而打包好后我们可以通过启动参数:--server.port=9090来改变此端口。但是若我们配置文件这么写:

server.port=${port:8080}

那我们启动参数可以变短了,这样写就成:--port=9090。相信了解了上面原理的小伙伴,理解这个小技巧是非常简单的事咯~~~

${port:8080}表示没有port这个key,就用8080。 有这个key就用对应的值~


总结

PropertyResolver作为一个处理底层属性源的接口,可能很少有人熟悉。但是Environment作为一个上层应用接口,代表着Spring应用运行环境属性信息,可以说还是非常的重要的。毕竟平时开发中,我们也不乏少用~

另外它和Spring的属性源:PropertySource也有非常大的关联,而属性源这个概念在任何一个框架设计中我认为都是很重要的考量,Spring、SpringBoot尤甚。因此了解了Env的理论能够奠定一个很好的框架设计基础~

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • [享学Eureka] 九、远程通信模块:使用TransportClientFactory构建底层请求客户端完成服务注册、服务下线

    代码下载地址:https://github.com/f641385712/netflix-learning

    YourBatman
  • 【小家java】java8新特性之---方法引用

    简单地说,就是一个Lambda表达式。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的...

    YourBatman
  • [享学Eureka] 三、Eureka配置之:EurekaInstanceConfig实例配置

    大家对Spring Cloud技术体系的使用应该有个感受:配置太多了,真的是多如牛毛啊。这是实话且是现状,因此坊间笑言:现在很多架构师为“配置工程师”或许更为恰...

    YourBatman
  • 泛型

    葆宁
  • springboot整合redis发送手机验证码注册登录

    短信验证码是通过发送验证码到手机的一种有效的验证码系统。主要用于验证用户手机的合法性及敏感操作的身份验证。常见的使用场景有:登录注册、信息修改、异常登录、找回密...

    崔笑颜
  • Android中的WebView之loadDataWithBaseURL()与loadData()Android中的WebView之loadDataWithBaseURL()与loadData()

    一个会写诗的程序员
  • java中复制对象通过反射或序列化

    在使用缓存读取数据后修改发现缓存被修改。于是找了下复制对象的方法。 关于对象克隆 ---- 按我的理解,对象是包含引用+数据。通常变量复制都是将引用传递过去。比...

    Ryan-Miao
  • 教你用构建者(生成器)模式优雅地创建对象

    很多博客文章上来就先抛出一个定义,我们不妨反过来问一句为什么要用构建者模式。 首先我们创建一个User类,然后采用有参构造器的方式创建对象。

    java技术爱好者
  • Android 中Volley二次封装并实现网络请求缓存

    Android目前很多同学使用Volley请求网络数据,但是Volley没有对请求过得数据进行缓存,因此需要我们自己手动缓存。 一下就是我的一种思路,仅供参考

    砸漏
  • 设计模式(四) | 简历复印与原型模型不得不说的一些事

    谭庆波

扫码关注云+社区

领取腾讯云代金券