简要说明
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取值分离。