• Mybatis处理动态占位符实现


    背景

    最近做一个打招呼需求,打招呼的内容类似模板形式,但是模板中有动态占位符,比如:

    老乡式打招呼 -> “你好,我也是 xxx 的,我们是老乡呀!”(老乡见老乡,少来这套,来了就是深圳人)

    高学历牛逼式打招呼 -> “你好,我是 xxx 高材生,很高兴认识你!” (我心想,谁TM稀罕)

    炫富式打招呼 -> “你好,我年薪 xxx,能和你交个朋友么?”(你是想做py交易吧)

    模板就是这么简单,内容中的 xxx 是动态的,根据用户信息改变。看到这里的你是不是已经开始躁动了, String.replace()不就解决问题了么?是的,如果你是这么做的,那么恭喜你能快速完成任务!

    我这人想的比较多,如果PM后面要把模板改成一个文案中有多个 xxx ,并且多个 xxx 位置顺序不确定的情况怎么办?想到这里我脑海中出现的就是占位符,然后把值存到Map中,key就是 xxx。占位符习惯性想到用 ${xxx},当时想手撸一个解析${}工具类。由于我本人是比较懒的,绞尽脑汁想这种需求在业界应该很常见,有没有可用的工具类呢?想到工具类就肯定会想到apache的spring,spring加载xml文件中属性一般值会存放在properties文件中,这也是占位符的一种方式。还想到了mybatis中的sql动态替换不也是跟这需求差不多!

    Spring动态占位符实现

    影像中之前调试spring启动过程看到过对属性处理,后面再次调试是发现了 PropertyPlaceholderHelper,这个需求我就用的是这个工具类来实现的(当时没有撸过mybatis源码),具体代码如下:

    public class PropertyPlaceHolderUtil {
        private static final String placeholderPrefix = "${";
        private static final String placeholderSuffix = "}";
    
        private static final PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(placeholderPrefix, placeholderSuffix);
    
        private PropertyPlaceHolderUtil() {
        }
    
        public static final String replace(String text, Properties props) {
            if (StringUtils.isBlank(text) || props == null || props.isEmpty()) {
                return text;
            }
    
            return helper.replacePlaceholders(text, props);
        }
    
    }
    

    懒人是不会想着去造轮子的,但必须知道轮子的原理和应用!这个类在 spring-core包中,用到spring框架的应该基本都有该类。

    Mybatis动态占位符实现

    最近在撸mybatis源码,撸到parsing包(解析器模块)时意外发现Mybatis处理动态占位符实现。主要实现在GenericTokenParser 和 PropertyParser 类中。

    GenericTokenParser

    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
        // 寻找开始的 openToken 下标
        int start = text.indexOf(openToken, 0);
        // 如果没有包含 openToken 直接返回
        if (start == -1) {
          return text;
        }
        char[] src = text.toCharArray();
        int offset = 0;
        // 临时存放结果对象
        final StringBuilder builder = new StringBuilder();
        // 匹配到 openToken 和 closeToken 之间的表达式
        StringBuilder expression = null;
        // 循环匹配 ,text中有可能存在多个 ${} ${}
        while (start > -1) {
          // 转义字符
          if (start > 0 && src[start - 1] == '\') {
            // this open token is escaped. remove the backslash and continue.
            // 因为 openToken 前面一个位置是  转义字符,所以忽略 
            // 添加 [offset, start - offset - 1] 和 openToken 的内容,添加到 builder 中, 即把 ${ 放到builder中
            builder.append(src, offset, start - offset - 1).append(openToken);
            // 重新修改 offset 值,offset 往后移动
            offset = start + openToken.length();
          } else { // 非转义字符
            // found open token. let's search close token.
            if (expression == null) {
              expression = new StringBuilder();
            } else {
              expression.setLength(0);
            }
            // 添加 offset 和 openToken 之间的内容,添加到 builder 中
            builder.append(src, offset, start - offset);
            // 修改起始下标 offset
            offset = start + openToken.length();
            // 从 下标 offset 往后查找 closeToken 并返回下标值
            int end = text.indexOf(closeToken, offset);
            while (end > -1) {
              // 判断找到的 closeToken 是否是被转义字符修饰的
              if (end > offset && src[end - 1] == '\') {
                // this close token is escaped. remove the backslash and continue.
                // 被转义字符修饰的 closeToken,把 offset 到该closeToken下标的字符添加到expression中
                expression.append(src, offset, end - offset - 1).append(closeToken);
                // 查找开始下标继续后移
                offset = end + closeToken.length();
                // 下一个 closeToken 所在下标值
                end = text.indexOf(closeToken, offset);
              } else {
                // 添加 offset 到 end 之间的内容到 expression 中, 如 ${key} 把key存放到 expression中
                expression.append(src, offset, end - offset);
                // 查找开始下标继续后移
                offset = end + closeToken.length();
                break;
              }
            }
            if (end == -1) {
              // close token was not found.
              // 如果没有找到closeToken,把剩下的都追加到 builder 中
              builder.append(src, start, src.length - start);
              // 把offset设置成最大下标,跳出外层的 while 循环
              offset = src.length;
            } else {
              // 把匹配到的表达式交给 handler 处理,并把结果追加到 builder 中
              builder.append(handler.handleToken(expression.toString()));
              // 查找开始下标继续后移
              offset = end + closeToken.length();
            }
          }
    
          // 查询下一个开始的 openToken 下标
          start = text.indexOf(openToken, offset);
        }
        // 拼接剩下内容
        if (offset < src.length) {
          builder.append(src, offset, src.length - offset);
        }
        return builder.toString();
      }
    }
    

    该类就是分析 text 中 openToken 和 closeToken成双成对的中间关键字,然后交给 handler 处理。

    PropertyParser

    public class PropertyParser {
    
      private static final String KEY_PREFIX = "org.apache.ibatis.parsing.PropertyParser.";
      /**
       * 是否开启默认值,如 ${key:v1} 如果key没有值默认设置 v1
       * The special property key that indicate whether enable a default value on placeholder.
       * <p>
       *   The default value is {@code false} (indicate disable a default value on placeholder)
       *   If you specify the {@code true}, you can specify key and default value on placeholder (e.g. {@code ${db.username:postgres}}).
       * </p>
       * @since 3.4.2
       */
      public static final String KEY_ENABLE_DEFAULT_VALUE = KEY_PREFIX + "enable-default-value";
    
      /**
       * The special property key that specify a separator for key and default value on placeholder.
       * <p>
       *   The default separator is {@code ":"}.
       * </p>
       * @since 3.4.2
       */
      public static final String KEY_DEFAULT_VALUE_SEPARATOR = KEY_PREFIX + "default-value-separator";
      /**
       * 是否开启设置默认值功能 如 ${key:v1} 如果variables中没有key属性默认设置 v1
       */
      private static final String ENABLE_DEFAULT_VALUE = "false";
      private static final String DEFAULT_VALUE_SEPARATOR = ":";
    
      private PropertyParser() {
        // Prevent Instantiation
      }
    
      public static String parse(String string, Properties variables) {
        VariableTokenHandler handler = new VariableTokenHandler(variables);
        // 1. 写死了只处理${}占位符
        GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
        return parser.parse(string);
      }
    
      private static class VariableTokenHandler implements TokenHandler {
        private final Properties variables;
        /**
         * 是否开启设置默认值功能 如 ${key:v1} 如果variables中没有key属性默认设置 v1
         */
        private final boolean enableDefaultValue;
        /**
         * 默认值分隔符号
         */
        private final String defaultValueSeparator;
    
        private VariableTokenHandler(Properties variables) {
          this.variables = variables;
          this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE));
          this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR);
        }
    
        private String getPropertyValue(String key, String defaultValue) {
          return (variables == null) ? defaultValue : variables.getProperty(key, defaultValue);
        }
    
        @Override
        public String handleToken(String content) {
          if (variables != null) {
            String key = content;
            // 开启默认值
            if (enableDefaultValue) {
              // 分离key和默认值
              final int separatorIndex = content.indexOf(defaultValueSeparator);
              String defaultValue = null;
              if (separatorIndex >= 0) {
                key = content.substring(0, separatorIndex);
                defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
              }
              // variables存在key属性直接返回,否则返回默认值
              if (defaultValue != null) {
                return variables.getProperty(key, defaultValue);
              }
            }
            // 没有默认值,如果variables中存在返回该属性值
            if (variables.containsKey(key)) {
              return variables.getProperty(key);
            }
          }
          // 直接返回
          return "${" + content + "}";
        }
      }
    
    }
    

    该类构造函数是private的,注定了他是一个静态工具类。1处写死了只处理${}方式的占位符。

    VariableTokenHandler 提供了实现默认值方式。

    测试

    看源码时最好是用源码提供的测试单元来debug,用GenericTokenParserTest来debug GenericTokenParser实现,用PropertyParserTest 来debug PropertyParser 实现。

    总结

    作为代码的搬运工,不要轻易的造轮子,因为自己造的轮子可能存在隐患的bug,用开源的工具会安全很多,毕竟那么多开发者帮他们测试过。但是用别人的东西最基本得知道怎么用!!!

  • 相关阅读:
    JavaScript中的闭包(closure)
    JavaScript中的继承与原型链
    一个PHP操作大变量的例子
    深入PHP内核之参数
    django创建一个简单的web站点
    django通过添加session来保存公共变量
    第一次登录mysql,使用任何命令都报错ERROR 1820 (HY000): You must reset your password using ALTER USER statement before executing this statement.
    django搭建的站点,通过localhost能访问,但是通过ip不能访问
    django models实际操作中遇到的一些问题
    python3 django连接mysql,同步表结构
  • 原文地址:https://www.cnblogs.com/wolf-bin/p/12582584.html
Copyright © 2020-2023  润新知