消息模板解析填充方案

消息模板解析填充方案

需求描述

在涉及到消息推送相关的需求时,我们经常需要对数据按照模板中配置的字段解析,并填充模板。

例如我们的模板为:

 {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中的现有方法,让我们感觉源码真没白学。如果大佬们有其它优雅的方案,欢迎交流指点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

结构化思维wz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值