字符串模板格式化和解析-StrTemplate
# 📚简介
StrTemplate提供了一系列的封装对象用于操作字符串模板的格式化和解析。
字符串模板,或者叫模板字符串,用过SpringBoot的朋友应该不陌生,就是YAML配置文件中的占位符,例如定义这样一个模板字符串I'm ${age} years old.,
在这个字符串中,我们可以看到一个特别的部分${age},这就是占位符,可以在程序运行时,将外部age变量的值,直接替换进去,
然后生成结果字符串,例如age=18,则模板字符串格式化的结果为I'm 18 years old.。反过来,根据一个模板字符串和一个结果字符串,
也可以从结果字符串中解析出占位符对应的实际变量值。
请注意,这不是用于json序列化和反序列化的工具, 也不支持${nale | hutool}这样的默认值定义和递归解析。
在hutool之前的版本中,我们提供了StrFormatter、StrMatcher以及PlaceholderParser来辅助完成字符串模板的处理,
而在6.x版本之后, 我们提供了全新的StrTemplate类来处理。
# 名词定义
- 用
占位符的实际变量值去替换字符串模板中的占位符,最后生成结果字符串,这个过程我们称为格式化; - 根据
结果字符串和字符串模板提取出占位符的实际变量值,这个过程我们称为解析; 转义符:转义符是指,当字符串模板中有占位符,但是我们希望它在结果字符串里出现时使用的标记符。例如字符串模板为"?, 你要干什么?!",其中?为占位符, 但是其实只有第一个?是我们真正需要替换的占位符,第二个?仅仅是字符串的一部分。StrTemplate显然没有那么智能,能自己区分出来, 这个时候就需要用转义符标记一下,代表这个占位符只是普通字符,比如默认的转义符\\(单个反斜杠),最后的字符串模板定义为:”?, 你要干什么\?!“。 转义符是可以自定义的。
# 占位符类型
- 匿名占位符:
?,{},${},$$$,也叫单占位符,即一个不可拆分整体符号,它常用于格式化,在解析中用得很少; - 有名占位符:
{1},{name},#{id},它必须拥有前缀和后缀,以及一个有意义的名字,这个名字可以是数字,代表是数组或者列表的下标;
# 简单使用
# 匿名占位符
// 字符串模板
String commonTemplate = "select * from ? where id = ?" ;
// 根据 字符串模板 生成 匿名占位符处理器,我们使用 Builder 模式创建
SinglePlaceholderStrTemplate template = StrTemplate
// 创建一个匿名占位符处理器
.of(commonTemplate)
// 指定匿名占位符
.placeholder("?")
// 创建处理器
.build();
// 格式化
String format = template.format("user", 1001);
// "select * from user where id = 1001"
Console.log(format);
// 解析
// 从 结果字符串中 解析出 格式化时使用的实际值
List<String> matches = template.matches(format);
// ["user", "1001"],是的,很遗憾,解析出的值都是字符串
Console.log(matches);
# 有名占位符
# 普通使用
// 字符串模板
String commonTemplate = "select * from ${table} where id = ${id}";
// 根据 字符串模板 生成 有名占位符处理器,我们使用 Builder 模式创建
NamedPlaceholderStrTemplate template = StrTemplate
// 创建一个有名占位符处理器
.ofNamed(commonTemplate)
// 指定占位符的前缀
.prefix("${")
// 指定占位符的后缀
.suffix("}")
// 创建处理器
.build();
// 按顺序传递占位符的值,完成格式化
String format = template.formatSequence("user", 1001);
// "select * from user where id = 1001"
Console.log(format);
// 解析
// 从 结果字符串中 解析出 格式化时使用的实际值
Map<String, String> matches = template.matches(format);
//{"id"="1001", "table"="user"}
Console.log(matches);
// 从 结果字符串中 按顺序解析出 格式化时使用的实际值
List<String> matcheValueList = template.matchesSequence(format);
// ["user", "1001"]
Console.log(matcheValueList);
# 使用Bean对象提供参数值
@Data
public class ParameterBean {
private String table;
private Integer id;
}
...
// 格式化
// 构造用于传递参数的bean对象
ParameterBean bean = new ParameterBean();
bean.setTable("user");
bean.setId(1001);
// 字段名必须完全匹配,而且不支持@Alias注解
String format = template.format(bean);
// "select * from user where id = 1001"
Console.log(format);
// 解析出bean对象
// 从 结果字符串中 解析出 格式化时使用的实际值,并填充到传入的bean中,字段名必须完全匹配,而且不支持@Alias注解
ParameterBean parsedBean = template.matches(format, ParameterBean::new);
# 使用Map提供参数值
// 格式化
// 构造用于传递参数的Map
Map<String, Object> parameterMap = MapUtil.<String, Object>builder()
.put("table", "user")
.put("id", 1001)
.build();
// 使用Map传递占位符的值,完成格式化
String format = template.format(parameterMap);
// "select * from user where id = 1001"
Console.log(format);
// 解析
// 从 结果字符串中 解析出 格式化时使用的实际值
Map<String, String> matches = template.matches(format);
//{"id"="1001", "table"="user"}
Console.log(matches);
# 使用数组或者列表提供参数
// 字符串模板,注意下标顺序
String commonTemplate = "select * from ${1} where id = ${0}";
// 根据 字符串模板 生成 有名占位符处理器,我们使用 Builder 模式创建
NamedPlaceholderStrTemplate template = StrTemplate
// 创建一个有名占位符处理器
.ofNamed(commonTemplate)
// 指定占位符的前缀
.prefix("${")
// 指定占位符的后缀
.suffix("}")
// 创建处理器
.build();
// 构造参数列表
List<Object> parameterList = Arrays.asList(1001, "user");
// 使用列表传递占位符的值,完成格式化
String format = template.formatIndexed(parameterList);
// "select * from user where id = 1001"
Console.log(format);
// 解析
// 从 结果字符串中 解析出 格式化时使用的实际值
List<String> matcheValueList = template.matchesIndexed(format);
// ["1001", "user"]
Console.log(matcheValueList);
# 其他技巧
isMatches:校验传入的结果字符串是否和模板匹配;getPlaceholderVariableNames:获取占位符变量名称列表,例如,"{}"返回"{}"、"{name}"返回"name";getPlaceholderTexts:获取占位符的完整文本列表,例如,"{}"返回"{}"、"{name}"返回"{name}";formatRawByKey:格式化时,由用户根据占位符变量名,返回参数值;formatRawBySegment:格式化时,由用户根据解析后的占位符信息对象,返回参数值;matchesByKey:解析时,给用户传递占位符变量名和提取到的解析值,由用户决定如何使用,会使用默认值相关配置;matchesRawByKey:解析时,给用户传递占位符变量名和提取到的解析值,由用户决定如何使用,没有任何额外处理;matchesRawBySegment:解析时,给用户传递解析后的占位符信息对象和提取到的解析值,由用户决定如何使用,没有任何额外处理;
# 总结
# 匿名占位符
不论是格式化还是解析,主要都是根据顺序完成的。 格式化时,可以传递可变参,数组,迭代器; 解析时,可以获得实际值数组或者列表
# 有名占位符
核心在于根据名称映射,数组和列表的下标可以看作一种特殊的名称。 格式化时,可以基于顺序传递参数、可以基于下标传递,可以基于键值对映射结构,可以基于Bean对象; 解析时则是反过来,可以提取出前文提到的数据类型。
# 使用建议
建议将构造好的StrTemplate对象保存起来,就像使用jdk提供的正则表达式对象Pattern一样,
StrTemplate也是通过提前”编译“来加快格式化和解析速度的,如果用一次就丢弃给GC,不仅非常浪费,而且性能不佳。
# 进阶使用
# 默认值处理器
有时候,生活就是这样,明明说好要来,都定好位置了,最终却没来, 字符串模板格式化时也会遇到这个问题, 因此我们需要一种方法来处理占位符没有对应的实际值的情况。
此时可以设置默认值,在构造StrTemplate对象时就可以设置。
一共有3种方式:
StringdefaultValue: 不论占位符是什么,都提供同一个默认值。UnaryOperator<String>defaultValueHandler:根据传入的占位符名称,由用户决定返回的默认值。UnaryOperator<String>globalDefaultValueHandler:全局默认的默认值处理器,和defaultValueHandler相同,但是全局生效,后续每个创建的StrTemplate都会使用该默认值处理器。
特别的,如果这三者中有多个都设置了值(是故意的还是不小心?), 则优先使用defaultValue,其次defaultValueHandler,最后globalDefaultValueHandler。
# 策略
有了默认值还是不够,毕竟默认值只能用于占位符对应的参数值不存在,
如果参数值是null,格式化该如何处理?
或者解析时提取出了空字符串,到底是返回空字符串还是null?
有人觉得用空字符串代替就行;
有人站了出来:“可是我希望它把“null”字符串打印出来,这样调试时就能发现潜在的问题”;
又有人说:“我希望把占位符原样打印出来”;
极个别人说:“我希望把占位符里的变量名打印出来”;
更有甚者:“我希望它直接报错”;
因此我们需要一种方法来处理格式化和解析时,占位符没有实际值、值为空字符串或者null的情况,我们使用策略枚举来配置想要的处理方式StrTemplate.Feature。
# 格式化策略
# 占位符对应的值不存在时的策略组
FORMAT_MISSING_KEY_PRINT_WHOLE_PLACEHOLDER:打印完整的占位符,例如"${name}",原样打印"${name}",全局默认策略;FORMAT_MISSING_KEY_PRINT_DEFAULT_VALUE:打印默认值,如果没有默认值,则抛出异常;FORMAT_MISSING_KEY_PRINT_NULL:打印默认值,没有默认值,则打印null字符串;FORMAT_MISSING_KEY_PRINT_EMPTY:打印空字符串;FORMAT_MISSING_KEY_PRINT_VARIABLE_NAME:打印占位符变量名,例如:"{}"打印"{}"、"{name}"打印"name";FORMAT_MISSING_KEY_THROWS:直接抛出异常;
# 占位符对应的值为null时的策略组
FORMAT_NULL_VALUE_TO_STR:打印null字符串,全局默认策略;FORMAT_NULL_VALUE_TO_EMPTY:打印空字符串;FORMAT_NULL_VALUE_TO_WHOLE_PLACEHOLDER:打印完整的占位符,例如"${name}",原样打印"${name}";FORMAT_NULL_VALUE_TO_DEFAULT_VALUE:打印默认值,如果没有默认值,则抛出异常;
# 解析策略
解析策略分为三个策略组:解析出默认值,解析出空字符串,解析出"null"字符串。
# 解析出默认值时的策略组
MATCH_KEEP_DEFAULT_VALUE:原样返回,全局默认策略;MATCH_IGNORE_DEFAULT_VALUE:如果解析值等于默认值,就忽略这个占位变量名,对于返回Map类型,则结果中不包含这个key,对于下标类型数据不生效;MATCH_DEFAULT_VALUE_TO_NULL:如果解析值等于默认值,就视为null,对于返回Map类型,包含这个key;
# 解析出空字符串时的策略组
MATCH_EMPTY_VALUE_TO_NULL:返回null,全局默认策略;MATCH_EMPTY_VALUE_TO_DEFAULT_VALUE:转为默认值,如果没有默认值,则转为null;MATCH_IGNORE_EMPTY_VALUE:忽略该占位变量名,对于返回Map类型,则不包含这个key,对于下标类型数据不生效;MATCH_KEEP_VALUE_EMPTY:返回空字符串;
# 解析出"null"字符串时的策略组
MATCH_NULL_STR_TO_NULL:返回null,全局默认策略;MATCH_KEEP_NULL_STR:返回"null"字符串;MATCH_IGNORE_NULL_STR:忽略该占位变量名,对于返回Map类型,则不包含这个key,对于下标类型数据不生效;
# 使用策略的注意事项
- 每个策略组只能选择一个策略,构造
StrTemplate时添加同一个组的多个策略,只会保留最后添加的那个; - 每次构造
StrTemplate时会将策略设置为全局默认策略,可以提前修改全局默认策略的值;