Mybatis源代码中SqlSource描述XML文件或者Java注解配置的SQL信息,SqlNode描述动态SQL配置中的<if>和<where>等标签,LanguageDriver的职责就是负责将Mapper SQL配置进行解析,然后将SQL配置信息转换为SqlSource对象。从而可见LanguageDriver是解析SQL信息配置关键所在,即触发解析SQL信息配置的源头。
总览
在Mybatis源代码中SQL配置解析过程:
- 通过LanguageDriver接口定义的createSqlSource方法来触发解析通过XML配置的SQL信息和通过Java注解配置的SQL信息;
- XMLLanguageDriver#createSqlSource方法,首先创建XMLScriptBuilder对象,然后调用XMLScriptBuilder对象的parseScriptNode方法和parseDynamicTags方法将SQL信息转换为DynamicSqlSource对象或者RawSqlSource对象;
- 通过SqlSource接口定义的getBoundSql方法生成其对应的BoundSql。
LanguageDriver
关于LanguageDriver的详细介绍可见Mybatis—LanguageDriver,这里以针对处理XML SQL信息配置的XMLLanguageDriver实现类为例,LanguageDriver接口定义了两个createSqlSource方法,这两个方法唯一的区别就在于入参的第二个参数,一个接口定义所需要的XNode类型的script,另一个接口定义则需要String类型的script。Mybatis之所以这么设计,是因为一个方法是用于处理通过XML配置的SQL信息,另一个方法则用于处理通过Java注解配置的SQL信息。
public interface LanguageDriver {
/**
* 用于处理通过XML配置的SQL信息
*/
SqlSource createSqlSource(
Configuration configuration,
XNode script, Class<?> parameterType);
/**
* 用于处理通过Java注解配置的SQL信息
*/
SqlSource createSqlSource(
Configuration configuration,
String script, Class<?> parameterType);
}
从XMLLanguageDriver源代码来看用于处理Java注解配置的SQL信息有两个处理分支,一是处理script文本中有<script>标签的SQL配置信息,二是处理不带<script>标签的SQL配置信息。当处理带有<script>标签的SQL配置信息时,会直接调用用于处理通过XML配置的SQL信息的createSqlSource方法。由此可见用于处理通过XML配置的SQL信息的createSqlSource方法是解析SQL配置信息的关键。
public class XMLLanguageDriver implements LanguageDriver {
@Override
public SqlSource createSqlSource(
Configuration configuration,
XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(
configuration, script, parameterType);
return builder.parseScriptNode();
}
}
由上述代码所示,用于处理通过XML配置的SQL信息的createSqlSource方法内部是委托给XMLScriptBuilder类来完成的,该方法的处理逻辑:
- 通过所传入的参数构建一个用于处理XML配置的XMLScriptBuilder对象;
- 调用XMLScriptBuilder#parseScriptNode方法将SQL信息转换为SqlSource对象。
XMLScriptBuilder
XMLScriptBuilder从命名上来看该类的主要职责就是解析XML中配置的SQL信息,从源代码上来看XMLScriptBuilder继承自BaseBuilder,而BaseBuilder类中实现了关于configuration、typeAliasRegistry和typeHandlerRegistry的一些基础逻辑。
XMLLanguageDriver#createSqlSource方法先是通过XMLScriptBuilder的构造函数来构建了一个XMLScriptBuilder对象,然后调用XMLScriptBuilder#parseScriptNode方法将SQL信息转换为SqlSource对象。
构造函数
XMLScriptBuilder虽然提供了两个构造函数,但是最终都会调用下述代码块中的构造函数,其逻辑如下所示:
- 调用父类BaseBuilder的构造函数,给configuration、typeAliasRegistry和typeHandlerRegistry属性赋值;
- 给context和parameterType属性赋值;
- 调用initNodeHandlerMap方法初始化各个用于处理SQL配置中信息的NodeHandler,如处理<if>标签的IfHandler、<foreach>标签的ForEachHandler和<set>标签的SetHandler。
public XMLScriptBuilder(
Configuration configuration,
XNode context, Class<?> parameterType) {
super(configuration);
this.context = context;
this.parameterType = parameterType;
initNodeHandlerMap();
}
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}
NodeHandler
用于处理各种SQL配置标签的NodeHandler接口是XMLScriptBuilder类中一个私有的接口定义,并在其中实现了用于处理各种SQL配置标签的实现类,结构如下图所示
从源代码来看Mybatis一共提供了TrimHandler、WhereHandler、SetHandler、ForEachHandler、IfHandler、ChooseHandler、OtherwiseHandler和BindHandler八个实现类,且每个实现类都只专注于处理一种SQL标签,如IfHandler处理<if>标签将其转换为IfSqlNode,ForEachHandler处理<foreach>标签将其转换为ForEachSqlNode等等,SqlNode可见Mybatis—SqlNode。
private interface NodeHandler {
void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
}
private class IfHandler implements NodeHandler {
public IfHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
// 解析当前节点中的子节点
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
// 获取<if>标签中的test属性
String test = nodeToHandle.getStringAttribute("test");
// 通过IfSqlNode构造函数创建其对象
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
// 将IfSqlNode放入SqlNode集合中
targetContents.add(ifSqlNode);
}
}
各个NodeHandler的处理逻辑大同小异都是将XNode对象转换为对应的SqlNode,然后将其放入SqlNode集合targetContents中,这里以IfHandler为例:
- 调用XMLScriptBuilder#parseDynamicTags方法解析标签中的子节点,并将其转换为MixedSqlNode对象;
- 获取<if>标签中test属性对应的OGNL表达式,如<if test=“name != null”></if>定义中的“name != null”;
- 通过IfSqlNode构造函数创建其对象,然后将该对象放入SqlNode集合targetContents中。
XMLScriptBuilder#parseScriptNode
从上述分析XMLLanguageDriver源代码可知,通过XMLScriptBuilder构造函数创建好XMLScriptBuilder对象之后,紧接着就会调用XMLScriptBuilder#parseScriptNode方法来解析SQL配置并其将解析结果转换为SqlSource。
public SqlSource parseScriptNode() {
// 解析动态SQL配置信息转换为MixedSqlNode
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
// 如果为动态SQL,创建DynamicSqlSource对象
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
// 如果不是动态SQL,创建RawSqlSource对象
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
XMLScriptBuilder#parseScriptNode方法,首先调用parseDynamicTags方法将SQL配置转换为MixedSqlNode对象,然后判断当前SQL配置是否是动态SQL,如果是动态SQL,就创建DynamicSqlSource对象,反之创建RawSqlSource对象。
XMLScriptBuilder#parseDynamicTags
XMLScriptBuilder#parseScriptNode方法通过调用parseDynamicTags方法将SQL配置转换为MixedSqlNode对象,而parseDynamicTags是XMLScriptBuilder的一个protected方法。其处理逻辑如下:
- 创建一个名为contents的变量用来表示一组SqlNode对象,紧接着遍历当前XNode节点下所有的子节点;
- 通过XNode的NodeType属性来判断当前子节点的类型,如果子节点是单纯的SQL文本内容,获取当前SQL内容,并将其转换为TextSqlNode对象。
- 如果当前SQL内容中包含${}占位符,就说明是动态SQL配置。先将TextSqlNode对象放入contents数组中,然后将isDynamic设置为true。
- 如果当前SQL内容中不包含${}占位符,就用StaticTextSqlNode来表述其内容,再将其放入contents数组中。
- 通过XNode的NodeType属性来判断当前子节点的类型,如果子节点包含<if>、<where>等标签,就使用之前在构造函数中通过initNodeHandlerMap方法初始化的NodeHandler来处理子节点SQL配置。
- 使用contents数组构造MixedSqlNode对象。
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
// 获取当前XNode节点下所有的子节点
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE
|| child.getNode().getNodeType() == Node.TEXT_NODE) {
// 子节点是SQL文本内容
// 获取当前子节点中SQL内容
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) {
// 当前SQL内容中包含${}占位符,当做动态SQL处理
contents.add(textSqlNode);
isDynamic = true;
} else {
// 当前SQL内容中不包含${}占位符
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
// 当前子节点包含<if>、<where>等标签,使用对应的NodeHandler处理
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <"
+ nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
SqlSource
至此整个SQL配置解析过程就已经分析完成,但是要想使用解析后生成的SqlSource对象,就需要通过其接口定义getBoundSql方法来获取对应的BoundSql。BoundSql是对SQL语句及参数信息的封装,是SqlSource解析后的结果。以DynamicSqlSource实现类为例,
- SqlSource接口定义的getBoundSql方法需要传入一个变量名为parameterObject的参数对象;
- 通过configuration配置和参数构造动态SQL上下文;
- 调用SqlNode#apply方法,通常rootSqlNode为MixedSqlNode对象;
- 创建SqlSourceBuilder对象,将参数对象、参数类型和SQL内容交给SqlSourceBuilder#parse方法解析#{}占位符后生成SqlSource对象;
- 调用新生成的SqlSource对象的getBoundSql方法来获取新的BoundSql;
- 遍历新的BoundSql对象,将其中<bind>标签绑定的参数添加到BoundSql对象中。
public class DynamicSqlSource implements SqlSource {
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 通过configuration配置和参数构造动态SQL上下文
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 调用SqlNode#apply方法,通常rootSqlNode为MixedSqlNode对象
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ?
Object.class : parameterObject.getClass();
// 利用SqlSourceBuilder#parse方法,再一次解析当前SQL配置
// 获取通过参数解析#{}占位符后的SqlSource
SqlSource sqlSource = sqlSourceParser.parse(
context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 遍历将<bind>标签绑定的参数添加到BoundSql对象中
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}