手写Spring系列3-Environment与占位符的替换
1. 什么是Environment
在Spring
当中,Environment
对象是一个很重要的组件,从名字我们可以知道它是一个环境,实际上它维护的是Spring
当中的环境信息,主要包括如下这些内容:
- 1.当前操作系统中的环境变量(比如你配置的
JAVA_HOME
); - 2.JVM系统属性(比如
java.class.path
,可以通过-Dkey=value
的JVM参数去进行配置); - 3.本地
properties
(或者yaml
)配置文件中的配置属性等。
Spring
将每个属性的来源抽象成为一个PropertySource
,称之为属性源,将多个属性源抽象成为一个MutablePropertySources
,其实MutablePropertySources
维护的就是一个属性源的列表。
对于每个属性源都会有一个getProperty
方法,可以去指定一个特定的key
从属性源当中去获取对应的属性值。
在Spring
中的Environment
中默认集成的PropertySource
包括systemEnvironment
(系统的环境变量)和systemProperties
(系统的属性)这两个,在SpringBoot
中还会通过增加application.properties
中配置属性的一个PropertySource
。
对于每个属性源,可以通过一个PropertyResolver
,称为属性解析器,去对该属性源进行解析,比如Environment
本身就实现了PropertyResolver
,支持对属性进行解析。
2. PropertyResolver
和PropertySource
的设计
2.1 PropertySource
的设计
public abstract class PropertySource<T> { private String name; private T source; public PropertySource() { this(null, null); } public PropertySource(String name, T source) { this.name = name; this.source = source; } public String getName() { return name; } public void setName(String name) { this.name = name; } public T getSource() { return source; } public void setSource(T source) { this.source = source; } /** * 判断属性源当中是否包含了该属性 */ public boolean containsProperty(String name) { return (getProperty(name) != null); } /** * 根据name获取到对应的属性值 */ public abstract Object getProperty(String name); }
其实PropertySource
(属性源)中属性的来源并不重要,只要你给我实现getProperty
方法就行,不管你是来自哪,比如你来自一个Map
,来自一个配置文件?
2.2 PropertyResolver
的设计
public interface PropertyResolver { /** * 如果这个key在环境当中,那么return true,不然return false * * @param key 属性key * @return key存在return true,不然return false */ boolean containsProperty(String key); /** * 给定一个key,查找和它关联的属性,如果key不能被解析,那么return null * * @param key 属性key * @return key存在则return查找到的属性,key不存在则return null */ String getProperty(String key); /** * 给定一个key,查找和它关联的属性,如果key不能被解析,那么return defaultValue * * @param key 想要去解析的key * @param defaultValue 解析失败的默认值 * @return key存在则return解析出来的属性,key不存在则return 默认值 */ String getProperty(String key, String defaultValue); /** * 给定一个key,给行想要得到的Class,将key解析成为一个对象,如果解析失败return null * * @param key 想要去解析的key * @param targetType 想要解析成为的目标对象类型 * @param <T> 需要解析成为的目标对象类型 * @return 目标类型的对象 */ <T> T getProperty(String key, Class<T> targetType); /** * 给定目标的属性key,解析成为目标对象,如果解析失败,那么return defaultValue * * @param key 期望进行解析的属性值 * @param targetType 目标类型 * @param <T> 目标类型 * @param defaultValue 如果没有返回的默认值 */ <T> T getProperty(String key, Class<T> targetType, T defaultValue); /** * 将目标key解析成为一个字符串,如果解析一个key失败 * 不会return null,而是抛出一个不合法的状态异常 */ String getRequiredProperty(String key) throws IllegalStateException; /** * 将目标key解析成为一个目标对象,如果解析一个key失败 * 不会return null,而是抛出一个不合法的状态异常 * * @throws IllegalStateException 如果给定的key不能被解析到 */ <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException; /** * 解析${...}这个占位符,使用{@link #getProperty} 相关联的属性值去代替它们 * 如果没有解析到,不会有默认值,而是去忽略它 * * @param text 给定等待解析的占位符文本 * @return 返回解析到的字符串,不会为空,如果为空直接抛出来异常 * @throws IllegalArgumentException 如果给定的text为空,那么抛出异常 */ String resolvePlaceholders(String text); /** * 解析${...}这个占位符,使用{@link #getProperty} 相关联的属性值去代替它们 * 如果没有解析到,不会有默认值,而是去忽略它 * * @param text 给定等待解析的占位符文本,如果为空直接抛出异常 * @return 返回解析到的字符串,不会为空,如果为空直接抛出来异常 * @throws IllegalArgumentException 如果给定的text为空/占位符是不能被解析的,那么抛出异常 */ String resolveRequiredPlaceholders(String text) throws IllegalArgumentException; }
主要提供的方法如下:
getProperty
的方法,用来指定key
去获取到对应的属性值。resolvePlaceholders
方法其实就是实现占位符的替换(placeholder
翻译过来就是占位符),比如你提供给一个${user.name}
这样的占位符,需要从各个PropertySource
中找到合适的要进行替换的属性。
它还有一个子接口ConfigurablePropertyResolver
,因为最大的接口只是提供一些get
的操作,它的子接口ConfigurableXXX
主要就是提供一些写操作。(在Spring
当中都是这样子设计的,凡是带有ConfigurableXXX
的,都是提供一些set
方法的)
public interface ConfigurablePropertyResolver extends PropertyResolver { /** * 设置占位符的前缀,例如${ */ void setPlaceholderPrefix(String placeholderPrefix); /** * 设置占位符的后缀,比如} */ void setPlaceholderSuffix(String placeholderSuffix); /** * 设置占位符的值的分隔符比如${user.name:wanna} * 代表如果有user.name属性,那么使用user.name属性,如果没有,那么使用默认值wanna * 中间的:就是要使用的占位符 */ void setValueSeparator(String valueSeparator); }
3. 如何实现Environment
?
Environment
(环境)对象当然是需要去支持属性值的解析,那么就可以实现PropertyResolver
接口,根据该接口提供相应的方法的实现即可。
关键是那些PropertySource
应该怎么去进行实现呢?
private final MutablePropertySources propertySources = new MutablePropertySources();
提供这样一个MutablePropertySources
,去实现对多个PropertySource
的集成和整合。
// 添加系统的属性信息,里面是维护了一些系统的属性(JVM相关的属性) propertySources.addLast( new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties())); // 添加系统的环境属性(系统变量信息,比如配置的JAVA_HOME等环境变量) propertySources.addLast( new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
关键是getSystemProperties
和getSystemEnvironment
这两个方法究竟做了什么?
/** * 获取系统的属性(JVM属性) */ @SuppressWarnings({"rawtypes", "unchecked"}) public Map<String, Object> getSystemProperties() { return (Map) System.getProperties(); } /** * 获取系统的环境变量信息,比如JAVA_HOME */ @SuppressWarnings({"rawtypes", "unchecked"}) public Map<String, Object> getSystemEnvironment() { return (Map) System.getenv(); }
其实很简单嘛,可以通过System.getenv
获取环境变量的内容,可以通过System.getProperties
获取系统当中的属性值。
4. 占位符的解析
在Spring
的XML
版本的IOC
容器当中,只要你配置了这样的一个标签,那么它就会往容器中导入一个组件,叫做PropertySourcesPlaceholderConfigurer
,这个类其实主要的作用,就是对占位符进行解析,而配置的locations
信息则会被配置到这个组件当中去。
<context:property-placeholder locations="classpath:application.properties"/>
PropertySourcesPlaceholderConfigurer
为什么能对占位符进行解析,其实很简单,它实现BeanFactoryPostProcessor
,在容器启动时,会回调它的postProcessBeanFactory
方法。
这个方***将BeanFactory
传给我们,既然拿到了BeanFactory
,我们完全可以遍历容器中所有的BeanDefinition
,对其中配置的属性值是占位符(${...}
)的情况去进行解析。
4.1 对各个属性源的整合
@Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { if (propertySources == null) { propertySources = new MutablePropertySources(); // 添加一个环境的属性源(环境当中包括了JVM系统的属性和系统的环境变量信息),可以支持从环境当中获取属性值 if (this.environment != null) { propertySources.addLast(new PropertySource<Environment>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) { @Override public Object getProperty(String name) { return getSource().getProperty(name); } }); } // 往尾部去添加一个本地属性的属性源,用来解析本地的属性源...主要就是配置的本地的properties属性... propertySources.addLast(new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, mergedProperties())); } // 对属性值去进行处理,包括属性/构造器参数中的... processProperties(beanFactory, new ProperSourcesPropertyResolver(this.propertySources)); }
在这个类当中,不仅不仅将Environment
作为PropertySource
了,还导入了一个新的一个本地配置文件的PropertySource
(其实就是根据配置的locations
属性去进行导入的),看看它的mergedProperties
方法的实现。
/** * 加载配置的本地properties配置文件信息,如果是以classpath:开头的,那么把classpath:去掉 * 直接使用jdk提供的Properties类,就可以自动解析properties配置文件 */ protected Map<String, Object> mergedProperties() { return PropertiesUtils.loadPropertiesFromClassPath(locations); }
用到的工具类如下
public class PropertiesUtils { @SuppressWarnings({"unchecked", "rawtypes"}) public static Map<String, Object> loadPropertiesFromClassPath(String... paths) { Properties properties = new Properties(); try { for (String path : paths) { path = StringUtils.replaceAllBlack(path); // 去掉多余的空白符 if (path.startsWith(SystemUtils.CLASSPATH_PREFIX)) { // 把`classpath:`切掉 path = path.substring(SystemUtils.CLASSPATH_PREFIX.length()); } URL resource = ClassLoader.getSystemClassLoader().getResource(path); AssertUtils.notNull(resource, path + "资源无法加载到"); properties.load(new FileInputStream(resource.getPath())); } } catch (Exception ex) { ex.printStackTrace(); } return (Map) properties; } }
使用JDK中自带的Properties
这个类,使用它的load
方法,它就可以直接将properties
配置文件中的配置的相关信息,直接导入到Properties
对象当中。
上面完成了需要的属性源的集成和整合,那么下一步,就需要对占位符去进行处理了。
4.2 占位符的真正处理
protected void processProperties(ConfigurableListableBeanFactory beanFactory, ConfigurablePropertyResolver resolver) { // 配置属性值解析器的前缀/后缀/值的分隔符 resolver.setPlaceholderPrefix(placeholderPrefix); resolver.setPlaceholderSuffix(placeholderSuffix); resolver.setValueSeparator(valueSeparator); // 使用lambda表达式去创建一个嵌入式的值解析器,判断是否忽略掉不能解析的值... // 如果可以忽略,那么解析到没有的属性直接return null,如果不能忽略,那么直接抛出异常 StringValueResolver valueResolver = str -> ignoreUnresolvablePlaceholders ? resolver.resolvePlaceholders(str) : resolver.resolveRequiredPlaceholders(str); doProcessProperties(beanFactory, valueResolver); } protected void doProcessProperties(ConfigurableListableBeanFactory beanFactory, StringValueResolver valueResolver) { List<String> beanDefinitionNames = beanFactory.getBeanDefinitionNames(); for (String beanDefinitionName : beanDefinitionNames) { BeanDefinition<?> beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName); // 处理BeanDefinition中的属性参数/构造器参数等位置的占位符... handleBeanDefinitionProperties(beanDefinition, valueResolver); } // 往BeanFactory中添加一个嵌入式的值解析器,后续在BeanFactory中就可以去进行使用了... beanFactory.addEmbeddedValueResolver(valueResolver); }
这里涉及到一个StringValueResolver
,它的定义如下:
/** * 提供StringValue的解析,主要用在占位符的处理 * 比如在配置文件中有这样一个配置user.name=wanna * 你想要通过${user.name}获取到该对象,你就可以使用到StringValueResolver * 去进行实现 * * @author wanna * @version v1.0 */ @FunctionalInterface public interface StringValueResolver { public String resolveStringValue(String str); }
它主要是提供一个resolveStringValue
方法去对占位符的处理,上面我们使用的是lambda
表达式的方式去定义了一个StringValueResolver
去实现它的resolveStringValue
方法,而真正的对占位符的处理是用到的ProperSourcesPropertyResolver
这个组件去进行解析的。
关于嵌入式值解析器的说明:
- 1.有一个很关键的点,它将配置的这个
StringValueResolver
都配置到BeanFactory
当中去了,为什么要添加呢?因为@Value
注解也需要用到这个StringValueResolver
,放到容器中,后面就能去进行使用了。 - 2.在
Spring
在实例化所有的单实例Bean
时,会有如下的代码,也就是说,如果你不添加自己的嵌入式值解析器,那么Spring
将会给你创建一个基于环境对象的嵌入式值解析器。(而占位符处理器这里添加了,那么Spring
就不给你添加默认的了)
if (!beanFactory.hasEmbeddedValueResolver()) { beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal)); }
真正逻辑占位符的逻辑在下面,就是用到了刚刚配置好的StringValueResolver
去进行实现的。
/** * 处理BeanDefinition中的属性参数/构造器参数等位置的占位符... */ protected void handleBeanDefinitionProperties(BeanDefinition<?> beanDefinition, StringValueResolver valueResolver) { handleProperty(beanDefinition, valueResolver); // 处理<property>标签中的占位符 handleConstructor(beanDefinition, valueResolver); // 处理<constructor>中的占位符 } /** * 处理<constructor>中的占位符 */ protected void handleConstructor(BeanDefinition<?> beanDefinition, StringValueResolver valueResolver) { ConstructorArgumentValues cav = beanDefinition.getConstructorArgumentValues(); // 如果配置了构造器参数,才需要去进行处理通用的构造器参数... if (cav != null && !cav.getGenericArgumentValues().isEmpty()) { for (ConstructorArgumentValues.ValueHolder genericArgumentValue : cav.getGenericArgumentValues()) { Object value = genericArgumentValue.getValue(); if (value instanceof TypeStringValue) { TypeStringValue targetVal = (TypeStringValue) value; String property = ((TypeStringValue) value).getValue(); String resolveStringValue = valueResolver.resolveStringValue(property); targetVal.setValue(resolveStringValue); genericArgumentValue.setValue(targetVal); // 把占位符的值替换掉... } } } } /** * 处理在<property>标签的value中配置的占位符 */ protected void handleProperty(BeanDefinition<?> beanDefinition, StringValueResolver valueResolver) { PropertyValues pvs; // 如果有要进行处理的属性值,那么 if ((pvs = beanDefinition.getPropertyValues()) != null) { for (PropertyValue pv : pvs) { Object value = pv.getValue(); if (value instanceof TypeStringValue) { TypeStringValue targetVal = (TypeStringValue) value; String property = targetVal.getValue(); String resolveStringValue = valueResolver.resolveStringValue(property); targetVal.setValue(resolveStringValue); pv.setValue(targetVal); // 替换值... } } } }
5. 环境当中的PropertySource
的自定义
在上面了解了Environment
和PropertySource
之后,我们完全可以对环境当中内容去进行自定义,比如我们可以实现BeanFactoryPostProcessor
,然后通过EnvironmentAware
注入Environment
对象,然后在BeanFactoryPostProcessor
中去添加一个属于自己自定义的一个PropertySource
。
@Component public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor, EnvironmentAware { Environment environment; @Override public void setEnvironment(Environment environment) { this.environment = environment; } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { StandardEnvironment standardEnvironment = (StandardEnvironment) this.environment; Properties properties = new Properties(); properties.put("name", "wanna66666"); standardEnvironment.getPropertySources().addLast(new PropertySource<Properties>() { @Override public Object getProperty(String name) { return properties.getProperty(name); } }); } }
通过这样,我们就已经实现了往Environment
当中去放置一个自定义的PropertySource
,就可以通过XML
或者@Value
的方式,使用占位符${name}
,去获取到我们设置进去的wanna66666
!
在了解了以上的内容之后,相信你已经对SpringBoot
中properties
配置文件的解析已经有了一些了解,SpringBoot
是在容器启动前就准备好环境,而我们是在BeanFactoryPostProcessor
中去准备好的环境,其实本质上差别不大,区别只是在于加载配置文件的时机不同。
6.@PropertySource
注解的作用与原理
在Spring
当中提供了@PropertySource
/@PropertySources
注解,为了方便我们在注解版的IOC
容器中能更加方便的导入properties
配置文件。
@Configuration @PropertySources({ @PropertySource({"classpath:test1.properties","classpath:test2.properties"}) }) public class Config { }
使用如上的代码,就可以将test1.properties
和test2.properties
这两个配置文件当中的属性全部加入到Environment
当中去。
对@PropertySource
注解的简单处理和实现如下(所在类为ConfigurationClassParser
)。
/** * 处理@PropertySource往环境中注册的属性信息 * * @param configurationClass 目标配置类 */ protected void processPropertySources(ConfigurationClass configurationClass) { // 如果有@PropertySource/@PropertySources注解,那么需要进行处理 AnnotationMetadata metadata = configurationClass.getMetadata(); Set<PropertySource> set = AnnotatedElementUtils.getMergedRepeatableAnnotations(metadata.getIntrospectedClass(), PropertySource.class); Set<AnnotationAttributes> attributesSet = AnnotationAttributesUtils.asAnnotationAttributesSet(set); // 遍历所有的PropertySource的注解属性,去进行处理... for (AnnotationAttributes attributes : attributesSet) { String[] sources = attributes.getStringArray("value"); // 获取配置的位置列表 // 获取要创建的Factory,默认值为PropertySourceFactory.class,可以去进行自定义 Class<?> factoryClass = attributes.getForType("factory", Class.class); PropertySourceFactory factory = factoryClass == DEFAULT_PROPERTY_SOURCE_FACTORY_CLASS ? DEFAULT_PROPERTY_SOURCE_FACTORY : (PropertySourceFactory) ClassUtils.newInstance(factoryClass); if (this.environment instanceof ConfigurableEnvironment && factory != null) { ConfigurableEnvironment environment = (ConfigurableEnvironment) this.environment; // 使用导入属性的Factory去导入相关的属性...(如果指定了自定义的Factory那么就使用指定的,不然就使用默认的) environment.getPropertySources() .addLast(factory.createPropertySource(RESOURCES_PROPERTY_SOURCE_NAME, sources)); } } }
而默认的工厂类为
public class DefaultPropertySourceFactory implements PropertySourceFactory { @Override public PropertySource<?> createPropertySource(String name, String... resources) { Map<String, Object> properties = PropertiesUtils.loadPropertiesFromClassPath(resources); return new PropertiesPropertySource(name, properties); } }
7. 多层占位符的递归解析
需要支持使用如下这种方式去进行占位符解析:`{user.name}}`,比如配置了如下的属性值
user.name=name name=wanna
我们就可以通过`{user.name}}
获取到
wanna。我们如何去进行解析呢?我的想法是。首先,解析出来占位符的层数,就统计之前有多少层的
${,之后有多少层的
}就可以进行统计出来,我们将最内层的值称为
inner`。
- 1.在最开始的情况下,
inner=user.name
,这个时候我们使用${user.name}
(拼接占位符的前缀和后缀)就可以获取到name
,并赋值给inner
。 - 2.接着
inner=name
,我们继续拼接占位符使用${name}
就可以去进行解析到wanna
的值。 - 3.我们可以发现,占位符的处理次数,其实就和占位符的层数一样,我们直接两个for循环给它解决。
public String doResolvePlaceHolders(String text) { int placeholderCount = 0; String inner = text; // 解析出来的占位符嵌套了层数?比如${${user.name}}就嵌套了两层,嵌套了多少层就代表了执行多少次解析 for (; hasPlaceHolder(inner, placeholderPrefix, placeholderSuffix); placeholderCount++) { inner = inner.substring(placeholderPrefix.length(), inner.length() - placeholderSuffix.length()); } // 执行n次解析,比如user.name=wanna,wanna=123,通过${${user.name}} // 第一次要解析的是${user.name},得到wanna,第二次需要使用${wanna}去进行解析,得到123 // 每进行一次解析,就将解析到的结果加上前缀${和后缀},去执行解析占位符... for (int i = 0; i < placeholderCount; i++) { inner = doResolvePlaceHolder(placeholderPrefix + inner + placeholderSuffix); if (inner == null) { return null; } } // 如果最终解析出来的表达式当中还有占位符,那么还得递归解析一遍... if (hasPlaceHolder(inner, placeholderPrefix, placeholderSuffix)) { inner = doResolvePlaceHolders(inner); } return inner; } /** * 负责处理单层的占位符,比如${user.name},多层的处理暂时不支持 */ private String doResolvePlaceHolder(String text) { // 如果是以'${'作为前缀,以`}`作为后缀,那么才需要进行占位符的处理... if (!StringUtils.isNullOrEmpty(text) && text.startsWith(placeholderPrefix) && text.endsWith(placeholderSuffix)) { // 把前缀和后缀切掉 text = text.substring(placeholderPrefix.length(), text.length() - placeholderSuffix.length()); String[] sep = text.split(valueSeparator); // 使用`:`去分开key和默认值 String key = sep[0]; // 第一个参数是要替换的key String defaultValue = sep.length == 2 ? sep[1] : ""; // 第二个参数是默认值 String value = getProperty(key); // 根据属性key,去获取从属性源当中去获取value return value == null ? defaultValue : value; // 如果没有候选的,默认值也为空,那么啥也不做,保持不变... } return text; } private boolean hasPlaceHolder(String text, String placeholderPrefix, String placeholderSuffix) { return !StringUtils.isNullOrEmpty(text) && text.startsWith(placeholderPrefix) && text.endsWith(placeholderSuffix); } private String getProperty(String key) { for (PropertySource<?> propertySource : propertySources) { Object property = propertySource.getProperty(key); if (property != null) { return (String) property; } } return null; }#Java学习##Java##学习路径#