写点什么

深入了解 Spring 之 Environment

用户头像
邱学喆
关注
发布于: 3 小时前
深入了解Spring之Environment

一. 概述

在进行业务代码开发时,不单单只包含业务代码,同时也会包含环境信息;业务代码会根据环境信息从而采取不同的业务处理;在 spring 中,有关环境信息的全部都由 Environment 接口的实现类保存。我们带着相关的疑问去解读;

  • Environment 接口的实现类的数据结构是怎么样的,其是如何运作的?

  • 环境信息是如何保存到 Environment 中的?

  • 业务代码是如何于 Environment 交互的?

二. 原理

1. Environment 类图

在 spring 中,Environment 的继承关系以及该实现类中所含有的关键属性的结构,类图如下:

类图中的方法,就不过多介绍了;接下来简单介绍一些关键的类。

  • AbstractEnvironment 是抽象类,里面包含关键的属性信息

  • activeProfiles 激活的 profile 标志列表,从 MutablePropertySources 中找到以"spring.profiles.active"对应的字符串,多个以","分隔开;

  • defaultProfiles 默认的 profile 标志列表,从 MutablePropertySources 中找到以"spring.profiles.default"对应的字符串,多个以","分隔开;

  • propertySources 环境信息都保存在这里

  • propertyResolver 环境信息解析,主要是类型转换;实现类为 PropertySourcesPropertyResolver

  • MutablePropertySources 保存所有环境信息的门面,其属性是 PropertySource 集合;意味着有很多不同来源的环境信息;

  • PropertySource 真正存储环境信息的地方,

  • name 标明来源方式的名称

  • source 环境信息的数据存储,之所以采用泛型,是因为其提供更加灵活处理,然而其限制了行为方式。

2. PropertySource

上文说到,其是真正存储环境信息的地方;我们查看其提供的方法,代码如下:

public abstract class PropertySource<T> {    public String getName() {    return this.name;  }  	public T getSource() {		return this.source;	}  	public boolean containsProperty(String name) {		return (getProperty(name) != null);	}  /**   * 有子类自由实现   */  public abstract Object getProperty(String name);  }
复制代码

子类会很多,我们挑几个进行简单的介绍:

  • MapPropertySource T 的类型为 Map 类型,所以其方法很简单,代码如下:

//MapPropertySourcepublic Object getProperty(String name) {   return this.source.get(name);}
复制代码
  • SimpleCommandLinePropertySource T 的类型为 CommandLineArgs,这个是 spring boot 框架启动时,传入的参数;其逻辑算相对于有点复杂,代码如下:

public final String getProperty(String name) {  if (this.nonOptionArgsPropertyName.equals(name)) {    Collection<String> nonOptionArguments = this.getNonOptionArgs();    if (nonOptionArguments.isEmpty()) {      return null;    }else {      return StringUtils.collectionToCommaDelimitedString(nonOptionArguments);    }  }  Collection<String> optionValues = this.getOptionValues(name);  if (optionValues == null) {    return null;  } else {    return StringUtils.collectionToCommaDelimitedString(optionValues);  }}
复制代码

想要看清楚上面的逻辑,就需要了解这个 CommandLineArgs 对象的数据结构,如下:

  • optionArgs 类型为 Map<String, List<String>>

  • nonOptionArgs 类型为 List<String>

其逻辑就不言而喻了,相对于简单;至于 CommandLineArgs 是怎么样创建的,以及属性的值是怎么设置上去的,稍后在讲解;

