Spring Boot环境属性占位符解析及类型转换详解

前提

前面写过一篇关于Environment属性加载的源码分析和扩展,里面提到属性的占位符解析和类型转换是相对复杂的,这篇文章就是要分析和解读这两个复杂的问题。关于这两个问题,选用一个比较复杂的参数处理方法PropertySourcesPropertyResolver#getProperty,解析占位符的时候依赖到

PropertySourcesPropertyResolver#getPropertyAsRawString:

protected String getPropertyAsRawString(String key) {
 return getProperty(key, String.class, false);
}

protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
 if (this.propertySources != null) {
  for (PropertySource<?> propertySource : this.propertySources) {
   if (logger.isTraceEnabled()) {
    logger.trace("Searching for key '" + key + "' in PropertySource '" +
       propertySource.getName() + "'");
   }
   Object value = propertySource.getProperty(key);
   if (value != null) {
    if (resolveNestedPlaceholders && value instanceof String) {
     //解析带有占位符的属性
     value = resolveNestedPlaceholders((String) value);
    }
    logKeyFound(key, propertySource, value);
    //需要时转换属性的类型
    return convertValueIfNecessary(value, targetValueType);
   }
  }
 }
 if (logger.isDebugEnabled()) {
  logger.debug("Could not find key '" + key + "' in any property source");
 }
 return null;
}

属性占位符解析

属性占位符的解析方法是PropertySourcesPropertyResolver的父类AbstractPropertyResolver#resolveNestedPlaceholders:

protected String resolveNestedPlaceholders(String value) {
 return (this.ignoreUnresolvableNestedPlaceholders ?
  resolvePlaceholders(value) : resolveRequiredPlaceholders(value));
}

ignoreUnresolvableNestedPlaceholders属性默认为false,可以通过AbstractEnvironment#setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders)设置,当此属性被设置为true,解析属性占位符失败的时候(并且没有为占位符配置默认值)不会抛出异常,返回属性原样字符串,否则会抛出IllegalArgumentException。我们这里只需要分析AbstractPropertyResolver#resolveRequiredPlaceholders:

//AbstractPropertyResolver中的属性:
//ignoreUnresolvableNestedPlaceholders=true情况下创建的PropertyPlaceholderHelper实例
@Nullable
private PropertyPlaceholderHelper nonStrictHelper;

//ignoreUnresolvableNestedPlaceholders=false情况下创建的PropertyPlaceholderHelper实例
@Nullable
private PropertyPlaceholderHelper strictHelper;

//是否忽略无法处理的属性占位符,这里是false,也就是遇到无法处理的属性占位符且没有默认值则抛出异常
private boolean ignoreUnresolvableNestedPlaceholders = false;

//属性占位符前缀,这里是"${"
private String placeholderPrefix = SystemPropertyUtils.PLACEHOLDER_PREFIX;

//属性占位符后缀,这里是"}"
private String placeholderSuffix = SystemPropertyUtils.PLACEHOLDER_SUFFIX;

//属性占位符解析失败的时候配置默认值的分隔符,这里是":"
@Nullable
private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR;

public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
 if (this.strictHelper == null) {
  this.strictHelper = createPlaceholderHelper(false);
 }
 return doResolvePlaceholders(text, this.strictHelper);
}

//创建一个新的PropertyPlaceholderHelper实例,这里ignoreUnresolvablePlaceholders为false
private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) {
 return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix, this.valueSeparator, ignoreUnresolvablePlaceholders);
}

//这里最终的解析工作委托到PropertyPlaceholderHelper#replacePlaceholders完成
private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
 return helper.replacePlaceholders(text, this::getPropertyAsRawString);
}

最终只需要分析PropertyPlaceholderHelper#replacePlaceholders,这里需要重点注意:

注意到这里的第一个参数text就是属性值的源字符串,例如我们需要处理的属性为myProperties: ${server.port}-${spring.application.name},这里的text就是${server.port}-${spring.application.name}。

replacePlaceholders方法的第二个参数placeholderResolver,这里比较巧妙,这里的方法引用this::getPropertyAsRawString相当于下面的代码:

