手写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. PropertyResolverPropertySource的设计

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;
}

主要提供的方法如下:

  1. getProperty的方法,用来指定key去获取到对应的属性值。
  2. 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()));

关键是getSystemPropertiesgetSystemEnvironment这两个方法究竟做了什么?

    /**
     * 获取系统的属性(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. 占位符的解析

SpringXML版本的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的自定义

在上面了解了EnvironmentPropertySource之后,我们完全可以对环境当中内容去进行自定义,比如我们可以实现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

在了解了以上的内容之后,相信你已经对SpringBootproperties配置文件的解析已经有了一些了解,SpringBoot是在容器启动前就准备好环境,而我们是在BeanFactoryPostProcessor中去准备好的环境,其实本质上差别不大,区别只是在于加载配置文件的时机不同。

6.@PropertySource注解的作用与原理

Spring当中提供了@PropertySource/@PropertySources注解,为了方便我们在注解版的IOC容器中能更加方便的导入properties配置文件。

@Configuration
@PropertySources({
        @PropertySource({"classpath:test1.properties","classpath:test2.properties"})
})
public class Config {

}

使用如上的代码,就可以将test1.propertiestest2.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##学习路径#
全部评论
感谢楼主分享!!!
点赞 回复
分享
发布于 2022-01-12 16:19

相关推荐

1 1 评论
分享
牛客网
牛客企业服务