模板字段解析填充计划
需求描绘
在涉及到音讯推送相关的需求时,咱们经常需求对数据依照模板中装备的字段解析,并填充模板。
例如咱们的模板为:
{startTime}监控到{alertName}报警,报警概况如下:{detail.data},请尽快处理!
咱们需求从报警数据中,解析{}
中装备的字段,获取对应的值并填充到模板最终生成咱们要发送的音讯内容。
考虑:这个需求怎样感觉似曾相识经常遇到呢?
恍然大悟,咱们平常用
MyBatis
的时分基本上每个SQL
都会用到#{}
这种方式的参数啊。那咱们这个需求是不是能够复用MyBatis源码中的某个类呢?
计划规划
既然咱们要封装一个东西,那么咱们就要让它能适用于多种场景。
Java中最常见的数据无非便是目标和JSON,所以咱们要提供这两种类型的处理计划。不论数据源是目标仍是JSON都能经过模板中的字段解析出来。而且需求支撑嵌套类型的解析。
咱们之前起早贪黑的学习源码,现在用它的时分不就来了吗?咱们先看看MyBatis中是怎样解析的:GenericTokenParser#parse(String text)
。咱们能够发现,咱们只需求对不同的事务完结不同的handler即可。(即使你的项目里没有用MyBatis,那你完全能够把这个类copy下来以完结咱们解析的需求)。
public class GenericTokenParser {
private final String openToken; // 界说开端符号符
private final String closeToken; // 界说完毕符号符
private final TokenHandler handler; // 处理符号的接口
public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
this.openToken = openToken;
this.closeToken = closeToken;
this.handler = handler;
}
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// search open token 查找开端符号符
int start = text.indexOf(openToken);
if (start == -1) {
return text;
}
char[] src = text.toCharArray(); // 将待解析的文本转换为字符数组
int offset = 0; // 偏移量,符号已解析的文本长度
final StringBuilder builder = new StringBuilder(); // 用于构建成果字符串的可变字符串
StringBuilder expression = null; // 用于构建注释的可变字符串
while (start > -1) { // 循环直到找不到开端符号符
if (start > 0 && src[start - 1] == '\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset); // 查找完毕符号符
while (end > -1) { // 循环直到找不到完毕符号符
if (end > offset && src[end - 1] == '\') {
// this close token is escaped. remove the backslash and continue.
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset); // 持续查找完毕符号符
} else {
expression.append(src, offset, end - offset);
break;
}
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
builder.append(handler.handleToken(expression.toString())); // 处理注释部分
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset); // 持续查找开端符号符
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset); // 将剩余文本添加到成果字符串中
}
return builder.toString(); // 回来解析后的成果字符串
}
}
既然咱们已经找到了方向,那就来看看类该怎样规划吧。
计划完结
TemplateParser
咱们之前看了MyBatis的源码,咱们再来看看MyBatis中是如何运用的:
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
//handler
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
//结构办法
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
所以咱们能够照葫芦画瓢,咱们界说一个TemplateParser
类:
public class TemplateParser {
private final GenericTokenParser tokenParser;
private final TokenHandler tokenHandler;
public TemplateParser(TokenHandler tokenHandler) {
this.tokenHandler = tokenHandler;
this.tokenParser = new GenericTokenParser("{", "}", this::handleToken);
}
public String parseTemplate(String template) {
return tokenParser.parse(template);
}
private String handleToken(String content) {
return tokenHandler.handleToken(content);
}
}
这个类界说好后,咱们的全体思路很明确,经过完结TokenHandler
接口来完结不同的事务功用。
东西类的界说
下面,咱们界说东西类,也便是此功用的进口:
public class MessageBuildUtils {
/**
* 告警音讯,依据json填充模板
* @param json json字符串
* @param template 音讯模板
* @return 结构完结的音讯
*/
public static String buildMsgByJson(String json,String template){
// 创立 TokenHandler 处理器
TokenHandler handler = new JsonTokenHandler(json);
// 创立 TemplateParser 解析器
TemplateParser parser = new TemplateParser(handler);
// 解析并替换占位符为实践值
return parser.parseTemplate(template);
}
/**
* 依据目标填充模板
* @param obj 目标
* @param template 音讯模板
* @return 结构完结的音讯
*/
public static String buildMsgByObj(Object obj,String template){
// 创立 TokenHandler 处理器
TokenHandler handler = new ObjectTokenHandler(obj);
// 创立 TemplateParser 解析器
TemplateParser parser = new TemplateParser(handler);
// 解析并替换占位符为实践值
return parser.parseTemplate(template);
}
}
界说好了进口,咱们就去写JSON和obj两种类型的完结类。
TokenHandler
JSON处理器
public class JsonTokenHandler implements TokenHandler {
private final ObjectMapper objectMapper;
private JsonNode jsonNode;
/**
* 结构函数,接收一个 JSON 字符串作为参数,并初始化 ObjectMapper 和 JsonNode
* @param json json
*/
public JsonTokenHandler(String json) {
objectMapper = new ObjectMapper();
try {
// 将 JSON 字符串解析为 JsonNode 目标
jsonNode = objectMapper.readTree(json);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public String handleToken(String content) {
//初始值,也便是假如没匹配到模板中要填充什么?
String value = "'-'";
if (jsonNode != null) {
String[] fieldPath = content.split("\.");
JsonNode currentNode = jsonNode;
for (String fieldName : fieldPath) {
if (currentNode.isObject()) {
currentNode = currentNode.get(fieldName);
} else {
// 假如当时节点不是目标,则尝试解析为字符串方式的 JSON
try {
JsonNode jsonValue = objectMapper.readTree(currentNode.asText());
if (jsonValue != null && jsonValue.isObject()) {
currentNode = jsonValue.get(fieldName);
} else {
break;
}
} catch (Exception e) {
break;
}
}
if (currentNode == null) {
break;
}
}
//将json中解析到的数据做特别处理
if(currentNode != null){
value = currentNode.asText();
}
}
return "null".equals(value) ? "'-'" : value;
}
}
Obj处理器
@Slf4j
public class ObjectTokenHandler implements TokenHandler {
private final Object obj;
public ObjectTokenHandler(Object obj) {
this.obj = obj;
}
@Override
public String handleToken(String content) {
// 依据占位符的内容从目标中获取对应的值
String value = "'-'";
try {
value = getObjectValue(obj, content);
} catch (Exception e) {
// 处理异常情况,例如字段不存在等
e.printStackTrace();
}
return value;
}
/**
* 获取目标中指定字段的值
*/
private String getObjectValue(Object obj, String fieldPath) throws Exception {
//将字段途径按点号拆分为多个字段名
String[] fields = fieldPath.split("\.");
// 从 obj 目标开端,逐级获取字段值
Object fieldValue = obj;
for (String field : fields) {
// 获取当时字段名对应的字段值
fieldValue = getFieldValue(fieldValue, field);
if (fieldValue == null) {
// 假如字段值为空,则退出循环
break;
}
}
// 将字段值转换为字符串并回来
return fieldValue != null ? fieldValue.toString() : "'-'";
}
/**
* 获取目标中指定字段的值
*/
private Object getFieldValue(Object obj, String fieldName) throws Exception {
// 获取字段目标
Field field = obj.getClass().getDeclaredField(fieldName);
// 设置字段可访问
field.setAccessible(true);
// 获取字段值
return field.get(obj);
}
}
到这里,咱们就能够愉快的写个测试类,调用一下东西类中的办法了。
拓展与优化
考虑:会不会有这种情况呢?
我JSON中的数据一些状况值是0、1这种,但是咱们音讯中需求时具体的状况说明;
又或许JSON中的数据中关于时刻的格局不对,咱们需求年月日能够看的很清楚的表述,数据却是时刻戳;
又或许模板中的某一个字段,咱们需求有兜底战略,这个字段必须有值…….
为了完结上述的功用,而不影响咱们之前封装的Handler。我做了如下规划:
添加SpFieldHandler
接口,用于界说某个事务中对特定特别字段的处理逻辑。
public interface SpFieldHandler<T> {
/**
* 解析特别字段,配合TokenHandler运用
* @param filedName 字段名
* @param node 值 能够是JsonNode 也可也是Object
* @return 处理完的字符串
*/
String parseSpFieldValue(String filedName, T node);
}
举个例子,例如咱们处理报警事务中,有一些特别字段需求处理。数据来历是经过Kafka推送过来的,所以咱们发送音讯需求对其间的一些字段二次处理。
public class AlarmJsonSpHandler implements SpFieldHandler<JsonNode>{
//特别字段
List<String> spFieldList = Arrays.asList("startTime", "endTime", "state", "alarmSource");
@Override
public String parseSpFieldValue(String filedName, JsonNode node) {
if(spFieldList.contains(filedName)){
//假如是特别字段
switch (filedName){
case "startTime":
case "endTime": {
return DateUtil.format(new Date(node.asLong()), "yyyy.MM.dd HH:mm:ss");
}
case "state":{
return AlertStatusEnum.getValueByCode(node.asInt());
}
case "alarmSource":{
return AlertOriginEnum.getValueByCode(node.asInt());
}
default: return node.asText();
}
}else {
//非特别字段
return node != null ? node.asText() : "null";
}
}
}
咱们再对JsonTokenHandler
进行优化:
public class JsonTokenHandler implements TokenHandler {
private final ObjectMapper objectMapper;
private JsonNode jsonNode;
private final SpFieldHandler<JsonNode> spFieldHandler;
/**
* 结构函数,接收一个 JSON 字符串作为参数,并初始化 ObjectMapper 和 JsonNode
* @param json json
*/
public JsonTokenHandler(String json,SpFieldHandler<JsonNode> spFieldHandler) {
this.spFieldHandler = spFieldHandler;
objectMapper = new ObjectMapper();
try {
// 将 JSON 字符串解析为 JsonNode 目标
jsonNode = objectMapper.readTree(json);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public String handleToken(String content) {
String value = "'-'";
if (jsonNode != null) {
String[] fieldPath = content.split("\.");
JsonNode currentNode = jsonNode;
for (String fieldName : fieldPath) {
if (currentNode.isObject()) {
currentNode = currentNode.get(fieldName);
} else {
// 假如当时节点不是目标,则尝试解析为字符串方式的 JSON
try {
JsonNode jsonValue = objectMapper.readTree(currentNode.asText());
if (jsonValue != null && jsonValue.isObject()) {
currentNode = jsonValue.get(fieldName);
} else {
break;
}
} catch (Exception e) {
break;
}
}
if (currentNode == null) {
break;
}
}
//将json中解析到的数据做特别处理
value = spFieldHandler.parseSpFieldValue(content,currentNode);
}
return "null".equals(value) ? "'-'" : value;
}
}
结构办法中添加了SpFieldHandler
的入参,并且在最后将节点交给SpFieldHandler
进行处理。
咱们的Utils中则需求将对应的SpFieldHandler
作为参数传入TokenHandler
。
public static String buildAlarmMsgByJson(String json,String template){
// 创立 TokenHandler 处理器
TokenHandler handler = new JsonTokenHandler(json,new AlarmJsonSpHandler());
// 创立 TemplateParser 解析器
TemplateParser parser = new TemplateParser(handler);
// 解析并替换占位符为实践值
return parser.parseTemplate(template);
}
关于此办法的功能呢,我大致的写了一个测试类,一秒内在我的电脑上(一台破win)能够履行1w次左右。
此计划特色是应用了MyBatis中的现有办法,让咱们感觉源码真没白学。假如大佬们有其它高雅的计划,欢迎交流指点。