一文搞懂 Mybatis 究竟是如何解析SQL语句(下)
前言:
上一篇文章(一文搞懂 Mybatis 究竟是如何解析SQL语句(上)) 由于篇幅的问题,将下半部分挪到了本文中,那么本文将会接着上一篇文章进行详细分析 SQL
解析的过程,此文中会伴随着 DEBUG
模式的调试截图,通过图文的方式来描述表达
一、解析 ${} 占位符
紧接着上一篇文章,末尾 textSqlNode.isDynamic()
方法的解析,代码如下:
public boolean isDynamic() {
// 创建一个动态令牌的解析器,该解析器主要是为了标记内部 isDynamic
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
// 然后创建一个通用的令牌解析器,令牌解析器,主要是针对携带 ${} 表示的sql进行参数处理
GenericTokenParser parser = createParser(checker);
// 使用通用 SQL 解析器对占位符进行替换
parser.parse(text);
// 在 DynamicCheckerTokenParser 中,没有调用 tokenHandler 方法,所以我们这里返回的一定是 false
// 这里返回 true 的情况:
// 1. 在创建 GenericTokenParser 时候,里面包含了 ${} 占位符
// 2. 在SQL语句内,包含了动态标签<where> <if> ... 等
return checker.isDynamic();
}
进入 createParser(checker)
方法内,注意,在我们自己的 UserMapper.xml
中并没有 ${}
标识的字符,所以,这里我们直接跳过,因为后续还有地方会调用 parser.parse(text)
方法,就是在解析 #{}
时候。
private GenericTokenParser createParser(TokenHandler handler) {
// 注意,在我们自己的
return new GenericTokenParser("${", "}", handler);
}
二、标记静、动态sql
回到 org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseDynamicTags(org.apache.ibatis.parsing.XNode)
方法内:
// 那么根据分析,这里返回的结果一定是false,因为我们的查询语句并不满足条件
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
// 将解析完毕的SQL包装为静态文本sql语句,然后添加到文本集合中
contents.add(new StaticTextSqlNode(data));
}
继续回到 org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode()
public SqlSource parseScriptNode() {
// 这里调用了上面的代码,得到了 MixedSqlNode 对象
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
// 从刚刚的结果中,不难发现这里一定是 false,不做多余的解释了
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
// 将
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
接下来回到 RawSqlSource 构造方法
public class RawSqlSource implements SqlSource {
private final SqlSource sqlSource;
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
// 这里调用了 getSql 方法,且 this 构造方法第二个参数是String
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
// 如果 crud 标签上没有配置 parameterType,则默认使用 Object
Class<?> clazz = parameterType == null ? Object.class : parameterType;
// 这里就是解析 #{} 的关键代码
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
// 此方法就是提取 SQL 语句的,根据debug,查看到 SQL 语句依然是携带 #{} 占位符的,并不是我们想要的
// 这里有兴趣的可以自己去DEBUG,本文中就不描述了,反正就是把StaticTextSqlNode类的text属性赋值给了
// context 对象
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);
}
}
三、解析 #{}
占位符入口
进入方法 org.apache.ibatis.builder.SqlSourceBuilder#parse()
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
// 很眼熟的代码,终于到了,使用通用的token解析器,依然来解析 #{} 占位符,
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql;
// 这个判断表示是否需要去掉sql内的空格,实际上解析SQL都是调用同一个方法
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
// 进入方法内,
sql = parser.parse(originalSql);
}
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
四、正式开始解析 #{}
接下来进入 parser.parse(text)
方法, 此方法需要 DEBUG
才能看懂其中的内涵,那么我通过画图的形式来阐述吧!
// openToken = #{
// closeToken = }
public String parse(String text) {
if(text == null || text.isEmpty()) {
return "";
}
// 得到 #{ 所在的位置
int start = text.indexOf(openToken);
if (start == -1) {
// 不存在 #{ 的情况直接返回
return text;
}
// 拿到char[]
char[] src = text.toCharArray();
int offset = 0;
// 定义存储原始SQL的对象
final StringBuilder builder = new StringBuilder();
// 需要进行处理的 sql 字段存储
StringBuilder expression = null;
while (start > -1) {
// 防止第一个 #{ 被转义了,转义为 \\#{
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);
}
// 寻找 #{ 所在的位置,将 0 - #{ 之前的所有内容放入到 builder 中
builder.append(src, offset, start - offset);
// #{ 所在的位置+#{ 长度 -> #{ 之后的位置
offset = start + openToken.length();
// 从剩下的内容中 phone} 获取到 } 所在的位置
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 {
// end - offset = }所在的位置 - #{ 之后的位置 例如:#{*phone*} 第一个*表示开始,第二个* 表示结束
// 通过这里就能拿到参数 phone 的名称
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 {
// 这句话就是来处理被截取出来的字符串 phone 的,那么此处的 handler 类型
// 则是 ParameterMappingTokenHandler, 可以通过本文中第三点中最后一个方法找到
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
以上代码处理 select id, user_name, password, address, phone from t_user where phone = #{phone}
大致的一个流程如下图:
五、解析完毕后,参数的处理
在第四点中 builder.append(handler.handleToken(expression.toString()));
是用来处理解析完毕后的参数的,那么我们进入这里,就能够看到在原始 SQL
的参数位置,通过组合一个 ?
来代替原来的参数,那么将原来的参数进行存储,放入到 ParameterMappingTokenHandler
的成员变量 List<ParameterMapping>
内进行存储,最终将方法执行完毕后,在将参数绑定到 StaticSqlSource
对象内(回到第三点
),代码如下:
@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
总结(下)
在还没有正式开始看源码之前,我手撸一个简单版本的 mybatis
,但是我发现我解析SQL
的方式并不是特别的优雅,于是我决定看看 mybatis
到底是如何去解析 XM
得到 SQL
,来解决我的疑问。顺便了解一下 Mybatis
动态 SQL
的解析方式(这些在文中都已经描述到了)。
阅读过 mybatis
源码后,我才发现我和大佬之间的区别,尤其是对字符串的理解。今天没白活。