我们在以前的文章中曾经介绍过 OGNL 强大的表达式引擎
我们知道在 BaseExecutor#query 中
@Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); } 复制代码
我们会通过 MappedStatement 获取解释之后的 Sql
public BoundSql getBoundSql(Object parameterObject) { BoundSql boundSql = sqlSource.getBoundSql(parameterObject); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings == null || parameterMappings.isEmpty()) { boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject); } // check for nested result maps in parameter mappings (issue #30) for (ParameterMapping pm : boundSql.getParameterMappings()) { String rmId = pm.getResultMapId(); if (rmId != null) { ResultMap rm = configuration.getResultMap(rmId); if (rm != null) { hasNestedResultMaps |= rm.hasNestedResultMaps(); } } } return boundSql; } 复制代码
Mybatis 中存在两种 SqlSource
public SqlSource parseScriptNode() { MixedSqlNode rootSqlNode = parseDynamicTags(context); SqlSource sqlSource; if (isDynamic) { sqlSource = new DynamicSqlSource(configuration, rootSqlNode); } else { sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); } return sqlSource; } 复制代码
一种是动态 Sql 一种是静态 Sql
对于静态 Sql、不需要解释和处理
// StaticSqlSource @Override public BoundSql getBoundSql(Object parameterObject) { return new BoundSql(configuration, sql, parameterMappings, parameterObject); } 复制代码
动态 Sql
@Override public BoundSql getBoundSql(Object parameterObject) { DynamicContext context = new DynamicContext(configuration, parameterObject); rootSqlNode.apply(context); SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); BoundSql boundSql = sqlSource.getBoundSql(parameterObject); context.getBindings().forEach(boundSql::setAdditionalParameter); return boundSql; } 复制代码
DynamicContext
public class DynamicContext { public static final String PARAMETER_OBJECT_KEY = "_parameter"; public static final String DATABASE_ID_KEY = "_databaseId"; static { OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor()); } private final ContextMap bindings; private final StringJoiner sqlBuilder = new StringJoiner(" "); private int uniqueNumber = 0; ...... 复制代码
主要存在两个成员变量 sqlBuilder 的类型为 StringJoiner 、JDK 类不再介绍
主要看看 bindings
static class ContextMap extends HashMap<String, Object> { private static final long serialVersionUID = 2977601501966151582L; private final MetaObject parameterMetaObject; private final boolean fallbackParameterObject; public ContextMap(MetaObject parameterMetaObject, boolean fallbackParameterObject) { this.parameterMetaObject = parameterMetaObject; this.fallbackParameterObject = fallbackParameterObject; } @Override public Object get(Object key) { String strKey = (String) key; if (super.containsKey(strKey)) { return super.get(strKey); } if (parameterMetaObject == null) { return null; } if (fallbackParameterObject && !parameterMetaObject.hasGetter(strKey)) { return parameterMetaObject.getOriginalObject(); } else { // issue #61 do not modify the context when reading return parameterMetaObject.getValue(strKey); } } } 复制代码
是 HashMap 、重写了 get 方法。
- 首先,尝试按照 Map 的规则查找 Key,如果查找成功直接返回;
- 如果 parameterMetaObject 为 null 直接返回 null
- 如果 parameterMetaObject 不为 null 、并且 fallbackParameterObject 为 false 或者 parameterMetaObject 有这个属性的 get 方法、那么就调用 getValue 尝试获取值
- 如果 parameterMetaObject 不为 null、并且 fallbackParameterObject 为 true (其实就是查询的参数的类型存在类型转换器)、并且不存在 get 方法、那么就直接返回这个查询参数
创建 ContextMap 在 DynamicContext 构造函数中
public DynamicContext(Configuration configuration, Object parameterObject) { if (parameterObject != null && !(parameterObject instanceof Map)) { MetaObject metaObject = configuration.newMetaObject(parameterObject); boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass()); bindings = new ContextMap(metaObject, existsTypeHandler); } else { bindings = new ContextMap(null, false); } // 这里实参对应的Key是_parameter bindings.put(PARAMETER_OBJECT_KEY, parameterObject); bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId()); } 复制代码
- 对于非Map类型的实参,会创建对应的MetaObject对象,并封装成ContextMap对象
- 对于Map类型的实参,这里会创建一个空的ContextMap对象
SqlNode
创建玩 DynamicContext 之后、就会调用 SqlNode 的 apply 方法
rootSqlNode.apply(context); 复制代码
public interface SqlNode { boolean apply(DynamicContext context); } 复制代码
apply 方法会根据用户传入的参数、解析 SqlNode 所表示的动态 SQL 内容并将解释之后的 Sql 片段追加到 Dynamic 中的 StringJoiner 类型的变量上、当 SQL 中全部动态片段都解释完成之后、就可以从 DynamicContext 中 StringJoiner 中获取到一条完整的 SQL
StaticTextSqlNode
用于表示非动态的 SQL 片段、啥都不用干、直接将对应的 Sql 字符串拼接到 context 中保存
public class StaticTextSqlNode implements SqlNode { private final String text; public StaticTextSqlNode(String text) { this.text = text; } @Override public boolean apply(DynamicContext context) { context.appendSql(text); return true; } } 复制代码
MixedSqlNode
在整个 SqlNode 中充当了树枝节点、主要作用就是组织聚合其他 SqlNode。
public class MixedSqlNode implements SqlNode { private final List<SqlNode> contents; public MixedSqlNode(List<SqlNode> contents) { this.contents = contents; } @Override public boolean apply(DynamicContext context) { contents.forEach(node -> node.apply(context)); return true; } } 复制代码
apply 方法直接遍历集合中的 SqlNode 的 apply 方法。
TextSqlNode
包含了 占位符的动态SQL片段、在apply方法中集合用户给定的参数解释{} 占位符的动态 SQL 片段、在 apply 方法中集合用户给定的参数解释 占位符的动态SQL片段、在apply方法中集合用户给定的参数解释{} 占位符
@Override public boolean apply(DynamicContext context) { GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter)); context.appendSql(parser.parse(text)); return true; } 复制代码
使用 GenericTokenParser 识别 ${} 占位符、在识别到占位符之后、会通过 BindingTokenParser 将占位符替换为用户传入的参数。
private GenericTokenParser createParser(TokenHandler handler) { return new GenericTokenParser("${", "}", handler); } 复制代码
public String parse(String text) { if (text == null || text.isEmpty()) { return ""; } // search open token int start = text.indexOf(openToken); if (start == -1) { return text; } char[] src = text.toCharArray(); int offset = 0; final StringBuilder builder = new StringBuilder(); StringBuilder expression = null; do { 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(); int end = text.indexOf(closeToken, offset); while (end > -1) { 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 { builder.append(handler.handleToken(expression.toString())); offset = end + closeToken.length(); } } start = text.indexOf(openToken, offset); } while (start > -1); if (offset < src.length) { builder.append(src, offset, src.length - offset); } return builder.toString(); } 复制代码
parse 方法会调用 handleToken 方法
private static class BindingTokenParser implements TokenHandler { 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); } 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()); } } } 复制代码
IfSqlNode
对应的是 <if>
标签、在 Mybatis 中、使用 <if>
标签可以通过 test 属性置顶一个表达式、当表达式成立时、<if>
标签内的 SQL 片段才会出现在完整的 SQL 语句中
public class IfSqlNode implements SqlNode { private final ExpressionEvaluator evaluator; private final String test; private final SqlNode contents; public IfSqlNode(SqlNode contents, String test) { this.test = test; this.contents = contents; this.evaluator = new ExpressionEvaluator(); } @Override public boolean apply(DynamicContext context) { if (evaluator.evaluateBoolean(test, context.getBindings())) { contents.apply(context); return true; } return false; } } 复制代码
在 IfSqlNode 中的 apply 方法实现中、会依赖 ExpressionEvaluator 工具类解释 test 表达式、只有 test 表达式为 true 、才会调用子 SqlNode 的 apply 方法。ExpressionEvaluator 底层也是依赖 OGNL 实现 test 表达式解释的。
TrimSqlNode
在使用 <trim>
标签的时候、我们可以指定 prefix 和 suffix 属性添加前缀和后缀、也可以指定 prefixesToOverride 和 suffixesToOverride 属性来删除多个前缀和后缀(使用|来分割不同字符串)
public class TrimSqlNode implements SqlNode { private final SqlNode contents; private final String prefix; private final String suffix; private final List<String> prefixesToOverride; private final List<String> suffixesToOverride; private final Configuration configuration; public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) { this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride)); } protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) { this.contents = contents; this.prefix = prefix; this.prefixesToOverride = prefixesToOverride; this.suffix = suffix; this.suffixesToOverride = suffixesToOverride; this.configuration = configuration; } @Override public boolean apply(DynamicContext context) { FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context); // 首先执行子SqlNode对象的apply()方法完成对应动态SQL片段的解析 boolean result = contents.apply(filteredDynamicContext); // 使用FilteredDynamicContext.applyAll()方法完成前后缀的处理操作 filteredDynamicContext.applyAll(); return result; } private static List<String> parseOverrides(String overrides) { if (overrides != null) { final StringTokenizer parser = new StringTokenizer(overrides, "|", false); final List<String> list = new ArrayList<>(parser.countTokens()); while (parser.hasMoreTokens()) { list.add(parser.nextToken().toUpperCase(Locale.ENGLISH)); } return list; } return Collections.emptyList(); } ....... 复制代码
FilteredDynamicContext 可以看作是 DynamicContext 的装饰器、额外添加了处理前缀和后缀的功能
public void applyAll() { sqlBuffer = new StringBuilder(sqlBuffer.toString().trim()); String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH); if (trimmedUppercaseSql.length() > 0) { applyPrefix(sqlBuffer, trimmedUppercaseSql); applySuffix(sqlBuffer, trimmedUppercaseSql); } delegate.appendSql(sqlBuffer.toString()); } 复制代码
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) { if (!prefixApplied) { prefixApplied = true; if (prefixesToOverride != null) { for (String toRemove : prefixesToOverride) { if (trimmedUppercaseSql.startsWith(toRemove)) { sql.delete(0, toRemove.trim().length()); break; } } } if (prefix != null) { sql.insert(0, " "); sql.insert(0, prefix); } } } private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) { if (!suffixApplied) { suffixApplied = true; if (suffixesToOverride != null) { for (String toRemove : suffixesToOverride) { if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) { int start = sql.length() - toRemove.trim().length(); int end = sql.length(); sql.delete(start, end); break; } } } if (suffix != null) { sql.append(" "); sql.append(suffix); } } } 复制代码
- applyPrefix() 方法在处理前缀的时候,首先会遍历 prefixesToOverride 集合,从 SQL 片段的头部逐个尝试进行删除,之后在 SQL 片段的头部插入一个空格以及 prefix 字段指定的前缀字符串。
- applySuffix() 方法在处理后缀的时候,首先会遍历 suffixesToOverride 集合,从 SQL 片段的尾部逐个尝试进行删除,之后在 SQL 片段的尾部插入一个空格以及 suffix 字段指定的后缀字符串。
WhereSqlNode
public class WhereSqlNode extends TrimSqlNode { private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t"); public WhereSqlNode(Configuration configuration, SqlNode contents) { super(configuration, contents, "WHERE", prefixList, null, null); } } 复制代码
作为 TrimSqlNode 的子类、在 WhereSqlNode 中将 prefix 设置为“WHERE”字符串,prefixesToOverride 集合包含 “OR”“AND”“OR\n”“AND\n”“OR\r”“AND\r” 等字符串,这样就实现了删除 SQL 片段开头多余的 “AND”“OR” 关键字,并添加“WHERE”关键字的效果。
SetSqlNode
public class SetSqlNode extends TrimSqlNode { private static final List<String> COMMA = Collections.singletonList(","); public SetSqlNode(Configuration configuration,SqlNode contents) { super(configuration, contents, "SET", COMMA, null, COMMA); } } 复制代码
在 SetSqlNode 中将 prefix 设置为“SET”关键字,prefixesToOverride 集合和 suffixesToOverride 集合只包含“,”(逗号)字符串,这样就实现了删除 SQL 片段开头和结尾多余的逗号,并添加“SET”关键字的效果。
ForEachSqlNode
在动态 SQL 语句中、我们可以使用 标签对一个集合进行迭代。我们可以通过 index 属性指定元素的下标索引(迭代 Map 集合的话、就是 key 值)、使用 item 属性置顶变量作为集合元素(迭代 Map 集合的话、就是 Value 值)。另外我们还可以通过 open 和 close 属性在迭代开始前和结束后添加相应的字符串、也允许使用 separator 属性自定义分隔符。
public class ForEachSqlNode implements SqlNode { public static final String ITEM_PREFIX = "__frch_"; private final ExpressionEvaluator evaluator; private final String collectionExpression; private final SqlNode contents; private final String open; private final String close; private final String separator; private final String item; private final String index; private final Configuration configuration; public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) { this.evaluator = new ExpressionEvaluator(); this.collectionExpression = collectionExpression; this.contents = contents; this.open = open; this.close = close; this.separator = separator; this.index = index; this.item = item; this.configuration = configuration; } ........ 复制代码
ChooseSqlNode
在有的业务场景中,可能会碰到非常多的分支判断,在 Java 中,我们可以通过 switch...case...default 的方式来编写这段代码;在 MyBatis 的动态 SQL 语句中,我们可以使用 、 和 三个标签来实现类似的效果。
标签会被 MyBatis 解析成 ChooseSqlNode 对象, 标签会被解析成 IfSqlNode 对象, 标签会被解析成 MixedSqlNode 对象。
public class ChooseSqlNode implements SqlNode { private final SqlNode defaultSqlNode; private final List<SqlNode> ifSqlNodes; public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) { this.ifSqlNodes = ifSqlNodes; this.defaultSqlNode = defaultSqlNode; } @Override public boolean apply(DynamicContext context) { for (SqlNode sqlNode : ifSqlNodes) { if (sqlNode.apply(context)) { return true; } } if (defaultSqlNode != null) { defaultSqlNode.apply(context); return true; } return false; } } 复制代码
VarDeclSqlNode
这个不太常用、
VarDeclSqlNode 抽象了 标签,其核心功能是将一个 OGNL 表达式的值绑定到一个指定的变量名上,并记录到 DynamicContext 上下文中。
VarDeclSqlNode 中的 name 字段维护了 标签中 name 属性的值,expression 字段记录了 标签中 value 属性的值(一般是一个 OGNL 表达式)。
在 apply() 方法中,VarDeclSqlNode 首先会通过 OGNL 工具类解析 expression 这个表达式的值,然后将解析结果与 name 字段的值一起绑定到 DynamicContext 上下文中,这样后面就可以通过 name 字段值获取这个表达式的值了。
public class VarDeclSqlNode implements SqlNode { private final String name; private final String expression; public VarDeclSqlNode(String var, String exp) { name = var; expression = exp; } @Override public boolean apply(DynamicContext context) { final Object value = OgnlCache.getValue(expression, context.getBindings()); context.bind(name, value); return true; } }