//PlaceholderResolver是一个函数式接口
@FunctionalInterface
public interface PlaceholderResolver {
 @Nullable
 String resolvePlaceholder(String placeholderName);
}
//this::getPropertyAsRawString相当于下面的代码
return new PlaceholderResolver(){

 @Override
 String resolvePlaceholder(String placeholderName){
  //这里调用到的是PropertySourcesPropertyResolver#getPropertyAsRawString,有点绕
  return getPropertyAsRawString(placeholderName);
 }
}  

接着看PropertyPlaceholderHelper#replacePlaceholders的源码:

//基础属性
//占位符前缀,默认是"${"
private final String placeholderPrefix;
//占位符后缀,默认是"}"
private final String placeholderSuffix;
//简单的占位符前缀,默认是"{",主要用于处理嵌套的占位符如${xxxxx.{yyyyy}}
private final String simplePrefix;

//默认值分隔符号,默认是":"
@Nullable
private final String valueSeparator;
//替换属性占位符
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
 Assert.notNull(value, "'value' must not be null");
 return parseStringValue(value, placeholderResolver, new HashSet<>());
}

//递归解析带占位符的属性为字符串
protected String parseStringValue(
  String value, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
 StringBuilder result = new StringBuilder(value);
 int startIndex = value.indexOf(this.placeholderPrefix);
 while (startIndex != -1) {
  //搜索第一个占位符后缀的索引
  int endIndex = findPlaceholderEndIndex(result, startIndex);
  if (endIndex != -1) {
   //提取第一个占位符中的原始字符串,如${server.port}->server.port
   String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
   String originalPlaceholder = placeholder;
   //判重
   if (!visitedPlaceholders.add(originalPlaceholder)) {
    throw new IllegalArgumentException(
      "Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
   }
   // Recursive invocation, parsing placeholders contained in the placeholder key.
   // 递归调用,实际上就是解析嵌套的占位符,因为提取的原始字符串有可能还有一层或者多层占位符
   placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
   // Now obtain the value for the fully resolved key...
   // 递归调用完毕后,可以确定得到的字符串一定是不带占位符,这个时候调用getPropertyAsRawString获取key对应的字符串值
   String propVal = placeholderResolver.resolvePlaceholder(placeholder);
   // 如果字符串值为null,则进行默认值的解析,因为默认值有可能也使用了占位符,如${server.port:${server.port-2:8080}}
   if (propVal == null && this.valueSeparator != null) {
    int separatorIndex = placeholder.indexOf(this.valueSeparator);
    if (separatorIndex != -1) {
     String actualPlaceholder = placeholder.substring(0, separatorIndex);
     // 提取默认值的字符串
     String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
     // 这里是把默认值的表达式做一次解析,解析到null,则直接赋值为defaultValue
     propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
     if (propVal == null) {
      propVal = defaultValue;
     }
    }
   }
   // 上一步解析出来的值不为null,但是它有可能是一个带占位符的值,所以后面对值进行递归解析
   if (propVal != null) {
    // Recursive invocation, parsing placeholders contained in the
    // previously resolved placeholder value.
    propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
    // 这一步很重要,替换掉第一个被解析完毕的占位符属性,例如${server.port}-${spring.application.name} -> 9090--${spring.application.name}
    result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
    if (logger.isTraceEnabled()) {
     logger.trace("Resolved placeholder '" + placeholder + "'");
    }
    // 重置startIndex为下一个需要解析的占位符前缀的索引,可能为-1,说明解析结束
    startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
   }
   else if (this.ignoreUnresolvablePlaceholders) {
    // 如果propVal为null并且ignoreUnresolvablePlaceholders设置为true,直接返回当前的占位符之间的原始字符串尾的索引,也就是跳过解析
    // Proceed with unprocessed value.
    startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
   }
   else {
    // 如果propVal为null并且ignoreUnresolvablePlaceholders设置为false,抛出异常
    throw new IllegalArgumentException("Could not resolve placeholder '" +
       placeholder + "'" + " in value \"" + value + "\"");
   }
   // 递归结束移除判重集合中的元素
   visitedPlaceholders.remove(originalPlaceholder);
  }
  else {
   // endIndex = -1说明解析结束
   startIndex = -1;
  }
 }
 return result.toString();
}

//基于传入的起始索引,搜索第一个占位符后缀的索引,兼容嵌套的占位符
private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
 //这里index实际上就是实际需要解析的属性的第一个字符,如${server.port},这里index指向s
 int index = startIndex + this.placeholderPrefix.length();
 int withinNestedPlaceholder = 0;
 while (index < buf.length()) {
  //index指向"}",说明有可能到达占位符尾部或者嵌套占位符尾部
  if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) {
   //存在嵌套占位符,则返回字符串中占位符后缀的索引值
   if (withinNestedPlaceholder > 0) {
    withinNestedPlaceholder--;
    index = index + this.placeholderSuffix.length();
   }
   else {
    //不存在嵌套占位符,直接返回占位符尾部索引
    return index;
   }
  }
  //index指向"{",记录嵌套占位符个数withinNestedPlaceholder加1,index更新为嵌套属性的第一个字符的索引
  else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) {
   withinNestedPlaceholder++;
   index = index + this.simplePrefix.length();
  }
  else {
   //index不是"{"或者"}",则进行自增
   index++;
  }
 }
 //这里说明解析索引已经超出了原字符串
 return -1;
}

