Mybatis SqlNode源码解析
1.ForEachSqlNode
mybatis的foreach标签可以将列表、数组中的元素拼接起来,中间可以指定分隔符separator
  <select id="getByUserId" resultMap="BaseMap">
    select <include refid="BaseFields"></include>
    from user
    <where>
      user_id in
      <foreach collection="userIdList" item="userId" open="(" separator="," close=")">
        #{userId}
      </foreach>
    </where>
  </select>上面这段select sql代码使用了foreach标签,传入了一个userIdList的列表,首先会转化为一个ForeachSqlNode对象,经过处理后foreach标签里面的代码会解析成 (假设userIdList=[101,102,103])
(#{__frch_userId_0}, #{__frch_userId_1},#{__frch_userId_2}) , 后续预处理值替换后就会变成 (101,102,103)
下面是具体的ForEachSqlNode的解析过程源码:
public class ForEachSqlNode implements SqlNode {
  public static final String ITEM_PREFIX = "__frch_";
  // 表达式值获取器
  private final ExpressionEvaluator evaluator;
  // collection userIdList
  private final String collectionExpression;
  private final SqlNode contents;
  // open值 (
  private final String open;
  // close值 )
  private final String close;
  // separator分隔符值 ,
  private final String separator;
  // item值 userId
  private final String item;
  // index值 null
  private final String index;
  // mybatis配置信息
  private final Configuration configuration;
  public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
    this.evaluator = new ExpressionEvaluator();
    this.collectionExpression = collectionExpression;
    this.contents = contents;
    this.open = open;
    this.close = close;
    this.separator = separator;
    this.index = index;
    this.item = item;
    this.configuration = configuration;
  }
  @Override
  public boolean apply(DynamicContext context) {
    Map<String, Object> bindings = context.getBindings();
    // 迭代器
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
    if (!iterable.iterator().hasNext()) {
      return true;
    }
    boolean first = true;
    // 添加open值
    applyOpen(context);
    int i = 0;
    for (Object o : iterable) {
      DynamicContext oldContext = context;
      // 第一次循环first为true,使用new PrefixedContext(context, "")构建context,因为第一个元素之前不用添加分隔符
      // 第一次循环完毕后first为false,使用new PrefixedContext(context, separator)构建,之后先添加分隔符再添加sql值
      if (first || separator == null) {
        context = new PrefixedContext(context, "");
      } else {
        context = new PrefixedContext(context, separator);
      }
      // 每次循环不同值
      int uniqueNumber = context.getUniqueNumber();
      // Issue #709
      if (o instanceof Map.Entry) {
        // map的index为key,item为value
        @SuppressWarnings("unchecked")
        Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
        applyIndex(context, mapEntry.getKey(), uniqueNumber);
        applyItem(context, mapEntry.getValue(), uniqueNumber);
      } else {
        // list的index为序号(从0开始递增),item为value元素值
        // 添加到上下文的bindings这个map中
        // index不为空,key = __frch_index_uniqueNumber的格式,value = i
        applyIndex(context, i, uniqueNumber);
        // item不为空, key = __frch_item_uniqueNumber的格式, value = o
        applyItem(context, o, uniqueNumber);
      }
      // context -> PrefixedContext
      // 处理sql内容
      contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
      if (first) {
        // 是否应用了分隔符,first=false
        first = !((PrefixedContext) context).isPrefixApplied();
      }
      context = oldContext;
      i++;
    }
    // 添加close
    applyClose(context);
    // 移除item和index
    context.getBindings().remove(item);
    context.getBindings().remove(index);
    return true;
  }
  private void applyIndex(DynamicContext context, Object o, int i) {
    if (index != null) {
      context.bind(index, o);
      context.bind(itemizeItem(index, i), o);
    }
  }
  private void applyItem(DynamicContext context, Object o, int i) {
    if (item != null) {
      context.bind(item, o);
      context.bind(itemizeItem(item, i), o);
    }
  }
  private void applyOpen(DynamicContext context) {
    if (open != null) {
      context.appendSql(open);
    }
  }
  private void applyClose(DynamicContext context) {
    if (close != null) {
      context.appendSql(close);
    }
  }
  private static String itemizeItem(String item, int i) {
    return ITEM_PREFIX + item + "_" + i;
  }
  // 动态过滤
  private static class FilteredDynamicContext extends DynamicContext {
    private final DynamicContext delegate;
    private final int index;
    private final String itemIndex;
    private final String item;
    public FilteredDynamicContext(Configuration configuration,DynamicContext delegate, String itemIndex, String item, int i) {
      super(configuration, null);
      this.delegate = delegate;
      // uniqueNumber序号
      this.index = i;
      this.itemIndex = itemIndex;
      this.item = item;
    }
    @Override
    public Map<String, Object> getBindings() {
      return delegate.getBindings();
    }
    @Override
    public void bind(String name, Object value) {
      delegate.bind(name, value);
    }
    @Override
    public String getSql() {
      return delegate.getSql();
    }
    @Override
    public void appendSql(String sql) {
      // 获取 #{}内的内容,之后用replaceFirst将item替换为 __frch__item_0
      // 类似 #{orderId} -> #{__frch_orderId_0}, #{__frch_orderId_1}, #{__frch_orderId_2} 长度取决于集合列表
      GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
        // 开头空格 + item + 后面是.,:或空格的字符串
        String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
        // itemIndex不为空且原字符串和新字符串相同
        if (itemIndex != null && newContent.equals(content)) {
          newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
        }
        return "#{" + newContent + "}";
      });
      delegate.appendSql(parser.parse(sql));
    }
    @Override
    public int getUniqueNumber() {
      return delegate.getUniqueNumber();
    }
  }
  // 前缀填充功能
  private class PrefixedContext extends DynamicContext {
    private final DynamicContext delegate;
    private final String prefix;
    private boolean prefixApplied;
    public PrefixedContext(DynamicContext delegate, String prefix) {
      super(configuration, null);
      this.delegate = delegate;
      this.prefix = prefix;
      this.prefixApplied = false;
    }
    public boolean isPrefixApplied() {
      return prefixApplied;
    }
    @Override
    public Map<String, Object> getBindings() {
      return delegate.getBindings();
    }
    @Override
    public void bind(String name, Object value) {
      delegate.bind(name, value);
    }
    @Override
    public void appendSql(String sql) {
      if (!prefixApplied && sql != null && sql.trim().length() > 0) {
        // 添加分隔符前缀,可以是逗号,等值
        delegate.appendSql(prefix);
        prefixApplied = true;
      }
      // 再添加sql内容
      delegate.appendSql(sql);
    }
    @Override
    public String getSql() {
      return delegate.getSql();
    }
    @Override
    public int getUniqueNumber() {
      return delegate.getUniqueNumber();
    }
  }
}2.TrimSqlNode
mybatis的trim标签可以添加/删除指定的前缀、后缀值
  <select id="getByUserId" resultMap="BaseMap">
    select <include refid="BaseFields"></include>
    from user
    <trim prefix="where" prefixOverrides="and | or">
      and user_id = #{userId}
    </trim>
  </select>这段代码使用了trim标签,会先匹配去除and | or开头的<trim>标签内的sql内容,接着加上前缀where,会解析成
