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,用開源的工具會安全很多,畢竟那么多開發者幫他們測試過。但是用別人的東西最基本得知道怎么用!!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM