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
分析完了PropertyResolver和Environment之后,来到我们今天的主菜: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取值的方式不一样~
- properties里面可以书写通配符如${app.name},但需要注意:1. properties里的内容都原封不动的被放进了PropertySource里(或者说是环境里),而是只有在需要用的时候才会解析它2. 可以引用系统属性、环境变量等,设置引用被的配置文件里都是ok的(只要保证在同一Environment就成)
- resolvePlaceholders()它的入参是${}一起也包含进来的。它有如下特点:1. 若${}里面的key不存在,就原样输出,不报错。若存在就使用值替换2. key必须用${}包着,否则原样输出~~3. 若是resolveRequiredPlaceholders()方法,那key不存在就会抛错~
- 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));
所以最终是委托给Environment的resolvePlaceholders()方法去处理的,所以由上结论可知,即是存在占位符的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的理论能够奠定一个很好的框架设计基础~