Mybatis在处理${}和#{}占位符时,底层使用到了GenericTokenParser类和TokenHandler的实现类。它的实现原理及其简单。
GenericTokenParser的作用是完成对字符窜中${}和#{}的内容定位,每次定位完成后,调用TokenHandler进行内容替换。
GenericTokenParser的定位原理很简单,将${}或者#{}进行拆分。${或#{部分为openToken,}部分为closeToken,然后不断使用indexOf进行循环定位替换。
package org.apache.ibatis.parsing;
/**
* 通用属性解析器 用于解析占位符标签
*/
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;
}
/**
*这段代码主要处理{@code text}拥有多个符合以{@code openToken}开头,{@code closeToken}结尾的字符窜的情况
*
* 同时还要处理拥有{@code openToken}或{@code closeToken},但是使用了转义字符的情况。
*
* @param text
* @return
*/
public String parse(String text) {
//非空判断
if (text == null || text.isEmpty()) {
return "";
}
// search open token
//获取第一个{@code openToken}的位置
int start = text.indexOf(openToken);
//如果这个位置不存在 则直接返回原字符窜
if (start == -1) {
return text;
}
//生成原字符窜的数组
char[] src = text.toCharArray();
//字符窜拥有多个符合条件的{@code openToken}时,将会进行多轮分析,以确认每一轮的{@code openToken}在原字符窜的位置,而
//offset就表示每一轮解析时,应该从原字符窜的哪个位置开始
int offset = 0;
//builder是拼接最后结果,进行输出的
final StringBuilder builder = new StringBuilder();
//expression的内容表示{@code openToken}和{@code closeToken}之间的内容
StringBuilder expression = null;
//下面这个循环 就是循环处理多个{@code openToken}、{@code closeToken}的情况
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
//寻找{@code openToken}的条件分支一:这一个条件判断 是处理出现了{@code openToken},但是这个{@code openToken}前面出现了转移字符
// this open token is escaped. remove the backslash and continue.
//这里表示既然遇到了转义字符 那么这个开始标识符不能当做开始标识符
// 因此它不是需要替换的部分,所以就要将从本轮开启的位置 到{@code openToken}结束位置的字符都直接拼接到{@code builder}上
builder.append(src, offset, start - offset - 1).append(openToken);
//确认新一轮的开始位置
offset = start + openToken.length();
} else {
//寻找{@code openToken}的条件分支二:下面的条件判断表示 出现了{@code openToken} 且 这个{@code openToken}前面没有转移字符的情况===
// found open token. let's search close token.
//重置复用expression
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
//这里表示如果有转义字符 则拼接转义的开始字符到真正的开始字符之间的部分
builder.append(src, offset, start - offset);
//{@code openToken}找到了,接下来来需要找{@code closeToken},其实{@code closeToken}的状况和{@code openToken}
//一样的情况
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
//遍历循环一直找{@code closeToken}的位置
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
//寻找{@code closeToken}的分支条件一:如果找到的{@code closeToken}是拥有转义字符的,则继续寻找,但是expression需要拼接本轮解析开始
//位置到{@code openToken}间的字符,因为这个也属于{@code openToken}和{@code closeToken}间的内容,然后进行下一轮
// 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 {
//寻找{@code closeToken}的分支条件二:这里表示找到了符合条件的{@code closeToken},那么将内容拼接到{@code expression}里
expression.append(src, offset, end - offset);
break;
}
}
if (end == -1) {
//综合评定 条件分支一:{@code closeToken}位置没有找到,那么结束了,直接拼接
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
//综合评定 条件分支二:{@code closeToken}位置也找到了,那么说明expression里也存放好了{@code openToken}和{@code closeToken}
//内容,这时候用handler去处理。
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
//这里表示 从offset位置 从新获取start的位置,很显然如果为0,start还是不变
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
}
TokenHandler的作用也很简单,他针对传入的字符窜获取对应的内容,至于如何获取的内容,将交由它的子类去实现。这些子类分别有BindingTokenParser、DynamicCheckerTokenParser、ParameterMappingTokenHandler、VariableTokenHandler等,这些实现类的唯一区别就是,获取content对应的数据源来自哪里。
public interface TokenHandler {
//根据传入的content获取对应的内容
String handleToken(String content);
}
DynamicCheckerTokenParser的作用是配合GenericTokenParaser使用的,它的目的是,一旦该对象的handleToken被调用,就证明字符窜里有符合GenericTokenParaser
的openToken和closeToken的内容。源码如下:
/**
* 动态标签 这个类的作用主要是看是否被调用过,如果被调用过isDynamic必为true
*/
private static class DynamicCheckerTokenParser implements TokenHandler {
private boolean isDynamic;
public DynamicCheckerTokenParser() {
// Prevent Synthetic Access
}
public boolean isDynamic() {
return isDynamic;
}
/**
* 如果这个方法
*
* @param content
* @return
*/
@Override
public String handleToken(String content) {
this.isDynamic = true;
return null;
}
}
DynamicCheckerTokenParser使用实再解析XML的Sql语句时,查看是否Sql里是否包含${},如果包含就叫动态SQl
ParameterMappingTokenHandler的作用是配合着GenericTokenParaser完成Mybatis的占位符#{}格式的处理。它的处理方式是将每个#{}的内容,使用?进行替换,并且将#{}里的内容转变成ParameterMapping对象。
其代码如下:
/**
* 用于处理 #{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}格式的入参
*/
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
/**
* {@link org.apache.ibatis.mapping.ParameterMapping}
*/
private List parameterMappings = new ArrayList<>();
private Class> parameterType;
private MetaObject metaParameters;
public ParameterMappingTokenHandler(Configuration configuration, Class> parameterType, Map additionalParameters) {
super(configuration);
this.parameterType = parameterType;
this.metaParameters = configuration.newMetaObject(additionalParameters);
}
public List getParameterMappings() {
return parameterMappings;
}
/**
* 处理展位符
*
* @param content
* @return
*/
@Override
public String handleToken(String content) {
//这里对内容进行了分析,并且注册到全局里。
parameterMappings.add(buildParameterMapping(content));
//结果返回的是一个?,也就是说#{}类型的参数都会被替换成?
return "?";
}
private ParameterMapping buildParameterMapping(String content) {
// #{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}
//这一步处理,是将{@code content}的内容进行了拆分
Map propertiesMap = parseParameterMapping(content);
//将参数进行组装
String property = propertiesMap.get("property");
Class> propertyType;
if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
propertyType = metaParameters.getGetterType(property);
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
propertyType = parameterType;
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
propertyType = java.sql.ResultSet.class;
} else if (property == null || Map.class.isAssignableFrom(parameterType)) {
propertyType = Object.class;
} else {
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
if (metaClass.hasGetter(property)) {
propertyType = metaClass.getGetterType(property);
} else {
propertyType = Object.class;
}
}
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
Class> javaType = propertyType;
String typeHandlerAlias = null;
for (Map.Entry entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {
builder.mode(resolveParameterMode(value));
} else if ("numericScale".equals(name)) {
builder.numericScale(Integer.valueOf(value));
} else if ("resultMap".equals(name)) {
builder.resultMapId(value);
} else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
} else if ("jdbcTypeName".equals(name)) {
builder.jdbcTypeName(value);
} else if ("property".equals(name)) {
// Do Nothing
} else if ("expression".equals(name)) {
throw new BuilderException("Expression based parameters are not supported yet");
} else {
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + PARAMETER_PROPERTIES);
}
}
if (typeHandlerAlias != null) {
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
}
return builder.build();
}
/**
* 解析#{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap} 解析结果是一个Map里
*
* @param content
* @return
*/
private Map parseParameterMapping(String content) {
try {
//这里可以看出来ParameterExpression是继承了Map的
return new ParameterExpression(content);
} catch (BuilderException ex) {
throw ex;
} catch (Exception ex) {
throw new BuilderException("Parsing error was found in mapping #{" + content + "}. Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex);
}
}
}
我们发现对于,handlerToken的方法是极其简单的,只要被调用,就会返回?。比较复杂的是如何将#{}的内容转换成ParameterMapping对,完成该这一动作的核心代码是在
buildParameterMapping方法里,而解析内容则放到了parseParameterMapping方法里,而这个方法也很简单,直接返回了ParameterExpression对象,也就是说对于#{}内容的解析是在 ParameterExpression内部进行的。这个解析过程是怎么样的呢?
ParameterExpression介绍
ParameterExpression内部完成了#{}内容的分解,并将其属性以key/value的形式放到HashMap里。具体的解析如下
public class ParameterExpression extends HashMap {
private static final long serialVersionUID = -2417552199605158680L;
/**
* 这里完成了实际的实际的内容拆分
*
* @param expression
*/
public ParameterExpression(String expression) {
parse(expression);
}
/**
* 完成对{@code expression}的解析代码
*
* @param expression
*/
private void parse(String expression) {
//这一步目的是为了去掉{@code expression}开头部分的滤掉控制字符或通信专用字符、和(
int p = skipWS(expression, 0);
// "("的ASC码为0x28
if (expression.charAt(p) == '(') {
expression(expression, p + 1);
} else {
property(expression, p);
}
}
/**
* 这里是找到闭合的的表达式()
*
* @param expression
* @param left
*/
private void expression(String expression, int left) {
int match = 1;
int right = left + 1;
//这一段代码就是找到闭合的() 这里如果到结束都找到不 会抛异常
while (match > 0) {
if (expression.charAt(right) == ')') {
match--;
} else if (expression.charAt(right) == '(') {
match++;
}
right++;
}
put("expression", expression.substring(left, right - 1));
jdbcTypeOpt(expression, right);
}
/**
* 最左边的位置
*
* @param expression 表示要解析的内容
* @param left 表示{@code expression}的位置
*/
private void property(String expression, int left) {
if (left < expression.length()) {
//这段代码是从{@code expression}的{@code left}的位置,获取第一个符合",:"的内容
int right = skipUntil(expression, left, ",:");
//trimmedStr的作用是截取{@code expression}从{@code left}到{@code right}之间的字符窜
put("property", trimmedStr(expression, left, right));
jdbcTypeOpt(expression, right);
}
}
/**
* 剔除{@code expression}的开头部分中所有的控制字符或通信专用字符
*
* @param expression 需要查看的字符窜
* @param p 本轮expression开始的位置
* @return 从{@code expression}的{@code p}位置开始 获取第一个非控制字符或通信专用字符所在位置
*/
private int skipWS(String expression, int p) {
for (int i = p; i < expression.length(); i++) {
// 0x20是Asc空格,这个条件的意思是过滤掉控制字符或通信专用字符
if (expression.charAt(i) > 0x20) {
return i;
}
}
return expression.length();
}
/**
* 从{@code expression}的p位置起,查找任意一个包含在{@code endChars}里字符,并且返回其在{@code expression}中的位置
*
* @param expression 表达式
* @param p 表示{@code expression}开始的位置
* @param endChars 表示结束的字符,如果{@code expression}包含{@code endChars}里的任何字符,都返回其对应的位置
* @return 从{@code expression}的p位置起,查找任意一个包含在{@code endChars}里字符,并且返回其在{@code expression}中的位置
*/
private int skipUntil(String expression, int p, final String endChars) {
for (int i = p; i < expression.length(); i++) {
char c = expression.charAt(i);
if (endChars.indexOf(c) > -1) {
return i;
}
}
return expression.length();
}
/**
* jdbc的选项剔除
*
* @param expression
* @param p
*/
private void jdbcTypeOpt(String expression, int p) {
//过滤从{@code expression}的{@code p}位置,过滤到控制字符和通信字符 已经(
p = skipWS(expression, p);
if (p < expression.length()) {
if (expression.charAt(p) == ':') {
//这一步的条件说明对于#{}的内容配置,其实是可以写成#{property:jdbcType}类型的,如果是这样的类型就走这一步
jdbcType(expression, p + 1);
} else if (expression.charAt(p) == ',') {
//这一步是走#{property,jdbcType=xx...}等类型的解析
option(expression, p + 1);
} else {
throw new BuilderException("Parsing error in {" + expression + "} in position " + p);
}
}
}
/**
* 这里可以看出来,#{}可以使用propertis:jdbcType的方式进行设置
*
* @param expression
* @param p
*/
private void jdbcType(String expression, int p) {
int left = skipWS(expression, p);
int right = skipUntil(expression, left, ",");
if (right > left) {
put("jdbcType", trimmedStr(expression, left, right));
} else {
throw new BuilderException("Parsing error in {" + expression + "} in position " + p);
}
option(expression, right + 1);
}
/**
* 递归处理的 xxx=xxx的
*
* @param expression
* @param p
*/
private void option(String expression, int p) {
int left = skipWS(expression, p);
if (left < expression.length()) {
int right = skipUntil(expression, left, "=");
String name = trimmedStr(expression, left, right);
left = right + 1;
right = skipUntil(expression, left, ",");
String value = trimmedStr(expression, left, right);
put(name, value);
option(expression, right + 1);
}
}
/**
* 获取stri的start和end之间的内容,并且清除控制字符或通信专用字符
*
* @param str
* @param start
* @param end
* @return
*/
private String trimmedStr(String str, int start, int end) {
//0x20以下的都是控制字符或通信专用字符
while (str.charAt(start) <= 0x20) {
start++;
}
while (str.charAt(end - 1) <= 0x20) {
end--;
}
return start >= end ? "" : str.substring(start, end);
}
}
从分析代码里我们可以得出如下两个结论
结论一:#{}内部支持
#{property, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}类型的解析,也支持
#{property:javaType}类型的解析。
结论二:#{}内部 会自动过滤到非每个属性前后的()和控制字符、通信字符等。
VariableTokenHandler主要是配合GenericTokenParaser完成Mybatis的占位符${}格式的处理,这个处理相对来说比较简单,就是完成${}内容的全局替换。同时这里面有两个属性比较重要分别是
org.apache.ibatis.parsing.PropertyParser.enable-default-value和org.apache.ibatis.parsing.PropertyParser.default-value-separator,他分别代表着是否开启默认值,以及如果开启了默认值,默认值和key之间的分隔符是什么。org.apache.ibatis.parsing.PropertyParser.enable-default-value默认值为false,org.apache.ibatis.parsing.PropertyParser.default-value-separator默认为:。如果开启了默认值,我们就可以写作${AA:BB}这样的入参,如果AA对应的值不存在,则他的默认值就是BB
private static class VariableTokenHandler implements TokenHandler {
private final Properties variables;
//这个表示是否使用默认值 默认是不适用
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);
}
/**
* 这里表示 从外部参数里获取enable-default-value的对应的值 并且默认为false
*
* @param key
* @param defaultValue
* @return
*/
private String getPropertyValue(String key, String defaultValue) {
return (variables == null) ? defaultValue : variables.getProperty(key, defaultValue);
}
/**
* 解析占位符的内容,content就是占位符的内容,这里表示能解析就解析,不能解析返回原值
*
* @param content
* @return
*/
@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 + "}";
}
}
BindingTokenParser的作用是使用OGNL表达式,完成对content内容的解析,该类的最好体现就是
/**
* 解决OGl表达式
*/
private static class BindingTokenParser implements TokenHandler {
/**
* 相当于OGl的表达式
*/
private DynamicContext context;
/**
* 正则拦截器
*/
private Pattern injectionFilter;
public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
this.context = context;
this.injectionFilter = injectionFilter;
}
@Override
public String handleToken(String content) {
/
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
context.getBindings().put("value", parameter);
}
//使用Ognl进行解析
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
checkInjection(srtValue);
return srtValue;
}
private void checkInjection(String value) {
if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
}
}
}
OgnlCache
OgnlCache的作用是对ONGL表达式进行了封装,因此在了解OgnlCache的作用前必须要先熟悉Ognl表达式的使用流程。Ognl的使用如下:
@Test
public void paraseToken() throws OgnlException {
//第一步创建数据源
Map map = new HashMap<>();
User user = new User();
user.setName("test");
user.setAge(2);
map.put("a", 1);
map.put("b", user);
//第二步 创建元素的访问权限
MemberAccess memberAccess = new IMemberClass();
//第三步 创建类解析器
ClassResolver classResolver = new DefaultClassResolver();
//第四步 创建
TypeConverter converter = new DefaultTypeConverter();
/**
* 上下文配置信息
*/
OgnlContext context = (OgnlContext) Ognl.createDefaultContext(map, memberAccess, classResolver, converter);
/**
* 表达式
*/
Object expression = Ognl.parseExpression("3==b.age + a");
//调用Ognl表达式获取结果
Object result2 = Ognl.getValue(expression, context,map);
System.out.println(result2);
}
OGNL有三个关键性语法,OGNL表达式、OGNL上下文、根对象,关于这三个参数可参考这篇文章。需要在这篇文章上补充的是,OGNL上下文,该对象包含四个参数
,分别是MemberAccess对象、ClassResolver对象、还有TypeConverter。
MemberAccess
MemberAccess对象主要是控制每个属性的访问权限,用到的方法是isAccessible方法,如果该方法返回的为false,则说明无访问权限。
public interface MemberAccess
{
public Object setup(Map context, Object target, Member member, String propertyName);
public void restore(Map context, Object target, Member member, String propertyName, Object state);
/**
* 对每个属性的访问权限进行控制
* @param context 上下文配置
* @param target 目标属性目标对象
* @param member 通过{@code target}获取{@code propertyName}对应的值时调用的方法其访;
* @param propertyName 属性名称
* @return
*/
public boolean isAccessible(Map context, Object target, Member member, String propertyName);
}
TypeConverter
TypeConverter的作用,是在Ognl表达式调用getValue(Object tree, Map context, Object root, Class resultType)时调用,该接口只有一个方法
public interface TypeConverter
{
/**
* 对每个属性的访问权限进行控制
* @param context 上下文配置
* @param target 目标属性目标对象
* @param member 通过{@code target}获取{@code propertyName}对应的值时调用的方法其访;
* @param propertyName 属性名称
* @param value 表示propertyName对应的值
* @param toType 表示Ognl想要将结果转换的类
* @return
*/
public Object convertValue(Map context, Object target, Member member, String propertyName, Object value, Class toType);
}
对象就是基于以上案例进行封装的,主要对其获取expression进行了缓存,其代码如下
public final class OgnlCache {
/**
* ognl访问
*/
private static final OgnlMemberAccess MEMBER_ACCESS = new OgnlMemberAccess();
/**
* ognl的类加载器 内部封装了ibatis的Resource
*/
private static final OgnlClassResolver CLASS_RESOLVER = new OgnlClassResolver();
/**
* 缓存结果 内部缓存了
*/
private static final Map expressionCache = new ConcurrentHashMap<>();
private OgnlCache() {
// Prevent Instantiation of Static Class
}
/**
* @param expression 表达式
* @param root
* @return
*/
public static Object getValue(String expression, Object root) {
try {
Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null);
return Ognl.getValue(parseExpression(expression), context, root);
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
/**
* 根据表达式
*
* @param expression
* @return
* @throws OgnlException
*/
private static Object parseExpression(String expression) throws OgnlException {
Object node = expressionCache.get(expression);
if (node == null) {
node = Ognl.parseExpression(expression);
expressionCache.put(expression, node);
}
return node;
}