核心处理层以基础支持层为基础,实现了MyBatis
的核心功能。这个部分将从MyBatis
的初始化、动态SQL
语句的解析、结果集的映射、参数解析以及SQL
语句的执行等几个方面分析MyBatis
的核心处理层,了解MyBatis
的核心原理。
本篇介绍SqINode&SqISource
映射配置文件中定义的SQL
节点会被解析成MappedStatement
对象,其中的SQL
语句会被解析成SqlSource
对象,SQL
语句中定义的动态SQL
节点、文本节点等,则由SqlNode
接口的相应实现表示。
SqlSource
接口的定义:
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}
|
SqlSource
接口的实现类图:
DynamicSqlSource
负责处理动态SQL
语句,RawSqlSource
负责处理静态语句,两者最终都会将处理后的SQL
语句封装成StaticSqlSource
返回。
DynamicSqlSource
与StaticSqlSource
的主要区别:
StaticSqlSource
中记录的SQL
语句中可能含有?
占位符,但是可以直接提交给数据库执行
DynamicSqlSource
中封装的SQL
语句还需要进行一系列解析,才会最终形成数据库可执行的SQL
语句
组合模式
组合模式是将对象组合成树形结构,以表示部分-整体
的层次结构(一般是树形结构),用户可以像处理一个简单对象一样来处理一个复杂对象,从而使得调用者无须了解复杂元素的内部结构。
组合模式中的各模式如下:
抽象组件(Component
)
Component
接口定义了树形结构中所有类的公共行为,例如这里的operation()
方法。
一般情况下,其中还会定义一些用于管理子组件的方法,例如这里的add()
、remove()
、getChild()
方法。
树叶(Leaf
)
Leaf
在树形结构中表示叶节点对象,叶节点没有子节点。
树枝(Composite
)
定义有子组件的那些组件的行为。该角色用于管理子组件,并通过operation()
方法调用其管理的子组件的相关操作。
调用者(Client
)
通过Component
接口操纵整个树形结构。
组合模式主要有两点好处,首先组合模式可以帮助调用者屏蔽对象的复杂性。
对于调用者来说,使用整个树形结构与使用单个Component
对象没有任何区别,也就是说,调用者并不必关心自己处理的是单个Component
对象还是整个树形结构,这样就可以将调用者与复杂对象进行解耦。
另外,使用了组合模式之后,我们可以通过增加树中节点的方式,添加新的Component对象,从而实现功能上的扩展,这符合开放-封闭
原则,也可以简化日后的维护工作。
组合模式在带来上述好处的同时,也会引入一些问题。
例如,有些场景下程序希望一个组合结构中只能有某些特定的组件,此时就很难直接通过组件类型进行限制(因为都是Component
接口的实现类),这就必须在运行时进行类型检测。而且,在递归程序中定位问题也是一件比较复杂的事情。
MyBatis
在处理动态SQL
节点时,应用到了组合设计模式。MyBatis
会将动态SQL
节点解析成对应的SqlNode
实现,并形成树形结构。
OGNL表达式
OGNL
(Object Graphic Navigation Language,对象图导航语言)表达式在Struts
、MyBatis
等开源项目中有广泛的应用,其中Struts
框架更是将OGNL
作为默认的表达式语言。
在MyBatis
中涉及的OGNL
表达式的功能主要是:存取Java
对象树中的属性、调用Java
对象树中的方法等。
OGNL中的几个概念:
表达式
OGNL
表达式执行的所有操作都是根据表达式解析得到的。
例如:
对象名.方法名
表示调用指定对象的指定方法
@[类的完全限定名]@[静态方法或静态字段]
表示调用指定类的静态方法或访问静态字段
OGNL
表达式还可以完成变量赋值、操作集合等操作。
root对象
OGNL
表达式指定了具体的操作,而root
对象指定了需要操作的对象。
OgnlContext(上下文对象)
OgnlContext
类继承了Map
接口,OgnlContext
对象说白了也就是一个Map
对象。
既然如此,OgnIContext
对象中就可以存放除root
对象之外的其他对象。
在使用OGNL
表达式操作非root
对象时,需要使用#
前缀,而操作root
对象则不需要使用#
前缀。
在MyBatis
中,使用OgnlCache
对原生的OGNL
进行了封装。OGNL
表达式的解析过程是比较耗时的,为了提高效率,OgnlCache
中使用expressionCache
字段(静态成员,ConcurrentHashMap<String,Object>
类型)对解析后的OGNL
表达式进行缓存。
private static final Map<String, Object> expressionCache = new ConcurrentHashMap<String, Object>();
public static Object getValue(String expression, Object root) { try { Map<Object, OgnlClassResolver> context = Ognl.createDefaultContext(root, new OgnlClassResolver()); return Ognl.getValue(parseExpression(expression), context, root); } catch (OgnlException e) { throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e); } }
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; }
|
DynamicContext
DynamicContext
主要用于记录解析动态SQL
语句之后产生的SQL
语句片段,可以认为它是一个用于记录动态SQL
语句解析结果的容器。
其中有两个核心字段:
private final ContextMap bindings;
private final StringBuilder sqlBuilder = new StringBuilder();
|
ContextMap
是DynamicContext
中定义的内部类,它实现了HashMap
并重写了get()
方法。
static class ContextMap extends HashMap<String, Object> { private static final long serialVersionUID = 2977601501966151582L;
private MetaObject parameterMetaObject; public ContextMap(MetaObject parameterMetaObject) { this.parameterMetaObject = parameterMetaObject; }
@Override public Object get(Object key) { String strKey = (String) key; if (super.containsKey(strKey)) { return super.get(strKey); }
if (parameterMetaObject != null) { return parameterMetaObject.getValue(strKey); }
return null; } }
|
DynamicContext
的构造方法会初始化bindings
集合,注意构造方法的第二个参数pammeterObject
,它是运行时用户传入的参数,其中包含了后续用于替换#{}
占位符的实参。
public DynamicContext(Configuration configuration, Object parameterObject) { if (parameterObject != null && !(parameterObject instanceof Map)) { MetaObject metaObject = configuration.newMetaObject(parameterObject); bindings = new ContextMap(metaObject); } else { bindings = new ContextMap(null); } bindings.put(PARAMETER_OBJECT_KEY, parameterObject); bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId()); }
|
SqlNode
接下来看看SqlNode的实现类是如何解析其对应的SQL节点。
public interface SqlNode { boolean apply(DynamicContext context); }
|
SqlNode
接口有多个实现类,每个实现类对应一个动态SQL
节点。按照组合模式的角色来划分,SqlNode
扮演了抽象组件的角色,MixedSqlNode
扮演了树枝节点的角色,TextSqlNode
节点扮演了树叶节点的角色等等。
1、StaticTextSqINode&MixedSqINode
StaticTextSqINode
中使用text
字段(String
类型)记录了对应的非动态SQL
语句节点,其apply()
方法直接将text
字段追加到DynamicContext.sqlBuilder
字段中。
MixedSqINode
中使用contents
字段(List<SqlNode>
类型)记录其子节点对应的SqlNode
对象集合,其apply()
方法会循环调用contents
集合中所有SqlNode
对象的apply()
方法。
2、TextSqlNode
TextSqlNode
表示的是包含占位符的动态SQL
节点。TextSqlNode.apply()
方法会使用GenericTokenParser
解析${}
占位符,并直接替换成用户给定的实际参数值。
@Override public boolean apply(DynamicContext context) { GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter)); context.appendSql(parser.parse(text)); return true; }
private GenericTokenParser createParser(TokenHandler handler) { return new GenericTokenParser("${", "}", handler); }
|
**BindingTokenParser
**是TextSqlNode
中定义的内部类,继承了TokenHandler
接口,它的主要功能是根据DynamicContext.bindings
集合中的信息解析SQL
语句节点中的${}
占位符。BindingTokenParser.context
字段指向了对应的DynamicContext
对象。
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)); 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()); } } }
|
3、IfSqlNode
IfSqlNode
对应的动态SQL
节点是<if>
节点,以下是几个核心字段。
private final ExpressionEvaluator evaluator;
private final String test;
private final SqlNode contents;
|
IfSqlNode.apply()
方法首先会通过ExpressionEvaluator.evaluateBoolean()
方法检测其test
表达
式是否为true
,然后根据test
表达式的结果,决定是否执行其子节点的apply()
方法。
@Override public boolean apply(DynamicContext context) { if (evaluator.evaluateBoolean(test, context.getBindings())) { contents.apply(context); return true; } return false; }
public boolean evaluateBoolean(String expression, Object parameterObject) { Object value = OgnlCache.getValue(expression, parameterObject); if (value instanceof Boolean) { return (Boolean) value; } if (value instanceof Number) { return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0; } return value != null; }
|
4、TrimSqINode&WhereSqINode&SetSqINode
TrimSqlNode
会根据子节点的解析结果,添加或删除相应的前缀或后缀。其中几个字段如下:
private final SqlNode contents;
private final String prefix;
private final String suffix;
private final List<String> prefixesToOverride; private final List<String> suffixesToOverride;
|
在TrimSqlNode
的构造函数中,会调用parseOverrides()
方法对参数prefixesToOverride
(对应<trim>
节点的prefixOverrides
属性)和参数suffixesToOverride
(对应<trim>
节点的suffixOverrides
属性)进行解析,并初始化prefixesToOverride
和sufflxesToOverride
。
private static List<String> parseOverrides(String overrides) { if (overrides != null) { final StringTokenizer parser = new StringTokenizer(overrides, "|", false); final List<String> list = new ArrayList<String>(parser.countTokens()); while (parser.hasMoreTokens()) { list.add(parser.nextToken().toUpperCase(Locale.ENGLISH)); } return list; } return Collections.emptyList(); }
|
TrimSqlNode.apply()
方法首先解析子节点,然后根据子节点的解析结果处理前缀和后缀。
public boolean apply(DynamicContext context) { FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context); boolean result = contents.apply(filteredDynamicContext); filteredDynamicContext.applyAll(); return result; }
|
处理前缀和后缀的主要逻辑是在FilteredDynamicContext
中实现的,它继承了DynamicContext
,同时也是DynamicContext
的代理类。
FilteredDynamicContext
除了将对应方法调用委托给其中封装的DynamicContext
对象,还提供了处理前缀和后缀的applyAll()
方法。
private class FilteredDynamicContext extends DynamicContext { private DynamicContext delegate;
private boolean prefixApplied; private boolean suffixApplied;
private StringBuilder sqlBuffer;
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); } } } }
|
WhereSqlNode
和SetSqlNode
都继承了TrimSqlNode
。
其中WhereSqlNode
指定了prefix
字段为WHERE
,prefixesToOverride
集合中的项为AND
和OR
,suffix
字段和suffixesToOverride
集合为null
。也就是说,<where>
节点解析后的SQL
语句片段如果以AND
或OR
开头,则将开头处的AND
或OR
删除,之后再将WHERE
关键字添加到SQL
片段开始位置,从而得到该<where>
节点最终生成的SQL
片段。
SetSqlNode
指定了prefix
字段为SET
,suffixesToOverride
集合中的项只有suffix
字段和prefixesToOverride
集合为null
。也就是说,<set>
节点解析后的SQL
语句片段如果以,
结尾,则将结尾处的删除掉,之后再将SET
关键字添加到SQL
片段的开始位置,从而得到该<set>
节点最终生成的SQL
片段。
5、ForeachSqINode
在动态SQL
语句中构建IN
条件语句的时候,通常需要对一个集合进行迭代,MyBatis
提供了<foreach>
标签实现该功能。在使用<foreach>
标签迭代集合时,不仅可以使用集合的元素和索引值,还可以在循环开始之前或结束之后添加指定的字符串,也允许在迭代过程中添加指定的分隔符。
解析<foreach>
节点对应的sqlnode
实现类是ForeachSqlNode
,以下是其中定义的字段:
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;
|
ForeachSqINode
中有两个内部类,分别是PrefixedContext
和FilteredDynamicContext
,它们都继承了DynamicContext
,同时也都是DynamicContext
的代理类。
PreFixedContext
private class PrefixedContext extends DynamicContext { private final DynamicContext delegate; private final String prefix; private boolean prefixApplied;
@Override public void appendSql(String sql) { if (!prefixApplied && sql != null && sql.trim().length() > 0) { delegate.appendSql(prefix); prefixApplied = true; } delegate.appendSql(sql); }
}
|
FilteredDynamicContext
FilteredDynamicContext
负责处理#{}
占位符,但它并未完全解析#{}
占位符。
private static class FilteredDynamicContext extends DynamicContext { private final DynamicContext delegate; private final int index; private final String itemIndex; private final String item;
@Override public void appendSql(String sql) { GenericTokenParser parser = new GenericTokenParser("#{", "}", new TokenHandler() { @Override public String handleToken(String content) { String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index)); if (itemIndex != null && newContent.equals(content)) { newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index)); } return new StringBuilder("#{").append(newContent).append("}").toString(); } });
delegate.appendSql(parser.parse(sql)); }
@Override public int getUniqueNumber() { return delegate.getUniqueNumber(); }
}
|
6、ChooseSqlNode
如果在编写动态SQL
语句时需要类似Java
中的switch
语句的功能,可以考虑使用<choose>
、<when>
和<otherwise>
三个标签的组合。MyBatis
会将<choose>
标签解析成ChooseSqlNode
,将<when>
标签解析成IfSqlNode
,将<otherwise>
标签解析成MixedSqlNode
。
ChooseSqlNode.apply()
方法的逻辑比较简单,首先遍历ifSqlNodes
集合并调用其中SqlNode
对象的apply()
方法,然后根据前面的处理结果决定是否调用defaultSqlNode
的apply()
方法。
7、VarDecISqINode
VarDeclSqlNode
表示的是动态SQL
语句中的<bind>
节点,该节点可以从OGNL
表达式中创建一个变量并将其记录到上下文中。
在VarDecISqINode
中通过name
字段记录<bind>
节点的name
属性值,expression
字段记录<bind>
节点的value
属性值。
SqlSourceBuilder
在经过SqlNode.apply()
方法的解析之后,SQL
语句会被传递到SqlSourceBuilder
中进行进一步的解析。
SqISourceBuilder
主要完成了两方面的操作,一方面是解析SQL
语句中的#{}
占位符中定义的属性,格式类似于#{__frc_item_0,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
,另一方面是将SQL
语句中的#{}
占位符替换成?
占位符。
SqlSourceBuilder
也是BaseBuilder
的子类之一,其核心逻辑位于parse()
方法中。
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()); }
|
DynamicSqlSource
DynamicSqlSource
负责解析动态SQL
语句,也是最常用的Sqlource
实现之一。SqlNode
中使用了组合模式,形成了一个树状结构,DynamicSqlSource
中使用rootSqlNode
字段(SqlNode
类型)记录了待解析的SqlNode
树的根节点。
DynamicSqlSource.getBoundSql()
方法:
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); for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) { boundSql.setAdditionalParameter(entry.getKey(), entry.getValue()); } return boundSql; }
|
RawSqISource
RawSqISource
是SqlSource
的另一个实现,其逻辑与DynamicSqlSource
类似,但是执行时机不一样,处理的SQL
语句类型也不一样。
前面介绍XMLScriptBuilder.parseDynamicTags()
方法时提到过,如果节点只包含#{}
占位符,而不包含动态SQL
节点或未解析的${}
占位符的话,则不是动态SQL
语句,会创建相应的StaticTextSqlNode
对象。
在XMLScriptBuilder.parseScriptNode()
方法中会判断整个SQL
节点是否为动态的,如果不是动态的SQL
节点,则创建相应的RawSqlSource
对象。
RawSqlSource
在构造方法中首先会调用getSql()
方法,其中通过调用SqlNode.apply()
方法完成SQL
语句的拼装和初步处理;之后会使用SqlSourceBuilder
完成占位符的替换和ParameterMapping
集合的创建,并返回StaticSqlSource
对象。
public class RawSqlSource implements SqlSource {
private final SqlSource sqlSource;
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) { this(configuration, getSql(configuration, rootSqlNode), parameterType); }
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) { SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class<?> clazz = parameterType == null ? Object.class : parameterType; sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>()); }
private static String getSql(Configuration configuration, SqlNode rootSqlNode) { DynamicContext context = new DynamicContext(configuration, null); rootSqlNode.apply(context); return context.getSql(); }
@Override public BoundSql getBoundSql(Object parameterObject) { return sqlSource.getBoundSql(parameterObject); }
}
|
无论是StaticSqlSource
、DynamicSqlSource
还是RawSqlSource
,最终都会统一生成BoundSql
对象,其中封装了完整的SQL
语句(可能包含?
占位符)、参数映射关系(parameterMappings
集合)以及用户传入的参数(additionalParameters
集合)。
另外,DynamicSqlSource
负责处理动态SQL
语句,RawSqlSource
负责处理静态SQL
语句,除此之外,两者解析SQL
语句的时机也不一样,前者的解析时机是在实际执行SQL
语句之前,而后者则是在MyBatis
初始化时完成SQL
语句的解析。
参考
《MyBatis技术内幕》
部分图片来源——《MyBatis技术内幕》