概述
根据上一篇文章,我们了解了 Mybatis 如何在加载配置文件后,根据指定配置方式寻找接口并且完成映射文件信息与接口的绑定的。在本篇文章,我们将延续上文,结合源码阐述映射文件中方法声明里的 sql 被解析为 java 对象的过程。
1.解析XML文件
1.1.XMLMapperBuilder
Sql 解析与 MappedStatement
的生成都在 XMLMapperBuilder
进行。根据上文可知,在 XMLMapperBuilder
的 parsePendingStatements()
方法如下:
private void parsePendingStatements() {
// 获取所有映射文件中的方法声明
Collection<XMLStatementBuilder> incompleteStatements = configuration.getIncompleteStatements();
synchronized (incompleteStatements) {
Iterator<XMLStatementBuilder> iter = incompleteStatements.iterator();
while (iter.hasNext()) {
try {
// 遍历并转换为Statement对象
iter.next().parseStatementNode();
iter.remove();
} catch (IncompleteElementException e) {
// Statement is still missing a resource...
}
}
}
}
其中,Configuration
类中的IncompleteStatements
是在XMLMapperBuilder.parse()
时添加进去的,我们可以理解他是一个刚从配置文件中根据<select><delete><update><insert>
标签名拿到的 XML 节点,还没有做任何解析。
等到完成了接口与映射文件信息的绑定以后,再遍映射文件中的方法声明依次解析。
1.2.parseStatementNode
这个方法比较长,但是主要作用就是解析一个方法声明中的属性与子标签:
public void parseStatementNode() {
// 获取id属性
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 获取标签节点名称
String nodeName = context.getNode().getNodeName();
// 判断sql类型
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 如果是select就判断是否需要获取/清空缓存
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// 将include节点转为相应的sql节点
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 获取入参类型
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
// 获取lang
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// 获取selectKey节点,解析完成后删除
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 解析sql
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
KeyGenerator keyGenerator; // 生成主键
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
// 如果设置了selectKey,则在插入获取主键生成器生成的主键
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 创建对应的SqlSource对象
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// 获取Statement类型,默认为prepared
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
// 获取一些连接参数
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
// 入参和返回值类型
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
// 构建MappedStatement并添加到配置类
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
1.3.SqlSource的构建
在上文中有一个 parseStatementNode()
方法,暂且忽略缓存与各种返回值类型的处理,我们关注一下addMappedStatement()
中传入的变量 sqlSource
,它本身是一个接口,跟DataSource
的功能一样,通过SqlSource
接口的实现类,可以获取 sql 对象:
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}
他具有的唯一一个抽象方法即为getBoundSql()
,这个方法获取的 BoundSql
类就是实际上的我们认为的方法声明中的那个 sql 对象:
public class BoundSql {
private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Object parameterObject;
private final Map<String, Object> additionalParameters;
private final MetaObject metaParameters;
}
里面包含有带占位符和动态标签的原始 sql 语句,以及方法声明上相关的入参。
2.SQL脚本的解析
2.1.LanguageDriver
LanguageDriver 本身也是一个接口,他的实现类用于解析参数以及注解和映射文件中的 sql,并最终根据此生成 sql 数据源,我们可以简单的理解为针对指定格式 sql 语句的解析器:
public interface LanguageDriver {
/**
* 创建一个参数处理器,将处理完参数后得到实际参数传递给JDBC语句。
*/
ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
/**
* 解析XML并创建SqlSource
*/
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
/**
* 解析注解并创建SqlSource
*/
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}
SqlSource
通过 LanguageDriver.createSqlSource()
创建:
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
... ...
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
而这里的getLanguageDriver()
方法,最终会回到Configuration.getLanguageDriver()
中:
if (langClass == null) {
return languageRegistry.getDefaultDriver();
}
languageRegistry.register(langClass);
return languageRegistry.getDriver(langClass);
而映射文件里方法声明中 lang
属性我们一般不会刻意设置,按上述逻辑,获取的是默认的语言驱动,这里的语言驱动来自于 Configuration
初始化时候注册的 XMLLanguageDriver
和 RawLanguageDriver
:
public Configuration() {
... ...
languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
languageRegistry.register(RawLanguageDriver.class);
}
2.2.动态SQL与静态SQL
XMLLanguageDriver
是LanguageDriver
接口的实现,它用于解析 xml 格式的 sql 语句——或者更准确点说,是 mybatis 特定的方法声明语法。
RawLanguageDriver
继承了XMLLanguageDriver
,而XMLLanguageDriver
又实现了LanguageDriver
接口:
public class XMLLanguageDriver implements LanguageDriver {
// 创建参数处理器
@Override
public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
}
// 解析映射文件的sql
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
// 解析有复杂参数的sql
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// issue #3
// 解析使用@Select这类sql标签上的sql
if (script.startsWith("<script>")) {
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
// 先转为xml再按照映射文件的方式解析
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
} else {
// issue #127
// 替换占位符中的变量,转为sql语法节点
script = PropertyParser.parse(script, configuration.getVariables());
TextSqlNode textSqlNode = new TextSqlNode(script);
// 如果含有动态标签
if (textSqlNode.isDynamic()) {
return new DynamicSqlSource(configuration, textSqlNode);
} else {
// 不含动态标签
return new RawSqlSource(configuration, script, parameterType);
}
}
}
}
基于XMLLanguageDriver
,RawLanguageDriver
进一步完善了方法:
public class RawLanguageDriver extends XMLLanguageDriver {
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
SqlSource source = super.createSqlSource(configuration, script, parameterType);
checkIsNotDynamic(source);
return source;
}
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
SqlSource source = super.createSqlSource(configuration, script, parameterType);
checkIsNotDynamic(source);
return source;
}
private void checkIsNotDynamic(SqlSource source) {
if (!RawSqlSource.class.equals(source.getClass())) {
throw new BuilderException("Dynamic content is not allowed when using RAW language");
}
}
}
主要是再做检查,防止非静态的 sql 使用 RawSqlSource。
对于 Myabtis 来说,sql 中带有 ${}
和 <where>
这类动态标签的都认为是动态 sql,反之则是静态 sql。
3.解析Mybatis标签
3.1.XMLScriptBuilder
XMLScriptBuilder
这个类用于对 Mybatis 的映射文件中的方法声明里的动态标签做解析。它的内部有一个 NodeHandle
接口实现类集合,用于处理专门的动态标签:
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());
}
public SqlSource parseScriptNode() {
// 解析动态标签
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
// 是否带有动态标签的非静态sql
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
// 静态sql
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
// 解析方法声明中的节点
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语句
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
// 是带有${}的动态sql
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
// 是静态sql
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
// sql中带有动态标签
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);
}
理论上来说,我们可以通过在此处注册 NodeHandler
来支持自定义的 SQL 语法。
3.2.SqlNode
parseDynamicTags()
最终返回的 MixedSqlNode
是一个 SqlNode 实现类,SqlNode 是一个表示节点的特殊接口:
public interface SqlNode {
boolean apply(DynamicContext context);
}
他的方法及其简单,每个 SqlNode 实现类都会实现apply()
方法,当调用以后,sql 节点会被解析为正常的 sql 语句放入 DynamicContext
上下文中。
所有方法声明中的 sql 节点都实现类这个接口:
简而言之,他们的实现类很多,但是实际上就分四类:
- StaticTextSqlNode:顾名思义,普通的静态 sql 语句部分,只带有
#{}
表达式而不含有动态标签和${}
赋值表达式; - TextSqlNode:带有
${}
赋值表达式的 sql 语句; - 动态标签节点:除了
TextSqlNode
、MixedSqlNode
和StaticTextSqlNode
的所有其他实现类,即处理 Mybatis 动态标签的特殊节点; - MixedSqlNode:如
parseDynamicTags()
所示,这是一个混合了所有类型标签的节点,也是实际上的根节点;
我们以 StaticTextSqlNode
为例:
public class StaticTextSqlNode implements SqlNode {
private final String text;
public StaticTextSqlNode(String text) {
this.text = text;
}
@Override
public boolean apply(DynamicContext context) {
// 拼接sql
context.appendSql(text);
return true;
}
}
普通静态 sql 节点 apply()
以后就把当前的节点内的 sql 拼接到上下文的 sql 中,同理,其他的实现类也差不多。由于节点之间还有嵌套关系,因此有些节点还会自己维护一个独立的上下文,内部处理的时候先把 sql 拼到独立上下文里面,等自己的节点处理完再把独立上下文中的 sql 拼接到父目录的上下文中。
根据各自的逻辑处理后,最终都会把节点代表的 sql 拼接到上下文里,最终所有节点逻辑处理完,就会在上下文中拼接处一条完整的 sql。
而起到这个作用的,就是根节点 MixedSqlNode
:
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;
}
}
MixedSqlNode
内部有所有的 SqlNode
,它的 apply()
方法就是遍历节点然后把每个节点的 apply()
方法都执行一遍。
这里另外提到一点,对于动态标签里的语法,比如 <if test="...">
里的 test,这里的表达式实际上是 Ognl 表达式,这在 jsp 中也有使用,我们可以简单理解为一个脚本语言,通过表达式,我们可以实现一些简单的功能,比如取值或者比较等等。
4.获得可执行SQL
4.1.RawSqlSource与DynamicSqlSource
XMLLanguageDriver.createSqlSource()
最终会根据textSqlNode.isDynamic()
的结果——也就是方法声明的 sql 中是否含有动态标签——区分要创建哪一种 SqlSource
,带${}
和动态标签的用DynamicSqlSource
,不带的用 RawSqlSource
。
我们以比较简单的 RawSqlSource
为例:
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<>());
}
// 从上下文中获取根标签,也就是之前提到的MixedSqlNode
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
DynamicContext context = new DynamicContext(configuration, null);
// 解析处理所有的SqlNode,并拼接到context里
rootSqlNode.apply(context);
// 获取拼接好的sql
return context.getSql();
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 获取最终的BoundSql
return sqlSource.getBoundSql(parameterObject);
}
}
可以看到,getSql()
本质上就是拿到处理好的 sql 节点,然后调用他们的 apply()
方法,最终拼接到上下文中,然后再返回这个拼好的 sql。
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
@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();
// 将#{}表达式替换为?占位符,获取最终的BoundSql
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 添加参数到boundSql中
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}
DynamicSqlSource
和RawSqlSource
没有什么区别,只不过由于存在动态标签与${}
,相比简单的静态 sql,需要先将这两者解析为 sql。
4.2.替换占位符
SqlSourceBuilder.getBoundSql()
的主要作用就是将#{}
占位符替换为 jdbc 中的?
占位符:
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;
// 如果有额外的空格就先删除
if (configuration.isShrinkWhitespacesInSql()) {
// 将#{}表达式替换为具体的参数值
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);
}
// 转为静态StaticSqlSource
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
所有的 sql 最后都会完成解析,变为最终的自带?占位符的静态 sql 字符串,sql 与对应的入参、返回值类型等信息最终被封装为一个对象,这个对象就是 BoundSql
:
public class BoundSql {
// sql字符串
private final String sql;
// 与占位符对应的入参信息
private final List<ParameterMapping> parameterMappings;
// 参数
private final Object parameterObject;
// 附加参数
private final Map<String, Object> additionalParameters;
private final MetaObject metaParameters;
}
里面包含有带占位符和动态标签的原始 sql 语句,以及方法声明上相关的入参。
然后我们再回头看看DynamicSqlSource
,我们知道由于存在动态标签,因此DynamicSqlSource
会在创建了StaticSqlSource
之后,通过StaticSqlSource
拿到仍然带有动态标签数据的 BoundSql
,接着在根据入参处理动态标签,最终再生成一个 BoundSql
。
总结
我们总结一下 Mapper 中方法声明被变成 sql 加载进 Mybatis 的过程:
SQL 的加载
- 方法声明的解析发生在一个 Mapper 文件被加载——也就是
XMLMapperBuilder
构建——的时候,此时各个 statement 都还只是未被解析的 XML 节点; - 在
XMLMapperBuilder
中,先通过parsePendingStatements()
遍历节点,然后依次调用节点的parseStatementNode()
方法,在这一步主要一下几件事:
- 获取 id、超时时间与缓存等相关的配置信息;
- 获取方法入参与返回值类型;
- 解析
<include>
节点,并替换为相应的内容; - 解析
<selectKey>
节点,根据配置设置相应的主键生成策略,完成后然后删除节点; - 解析sql,根据配置的
LanguageDriver
将 sql 解析为对应的SqlSource
; - 获取
StatementTyoe
,默认都为prepared
;
然后根据以上的信息构建一个 MappedStatement
对象,这个 MappedStatement
即是后续方法实现的基础。
SqlSource 的构建
然后回头再看看步骤二,步骤二干了很多事,但是最重要的在于解析 statement 语法生成 SqlSource
:
- 获取
LanguageDriver
接口实现类,该实现类为来在配置文件加载时 Mybatis 默认提供的XMLLanguageDriver
,主要用来解析 Myabtis 特有的带有动态标签与参数标签的语法; - 在
LanguageDriver
中创建XMLScriptBuilder
用于解析 statement 中的语法,这里如果是类似@Select
这样注解形式的sql,就先解析为 xml 再用XMLScriptBuilder
; - 在
XMLScriptBuilder
中将 statement 语法也解析为一串的 XML 节点,并根据节点对应的语法做了区分:
- 如果是带有
${}
表达式的字符串节点被认为是动态 sql 节点,转为TextSqlNode
, - 如果是只带有
#{}
表达式的字符串节点或者干脆就是纯字符串的字符串节点,转为StaticTextSqlNode
, - 如果是动态标签,就通过节点解析器
NodeHandler
处理,最后转为类似WhereSqlNode
这样的动态标签节点;
- 还是在
XMLScriptBuilder
中,解析出来的各种 Node 都是SqlNode
接口的实现类,他们都有apply()
方法,当调用的时候,会把自身对应的数据解析为 sql 并拼接到一个上下文对象的 sql 字符串中。 最终这些节点都被添加到一个集合中,最终放入一个根节点MixedSqlNode
里,当调用MixedSqlNode
的apply()
方法时,就会调用所有节点的apply()
方法民,最终就会得到一个完整的 sql。 - 回到
LanguageDriver
中,在它的createSqlSource()
方法中,根据是否带有${}
与动态标签区分为DynamicSqlSource
与RawSqlSource
两种DataSource
。
RawSqlSource
:调用MixedSqlNode.apply()
拿到完整的 sql,然后把#{}
表达式替换为?
占位符,接着根据 sql 创建一个StaticSqlSource
,然后通过StaticSqlSource
获取boundSql
。DynamicSqlSource
:跟RawSqlSource
一样,先获得完整的 sql,并生成一个StaticSqlSource
对象,然后通过StaticSqlSource
拿到boundSql
,接着再根据传入的参数处理BoundSql
中的动态标签,最终再生成一个BoundSql
。