模板字段解析填充计划

需求描绘

在涉及到音讯推送相关的需求时,咱们经常需求对数据依照模板中装备的字段解析,并填充模板。

例如咱们的模板为:

{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中的现有办法,让咱们感觉源码真没白学。假如大佬们有其它高雅的计划,欢迎交流指点。