MyBatis通用token解析器GenericTokenParser

简要说明

MyBatis是java开发中绕不开的一个ORM框架,他里面有许多值得借鉴的设计模式、思路和方法,这里就以其中不太被常提及的sql解析工具类为切入点,介绍一下他的通用token解析工具。

例子讲解

这里我直接写了一个例子方便理解,使用场景为:输入带有#{}标记的表达式,使用map中的变量替换表达式的占位符,然后输出替换后的字符串:

import java.util.HashMap;
import java.util.Map;

import org.apache.ibatis.parsing.GenericTokenParser;
import org.apache.ibatis.parsing.TokenHandler;

public class GenericTokenParserTest {
	
	public static void main(String[] args) {
		Map<String,Object> map = new HashMap<>();
		map.put("name", "jack");
		map.put("age", 12);
		MyTokenHandler handler = new MyTokenHandler(map);
		
		GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
		String originStr = "my name is #{name}, I am #{age} years old";
	    String str = parser.parse(originStr);
	    System.out.println(str);
		
	}
	static class MyTokenHandler implements TokenHandler {
		private Map<String,Object> map;
		public MyTokenHandler(Map<String,Object>map) {
			this.map = map;
		}

		@Override
		public String handleToken(String content) {
			Object val = map.get(content);
			if(val==null) {
				return null;
			}
			return val.toString();
		}
		
	}
}

整个处理过程分为如下几步:

1.构造MyTokenHandler,他负责将变量解析为值

2.构造GenericTokenParser,传入token的开始标签、结束标签,以及第一步的handler

3.使用parser解析输入串,得到输出串

处理流程为,parser解析表达式串,解析中碰到#{}这样的占位符,就将变量名取出,调用handler得到变量值,拼到结果串中,然后继续解析,重复此过程,直到解析完整个串。

GenericTokenParser在mybatis中使用场景列举

查看该类的引用,eclipse使用ctrl+shift+g快捷键,IDEA使用alt+F7:

1.SqlSourceBuilder:

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

这里就是解析Mapper.xml里的#{}括起来的变量,比如 select name from student where id=#{id, jdbcType=VARCHAR}

这里ParameterMappingTokenHandler就是解析这个#{id}的,得到变量名到类型的映射,同时返回问号占位符,也就是将sql变成select name from student where id=?,同时将占位符对应变量的类型及取值方式保存起来了。

    @Override
    public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }

2.PropertyParser

  public static String parse(String string, Properties variables) {
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    return parser.parse(string);
  }

这里处理的就是mybatis中的${}变量,这种变量的特点是直接替换,所以有sql注入风险。

VariableTokenHandler:

    @Override
    public String handleToken(String content) {
      if (variables != null) {
        String key = content;
        if (enableDefaultValue) {
          final int separatorIndex = content.indexOf(defaultValueSeparator);
          String defaultValue = null;
          if (separatorIndex >= 0) {
            key = content.substring(0, separatorIndex);
            defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
          }
          if (defaultValue != null) {
            return variables.getProperty(key, defaultValue);
          }
        }
        if (variables.containsKey(key)) {
          return variables.getProperty(key);
        }
      }
      return "${" + content + "}";
    }
  }

这个与例子讲解就很相似了。构建TokenHandler时传入了一个variables对象,变量就从这里面取值。具体取值规则为VariableTokenHandler,他支持默认值,也就是${name:123}这样的方式,如果有默认值,没取到就用默认值;没有默认值,如果有值,就直接取值;否则就保留原样。也就是保留为${name}、

其他几个也都差不多。

GenericTokenParser代码解析

/**
 *    Copyright 2009-2019 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.parsing;

/**
 * @author Clinton Begin
 */
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
    // start为${的位置
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    // 转成char数组,挨个处理
    char[] src = text.toCharArray();
    int offset = 0;
    // builder是结果对象,边解析边拼接
    final StringBuilder builder = new StringBuilder();
    // expression是表达式,也就是${xxx}中间的xxx部分
    StringBuilder expression = null;
    while (start > -1) {
      // 支持转义,比如\${就不会认为是token开始
      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();
        // 找token关闭标签,也就是}的位置
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          // 关闭标签支持转义,也就是\}不认为是标签结束
          // 这里为什么要考虑end>offset?考虑${}的情况,没有变量,这时候没必要判断是否转义。
          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 {
          // 找到了,使用handler把变量解析为值
          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();
  }
}

扩展点

  • TokenHandler 取值方式是自由的,可以从上下文取,静态变量,SpringUtil,传入参数的等,就看用户怎么实现他了。
  • Token的开始结束标记是灵活的,可以{{}},可以${},#{}等任意方式。
  • 职责分离,把expr解析和token取值分离。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值