//StringUtils#substringMatch,此方法会检查原始字符串str的index位置开始是否和子字符串substring完全匹配
public static boolean substringMatch(CharSequence str, int index, CharSequence substring) {
 if (index + substring.length() > str.length()) {
  return false;
 }
 for (int i = 0; i < substring.length(); i++) {
  if (str.charAt(index + i) != substring.charAt(i)) {
   return false;
  }
 }
 return true;
}

上面的过程相对比较复杂,因为用到了递归,我们举个实际的例子说明一下整个解析过程,例如我们使用了四个属性项,我们的目标是获取server.desc的值:

application.name=spring
server.port=9090
spring.application.name=${application.name}
server.desc=${server.port-${spring.application.name}}:${description:"hello"}

属性类型转换

在上一步解析属性占位符完毕之后,得到的是属性字符串值,可以把字符串转换为指定的类型,此功能由AbstractPropertyResolver#convertValueIfNecessary完成:

protected <T> T convertValueIfNecessary(Object value, @Nullable Class<T> targetType) {
 if (targetType == null) {
  return (T) value;
 }
 ConversionService conversionServiceToUse = this.conversionService;
 if (conversionServiceToUse == null) {
  // Avoid initialization of shared DefaultConversionService if
  // no standard type conversion is needed in the first place...
  // 这里一般只有字符串类型才会命中
  if (ClassUtils.isAssignableValue(targetType, value)) {
   return (T) value;
  }
  conversionServiceToUse = DefaultConversionService.getSharedInstance();
 }
 return conversionServiceToUse.convert(value, targetType);
}

实际上转换的逻辑是委托到DefaultConversionService的父类方法GenericConversionService#convert:

public <T> T convert(@Nullable Object source, Class<T> targetType) {
 Assert.notNull(targetType, "Target type to convert to cannot be null");
 return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType));
}

public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
 Assert.notNull(targetType, "Target type to convert to cannot be null");
 if (sourceType == null) {
  Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
  return handleResult(null, targetType, convertNullSource(null, targetType));
 }
 if (source != null && !sourceType.getObjectType().isInstance(source)) {
  throw new IllegalArgumentException("Source to convert from must be an instance of [" +
     sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
 }
 // 从缓存中获取GenericConverter实例,其实这一步相对复杂,匹配两个类型的时候,会解析整个类的层次进行对比
 GenericConverter converter = getConverter(sourceType, targetType);
 if (converter != null) {
  // 实际上就是调用转换方法
  Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
  // 断言最终结果和指定类型是否匹配并且返回
  return handleResult(sourceType, targetType, result);
 }
 return handleConverterNotFound(source, sourceType, targetType);
}

上面所有的可用的GenericConverter的实例可以在DefaultConversionService的addDefaultConverters中看到,默认添加的转换器实例已经超过20个,有些情况下如果无法满足需求可以添加自定义的转换器,实现GenericConverter接口添加进去即可。

小结

SpringBoot在抽象整个类型转换器方面做的比较好,在SpringMVC应用中,采用的是org.springframework.boot.autoconfigure.web.format.WebConversionService,兼容了Converter、Formatter、ConversionService等转换器类型并且对外提供一套统一的转换方法。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • .properties文件读取及占位符${...}替换源码解析

    前言 我们在开发中常遇到一种场景,Bean里面有一些参数是比较固定的,这种时候通常会采用配置的方式,将这些参数配置在.properties文件中,然后在Bean实例化的时候通过Spring将这些.properties文件中配置的参数使用占位符"${}"替换的方式读入并设置到Bean的相应参数中. 这种做法最典型的就是JDBC的配置,本文就来研究一下.properties文件读取及占位符"${}"替换的源码,首先从代码入手,定义一个DataSource,模拟一下JDB

  • javascript 文本框水印/占位符(watermark/placeholder)实现方法

    Firefox/Chrome/Opera从某一版本开始已经支持这一特性,但ie系列即使是ie9也还不支持,所以需要通过javascript来兼容这些不支持placeholder特性的浏览器. 普遍的做法 现在普遍使用的做法是通过表单元素的onfocus/onblur事件来改变value值,如下: 复制代码 代码如下: <input type="text" id="text1" /> <script> var el = document.get

  • 浅谈python中的占位符

    占位符,顾名思义就是插在输出里站位的符号.我们可以把它理解成我们预定饭店.当我们告诉饭店的时候,饭店的系统里会有我们的预定位置.虽然我们现在没有去但是后来的顾客就排在我们后面. 常见的占位符有三种: 1.%d 整数占位符 >>>'我考了%d分' % 20 '我考了20分' >>>'我考了%d分' % 20.5 ;我考了20分' >>>"我考了%d分,进步了%d分" % (50,10) "我考了50分,进步了10分"

  • 深入理解结构体中占位符的用法

    复制代码 代码如下: typedef union{    struct x{    char a1 : 2;    char b1 : 3;    char c1 : 3;    }x1;    char c;}my_un;int main(){    my_un a;    a.c = 100;    printf("%d/n",a.x1.c1);    printf("%d/n",sizeof(my_un)); return 0;} 输出结果:31即第一个是3,

  • iOS中修改UITextField占位符字体颜色的方法总结

    前言 最近学了UITextField控件, 感觉在里面设置占位符非常好, 给用户提示信息, 于是就在想占位符的字体和颜色能不能改变呢?下面是小编的一些简单的实现,有需要的朋友们可以参考. 修改UITextField的占位符文字颜色主要有三个方法: 1.使用attributedPlaceholder属性 @property(nullable, nonatomic,copy) NSAttributedString *attributedPlaceholder NS_AVAILABLE_IOS(6_0

  • 基于android布局中的常用占位符介绍

    大家在做布局文件是肯定会遇到过下面的这种情况 填充出现问题,所以需要用到占位符规范填充 汉字常用占位符: <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="这是测试:" android:textSize="22sp" /> <TextView android:layo

  • Json对象替换字符串占位符实现代码

    例如: 含有占位符的字符串hello,{name},your birthday is {birthday }; 提供的Json对象{name: "czonechan", birthday : "1989-07-02" } ; 替换后为 hello,czonechan,your birthday is 1989-07-02. 实现代码: 复制代码 代码如下: Object.prototype.jsonToString=function(str) { o=this; r

  • java字符串中${}或者{}等的占位符替换工具类

    正如标题所述,这是一个替换java字符串中${}或者{}等占位符的工具类,其处理性能比较令人满意.该类主要通过简单的改写myatis框架中的GenericTokenParser类得到.在日常开发过程中,可以将该类进行简单的改进或封装,就可以用在需要打印日志的场景中,现在张贴出来给有需要的人,使用方式参考main方法,不再赘述! public class Parser { /** * 将字符串text中由openToken和closeToken组成的占位符依次替换为args数组中的值 * @par

  • 解决Spring国际化文案占位符失效问题的方法

    写在前面:接下来很长一段时间的文章主要会记录一些项目中实际遇到的问题及对应的解决方案,在相应代码分析时会直指问题所在,不会将无关的流程代码贴出,感兴趣的读者可以自行跟踪.同时希望大家能够将心得体会在评论区分享出来,让大家共同进步! 环境或版本:Spring 3.2.3 现象:利用Spring自带的MessageSource来处理国际化文案,us状态下的文案有部分占位符未被替换,cn状态下的正常.文案如下: tms.pallet.order.box.qty=The total palletized

  • Spring Boot环境属性占位符解析及类型转换详解

    前提 前面写过一篇关于Environment属性加载的源码分析和扩展,里面提到属性的占位符解析和类型转换是相对复杂的,这篇文章就是要分析和解读这两个复杂的问题.关于这两个问题,选用一个比较复杂的参数处理方法PropertySourcesPropertyResolver#getProperty,解析占位符的时候依赖到 PropertySourcesPropertyResolver#getPropertyAsRawString: protected String getPropertyAsRawSt

  • Spring实战之属性占位符配置器用法示例

    本文实例讲述了Spring实战之属性占位符配置器用法.分享给大家供大家参考,具体如下: 一 配置文件 <?xml version="1.0" encoding="GBK"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xmlns

  • Spring boot项目部署到云服务器小白教程详解

    本篇文章主要介绍了Spring boot项目部署到云服务器小白教程详解,分享给大家,具体如下: 测试地址:47.94.154.205:8084 一.Linux下应用Shell通过SSH连接云服务器 //ssh 用户名@公网IP ssh josiah@ip // 输入密码 二.开始搭建SpringBoot的运行环境 1.安装JDK并配置环境变量 1) 打开JDK官网 www.oracle.com 2) 找面最新对应的JDK版本,下载 这里要注意的一个问题是:云服务器下载JDK时一定要在本地去ora

  • Spring Boot 2.0多数据源配置方法实例详解

    两个数据库实例,一个负责读,一个负责写. datasource-reader: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://192.168.43.61:3306/test?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false username: icbc password: icbc driver-class-na

  • Spring Boot 2 Thymeleaf服务器端表单验证实现详解

    这篇文章主要介绍了Spring Boot 2 Thymeleaf服务器端表单验证实现详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 表单验证分为前端验证和服务器端验证. 服务器端验证方面,Java提供了主要用于数据验证的JSR 303规范,而Hibernate Validator实现了JSR 303规范. 项目依赖加入spring-boot-starter-thymeleaf时,默认就会加入Hibernate Validator的依赖. 开

  • Spring Boot加密配置文件特殊内容的示例代码详解

    有时安全不得不考虑,看看新闻泄漏风波事件就知道了我们在用Spring boot进行开发时,经常要配置很多外置参数ftp.数据库连接信息.支付信息等敏感隐私信息,如下 ​ 这不太好,特别是互联网应用,应该用加密的方式比较安全,有点类似一些应用如电商.公安.安检平台.滚动式大屏中奖信息等显示身份证号和手机号都是前几位4109128*********和158*******.那就把图中的明文改造下1. 引入加密包,可选,要是自己实现加解密算法,就不需要引入第三方加解密库 <dependency> &l

  • 基于spring boot 1.5.4 集成 jpa+hibernate+jdbcTemplate(详解)

    1.pom添加依赖 <!-- spring data jpa,会注入tomcat jdbc pool/hibernate等 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <

  • Spring Boot项目中定制拦截器的方法详解

    这篇文章主要介绍了Spring Boot项目中定制拦截器的方法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Servlet 过滤器属于Servlet API,和Spring关系不大.除了使用过滤器包装web请求,Spring MVC还提供HandlerInterceptor(拦截器)工具.根据文档,HandlerInterceptor的功能跟过滤器类似,但拦截器提供更精细的控制能力:在request被响应之前.request被响应之后.视

  • 使用Spring Boot搭建Java web项目及开发过程图文详解

    一.Spring Boot简介 Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程.该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置.通过这种方式,Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者.SpringMVC是非常伟大的框架,开源,发展迅速.优秀的设计必然会划分.解耦.所以,spring有很多子项目,比如core.context.

  • Spring引入外部属性文件配置数据库连接的步骤详解

    直接配置数据库的信息 xml配置文件直接配置: <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.sprin

随机推荐