Mybatis - 一文搞懂 Mybatis 究竟是如何解析SQL语句(下)

一文搞懂 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));
}

标记为静态sql
继续回到 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 ${} #{} 解析时序图
在还没有正式开始看源码之前,我手撸一个简单版本的 mybatis,但是我发现我解析SQL 的方式并不是特别的优雅,于是我决定看看 mybatis 到底是如何去解析 XM 得到 SQL,来解决我的疑问。顺便了解一下 Mybatis 动态 SQL 的解析方式(这些在文中都已经描述到了)。
阅读过 mybatis 源码后,我才发现我和大佬之间的区别,尤其是对字符串的理解。今天没白活。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值