where user_id = #{userId}
下面是具体的ForEachSqlNode的解析过程源码:
public class TrimSqlNode implements SqlNode {
  private final SqlNode contents;
  private final String prefix;
  private final String suffix;
  private final List<String> prefixesToOverride;
  private final List<String> suffixesToOverride;
  private final Configuration configuration;
  public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
    this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
  }
  protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) {
    // 待处理的sql节点
    this.contents = contents;
    // 添加前缀
    this.prefix = prefix;
    // 要去除的前缀
    this.prefixesToOverride = prefixesToOverride;
    // 添加后缀
    this.suffix = suffix;
    // 要去除的后缀
    this.suffixesToOverride = suffixesToOverride;
    // mybatis配置
    this.configuration = configuration;
  }
  @Override
  public boolean apply(DynamicContext context) {
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    // 处理节点 文本添加到sqlBuffer中
    boolean result = contents.apply(filteredDynamicContext);
    filteredDynamicContext.applyAll();
    return result;
  }
  // 解析 prefixOverrides和suffixOverrides多个可用|分割
  private static List<String> parseOverrides(String overrides) {
    if (overrides != null) {
      final StringTokenizer parser = new StringTokenizer(overrides, "|", false);
      final List<String> list = new ArrayList<>(parser.countTokens());
      while (parser.hasMoreTokens()) {
        list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
      }
      return list;
    }
    return Collections.emptyList();
  }
  private class FilteredDynamicContext extends DynamicContext {
    private DynamicContext delegate;
    private boolean prefixApplied;
    private boolean suffixApplied;
    private StringBuilder sqlBuffer;
    public FilteredDynamicContext(DynamicContext delegate) {
      super(configuration, null);
      // 委托原始的context
      this.delegate = delegate;
      this.prefixApplied = false;
      this.suffixApplied = false;
      this.sqlBuffer = new StringBuilder();
    }
    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);
      }
      // 想上下文追加sql内容
      delegate.appendSql(sqlBuffer.toString());
    }
    @Override
    public Map<String, Object> getBindings() {
      return delegate.getBindings();
    }
    @Override
    public void bind(String name, Object value) {
      delegate.bind(name, value);
    }
    @Override
    public int getUniqueNumber() {
      return delegate.getUniqueNumber();
    }
    @Override
    public void appendSql(String sql) {
      sqlBuffer.append(sql);
    }
    @Override
    public String getSql() {
      return delegate.getSql();
    }
    private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
      if (!prefixApplied) {
        prefixApplied = true;
        if (prefixesToOverride != null) {
          for (String toRemove : prefixesToOverride) {
            // 判断开头是否匹配,只可匹配一次后续会直接退出循环
            if (trimmedUppercaseSql.startsWith(toRemove)) {
              // 从头开始删除匹配的字符toRemove长度
              sql.delete(0, toRemove.trim().length());
              break;
            }
          }
        }
        if (prefix != null) {
//          sql.insert(0, prefix + " ");
          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);
        }
      }
    }
  }
}另外其实<where>和<set>标签底层的原理也是和<trim>标签相同的,有继承关系,代码如下
<set>
去除首尾的逗号, 添加前缀SET
public class SetSqlNode extends TrimSqlNode {
  private static final List<String> COMMA = Collections.singletonList(",");
  public SetSqlNode(Configuration configuration,SqlNode contents) {
    super(configuration, contents, "SET", COMMA, null, COMMA);
  }
}
<where>
去除开头的AND/OR值,添加前缀WHERE
public class WhereSqlNode extends TrimSqlNode {
  private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
  public WhereSqlNode(Configuration configuration, SqlNode contents) {
    super(configuration, contents, "WHERE", prefixList, null, null);
  }
}Mybatis SqlNode源码解析的更多相关文章
- MyBatis详细源码解析(上篇)
		前言 我会一步一步带你剖析MyBatis这个经典的半ORM框架的源码! 我是使用Spring Boot + MyBatis的方式进行测试,但并未进行整合,还是使用最原始的方式. 项目结构 导入依赖: ... 