另外的一个重点问题:如果有多个 PropertySource 含有同样的 key 对应的值,具体是以那个 PropertySource 为准呢?说白了就是优先级问题。我们通过代码,可以快速的找到原因。PropertySourcesPropertyResolver 的 getProperty 方法,代码如下:

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) {        if (resolveNestedPlaceholders && value instanceof String) {          //解析value          value = resolveNestedPlaceholders((String) value);        }        //类型转换        return convertValueIfNecessary(value, targetValueType);      }    }  }    return null;}
复制代码

3. PropertySourcesPropertyResolver

该类是真正根据 key 从 MapPropertySource 找到环境信息对应的值接着进行解析,然后对其进行类型转换,类图如下:

这里稍微留意的是,PropertySourcesPropertyResolver 类中的 propertySources 与 AbstractEnvironment 中的 propertySources 都指向同一个内存地址,意味着我们通过 AbstractEnvironment 获取 propertySources,对其进行操作也会起到对 PropertySourcesPropertyResolver 中的 propertySources 同样的操作;之所以需要两个副本存在,主要是因为 AbstractEnvironment 是一个门面;

现在我们重点介绍 AbstractPropertyResolver 抽象类的关键属性:

  • conversionService 类型转换类

  • nonStrictHelper 宽松的属性解析 ,即 ignoreUnresolvableNestedPlaceholders 为 true;

  • strictHelper 严格的属性解析,即 ignoreUnresolvableNestedPlaceholders 为 false;

  • ignoreUnresolvableNestedPlaceholders 是否忽略未能解析值,意思是解析占位符拿到的值,从环境变量里面查找;

  • 值为 true 时,如果从 MutablePropertySources 对象找不到对应的值,则原封不动的返回出去

  • 值为 false 时,如果从 MutablePropertySources 对象找不到对应的值,则直接抛异常;

  • placeholderPrefix 占位符前缀,默认值"${"

  • placeholderSuffix 占位符后缀,默认值"}"

  • valueSeparator 值分隔符,默认值":"。主要用于当从环境信息中获去不到这个值,就使用这个分隔符后面的值

  • requiredProperties key 集合必须在环境信息中必须存在,如果不存在,则直接抛异常;

具体逻辑:解析占位符 ${variable:defaultValue},我们拿到 variable,接着从 MutablePropertySources 对象找到 variable 的值;如果找不到,则使用 defaultValue;

具体代码就不罗列了,感兴趣的,可以直接去看代码;

有关类型转换的,可以从看另外一篇《Spring 类型转换


三. 构建

我们通过原理的大体的介绍,对其认知有进一步的了解;那么 spring 是如何构建以及初始化 Environment 对象的;

1. 创建

在 AbstractApplicationContext 中的 refresh 方法会判断 Environment 是否为空。如果为空,直接通过空构造函数创建 StandardEnvironment。在空构造函数中,会优先初始化两个 PropertySource,代码如下:

protected void customizePropertySources(MutablePropertySources propertySources) {  propertySources.addLast(    new PropertiesPropertySource("systemProperties", getSystemProperties()));  propertySources.addLast(    new SystemEnvironmentPropertySource("systemEnvironment", getSystemEnvironment()));}
复制代码

2. 初始化 MutablePropertySources

如在 spring boot 框架中,其会解析传过来的参数进行解析从创建 SimpleCommandLinePropertySource 对象。具体如何解析交由 SimpleCommandLineArgsParser 对象来处理;

处理规则是:

  • --key=value 归属 optionArgs

  • key=value 归属 nonOptionArgs

并且插入 MutablePropertySources 对象集合中头部,即优先级最高;别名为:springApplicationCommandLineArgs


另外 spring boot 框架,提供了 EnvironmentPostProcessor 这个接口;spring boot 应用启动过程中,就可以对 Environment 对象进行操作;我们查看其接口的关键的实现类,其余类就不在展示:

  • RandomValuePropertySourceEnvironmentPostProcessor 创建 RandomValuePropertySource 对象,插入到 MutablePropertySources 尾部;其根据前缀"random."+type(类型)。类型主要有 int,long,uuid,byte 数组。具体可以查看该类

  • ConfigFileApplicationListener 有关解析 application.*配置文件;插入到 MutablePropertySources 尾部;优先级是 profile->default;

之所以 EnvironmentPostProcessor 会生效,是由于在 spring boot 框架中添加了事件机制;所以 EnvironmentPostProcessor 只在 spring boot 框架中使用,原生的 spring 没有该功能;


原生的提供了 @PropertySource、@PropertySources 注解,可以加载该注解指定的配置文件,解析该配置文件封装成 PropertySource 对象保存到 MutablePropertySources 尾部;该注解生效的前提是向 SpringContext 中添加 ConfigurationClassPostProcessor 处理器,同时在添加该注解的类添加 @Configuration 注解;

四. 运用

在 spring 启动过程已经对 Environment 进行初始化了,那么我们是如何使用该对象的;

1. EnvironmentAware

在类中通过实现该接口,spring 容器会自动帮我们注入 Environment 对象;这样子我们就可以拿到这个对象进行我们想要的操作;


2. StringValueResolver

该接口是对字符串进行解析,匿名内部类,代码如下:

//PropertySourcesPlaceholderConfigurerprotected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,			final ConfigurablePropertyResolver propertyResolver) throws BeansException {
propertyResolver.setPlaceholderPrefix("${"); propertyResolver.setPlaceholderSuffix("}"); propertyResolver.setValueSeparator(":"); //内部实现类 StringValueResolver valueResolver = strVal -> { String resolved = (ignoreUnresolvablePlaceholders ? propertyResolver.resolvePlaceholders(strVal) : propertyResolver.resolveRequiredPlaceholders(strVal)); if (trimValues) { resolved = resolved.trim(); } return (resolved.equals(nullValue) ? null : resolved); }; //向容器中保存该StringValueResolver对象进embeddedValueResolvers集合中; doProcessProperties(beanFactoryToProcess, valueResolver); }
复制代码

在 BeanFactory.doResolveDependency 方法中会调用 embeddedValueResolvers 进行字符串解析;

在我们常规开发中,对象中属性使用 @Value("${}"),就会触发上面的解析从 Environment 对象中查找对应的 value 进行替换;


同时 PropertySourcesPlaceholderConfigurer 类对 BeanDefinition 中标有 ${}的 factoryMethodName、beanClass 等等进行替换;具体还得看里面的代码,关键的代码 BeanDefinitionVisitor:

五. Profile

之所以单独拿来讲述,因为其会影响对象是否能保存到 spring 容器;

当一个类标有 @Profile 时,其会根据 Environment 变量中的 profile 来决定是否需要实例化该对象并且注入到容器中去;

具体是有 ProfileCondition 对象去判断,代码如下:

class ProfileCondition implements Condition {  @Override  public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {    MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());    if (attrs != null) {      for (Object value : attrs.get("value")) {        if (context.getEnvironment().acceptsProfiles((String[]) value)) {          return true;        }      }      return false;    }    return true;  }}
复制代码


发布于: 3 小时前阅读数: 3
用户头像

邱学喆

关注

计算机原理的深度解读,源码分析。 2018.08.26 加入

在IT领域keep Learning。要知其然,也要知其所以然。原理的爱好,源码的阅读。输出我对原理以及源码解读的理解。个人的仓库:https://gitee.com/Michael_Chan

评论

发布
暂无评论
深入了解Spring之Environment