手写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##学习路径#