- MyBatis 3源码解析(一)
		一.SqlSessionFactory 对象初始化 //加载全局配置文件 String resource = "mybatis-config.xml"; InputStream i ... 
- Mybatis SqlSessionTemplate 源码解析
		As you may already know, to use MyBatis with Spring you need at least an SqlSessionFactory and at le ... 
- MyBatis 3源码解析(四)
		四.MyBatis 查询实现 Employee empById = mapper.getEmpById(1); 首先会调用MapperProxy的invoke方法 @Override public O ... 
- MyBatis 3源码解析(三)
		三.getMapper获取接口的代理对象 1.先调用DefaultSqlSession的getMapper方法.代码如下: @Override public <T> T getMapper ... 
- MyBatis 3源码解析(二)
		二.获取SqlSession对象 1.首先调用DefaultSqlSessionFactory 的 openSession 方法,代码如下: @Override public SqlSession o ... 
- mybatis源码解析1--前言
		在开始分析mybatis源码之前,需要定一个目标,也就是我们不是为了读源码而去读,一定是带着问题去读,在读的时候去寻找到答案,然后再读码的同时整理总结,学习一些高级的编码方式和技巧. 首先我们知道my ... 
- Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码
		在文章:Mybatis源码解析,一步一步从浅入深(一):创建准备工程,中我们为了解析mybatis源码创建了一个mybatis的简单工程(源码已上传github,链接在文章末尾),并实现了一个查询功能 ... 
- Mybatis源码解析(二) —— 加载 Configuration
		Mybatis源码解析(二) -- 加载 Configuration 正如上文所看到的 Configuration 对象保存了所有Mybatis的配置信息,也就是说mybatis-config. ... 
随机推荐
- Unity实现A*寻路算法学习1.0
			一.A*寻路算法的原理 如果现在地图上存在两点A.B,这里设A为起点,B为目标点(终点) 这里为每一个地图节点定义了三个值 gCost:距离起点的Cost(距离) hCost:距离目标点的Cost(距 ... 
- 漏洞复现:MS12-020 远程桌面协议RDP远程代码执行漏洞
			漏洞复现:MS12-020 远程桌面协议RDP远程代码执行漏洞 攻击机:Kali2019 靶机:Win7 64位 解题步骤: 1.打开Kali2019和Win7 64位 ,确定IP地址是多少 2.确定 ... 
- Oracle中查找某个点半径范围内的所有经纬度(优化)
			需求: 已知一个点的经纬度,需要从表中找出以这个点为中心,半径M米范围内的所有经纬度数据. 假设现有表 TAB_LONG_LAT_DATA,字段如下: ID INTE ... 
- 干货 | 亿级Web系统负载均衡几种实现方式
			一个执着于技术的公众号 负载均衡(Load Balance)是集群技术(Cluster)的一种应用技术.负载均衡可以将工作任务分摊到多个处理单元,从而提高并发处理能力.目前最常见的负载均衡应用是Web ... 
- 实战| Nginx+keepalived 实现高可用集群
			一个执着于技术的公众号 前言 今天通过两个实战案例,带大家理解Nginx+keepalived 如何实现高可用集群,在学习新知识之前您可以选择性复习之前的知识点: 给小白的 Nginx 10分钟入门指 ... 
- js实时查询,为空提示
			<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ... 
- Web安全学习笔记 SQL注入中
			Web安全学习笔记 SQL注入中 繁枝插云欣 --ICML8 权限提升 数据库检测 绕过技巧 一.权限提升 1. UDF提权 UDF User Defined Function,用户自定义函数 是My ... 
- awk应用场景之过滤举例
			以/etc/passwd举例,passwd文本 [root@196 tmp]# cat /etc/passwd root:x:0:0:root:/root:/bin/bash bin:x:1:1:bi ... 
- 『忘了再学』Shell基础 — 19、使用declare命令声明变量类型
			目录 1.declare命令介绍 2.声明数组变量类型 3.声明变量为环境变量 4.声明只读属性 5.补充: 1.declare命令介绍 Shell中所有变量的默认类型是字符串类型,如果你需要进行特殊 ... 
- conda和pip加速参考
			conda install和创建虚拟环境下载慢,可以修改/root/.condarc文件: vim /root/.condarc 各系统都可以通过修改用户目录下的 .condarc 文件.Window ... 
