专栏首页Java技术大杂烩Mybatis 解析 SQL 源码分析二

Mybatis 解析 SQL 源码分析二

本文首发于个人公众号 Java 技术大杂烩,欢迎关注

Mybatis Mapper.xml 配置文件中 resultMap 节点的源码解析

Mybatis 解析 SQL 源码分析一

Mybatis Mapper 接口源码解析

Mybatis 数据库连接池源码解析

Mybatis 类型转换源码分析

Mybatis 解析配置文件的源码解析

前言

在上两篇文章 Mybatis 解析 SQL 源码分析一Mybatis Mapper.xml 配置文件中 resultMap 节点的源码解析 中分析了 Mybatis 是如何解析 Mapper.xml 配置文件的,配置文件中配置的 SQL 节点被解析成了一个个的 MappedStatement 对象放到了全局的配置对象 Configuration 中,其中 SQL 语句会被解析成 SqlSource 对象,这一步是在 Mybatis 加载的时候进行的;而在运行的时候,Mybatis 是如何把 SqlSource 对象和我们传入的参数解析成一条完整的,能够被数据库执行的 SQL 语句呢?下面就来看下这部分的源码,看看 Mybatis 是如何解析的。

该部分的解析会涉及到 组合模式OGNL 表达式的应用

SqlSource

在 Mybatis 解析 SQL 源码分析一 文章中我们知道,配置文件中的 SQL 语句会被解析成 SqlSource 对象,而 SQL 语句中定义的动态 SQL 节点,如 <where><if> 之类的使用 SqlNode 相关的实现类来表示。

现在先来看看 SqlSource 接口的定义:

1public interface SqlSource {
2
3  BoundSql getBoundSql(Object parameterObject);
4
5}

它只有一个方法 getBoundSql() ,该方法的返回值 BoundSql对象,它包含了 ? 占位符的 SQL 语句,以及绑定的实参,后面再来分析该类。

SqlSource 接口一共有 4 个实现类:

其中 DynamicSqlSource 负责处理动态 SQL 语句,RawSqlSource 负责处理静态 SQL 语句,它们最终会把处理后的 SQL 封装 StaticSqlSource 进行返回,StaticSqlSource 包含的 SQL 可能含有 占位符,可以被数据库直接执行,而 DynamicSqlSource 中的 SQL 还需要进一步解析才能被数据库执行。

这几个类后面再来看,现在先来看看动态 SQL 节点的解析。

DynamicContext

DynamicContext 该类主要用来存放解析动态 SQL 语句产生的 SQL 语句片段,比如说 解析 <if> 标签的时候,前面可能加 andor 之类的关键字,它就是用来存放这些 SQL 片段的。

 1public class DynamicContext {
 2
 3  // 有的SQL直接使用了该字面值,如 #{_parameter}
 4  public static final String PARAMETER_OBJECT_KEY = "_parameter";
 5  // 数据库ID,可以忽略
 6  public static final String DATABASE_ID_KEY = "_databaseId";
 7  // ................
 8  // 运行时传入的参数,是一个 map,内部类
 9  private final ContextMap bindings;
10
11  // 重要,用来拼接 SQL 语句的,每解析完一个动态SQL标签的时候,会把SQL片段拼接到该属性中,最后形成完整的SQL
12  private final StringBuilder sqlBuilder = new StringBuilder();
13
14  // 构造方法,就是把传进行的参数封装为 map
15  public DynamicContext(Configuration configuration, Object parameterObject) {
16    if (parameterObject != null && !(parameterObject instanceof Map)) {
17      MetaObject metaObject = configuration.newMetaObject(parameterObject);
18      bindings = new ContextMap(metaObject);
19    } else {
20      bindings = new ContextMap(null);
21    }
22    bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
23    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
24  }
25  // .............
26
27  // 添加 SQL 
28  public void appendSql(String sql) {
29    sqlBuilder.append(sql);
30    sqlBuilder.append(" ");
31  }
32
33  // 返回 SQL
34  public String getSql() {
35    return sqlBuilder.toString().trim();
36  }
37
38  // 存放参数的 map,继承了 HashMap,重写 get
39  static class ContextMap extends HashMap<String, Object> {
40
41    private MetaObject parameterMetaObject;
42
43    public ContextMap(MetaObject parameterMetaObject) {
44      this.parameterMetaObject = parameterMetaObject;
45    }
46
47    @Override
48    public Object get(Object key) {
49      String strKey = (String) key;
50      if (super.containsKey(strKey)) {
51        return super.get(strKey);
52      }
53      // 从运行参数中查找对应的属性
54      if (parameterMetaObject != null) {
55        return parameterMetaObject.getValue(strKey);
56      }
57      return null;
58    }
59  }
60}

SqlNode

在解析配置文件的时候知道 SQL 语句中定义的动态 SQL 节点,如 <where><if><foreach> 之类的使用SqlNode 相关的实现类来表示。SqlNode 的定义如下:

1public interface SqlNode {
2  boolean apply(DynamicContext context);
3}

它只有一个方法 apply(context) 方法,该方法会根据传进来的参数,解析该 SqlNode 代表的动态 SQL 节点,并调用 context.appendSql() 方法把解析后的 SQL 片段追加到 sqlBuilder 属性中进行保存。当 SQL 节点下的所有的 SqlNode 解析完毕后,就可以调用 context.getSql() 获取一条完整的 SQL。

现在来想想 Mybatis 有多少种动态SQL节点,如 <where><if><set><foreach><choose> 等等之类的,对应的 SqlNode 的实现类大概就有多少个。

SqlNode 的实现类有 10 个实现类,分别对应其动态SQL节点:

接下来依次看下每个动态的 SQL 节点是如何解析的.

StaticTextSqlNode

StaticTextSqlNode 表示的是 静态文本SQL节点,该种节点不需要解析,直接把对应的 SQL 语句添加到 DynamicContext.sqlBuilder 属性中即可。

 1public class StaticTextSqlNode implements SqlNode {
 2  // 对应的SQL片段
 3  private String text;
 4  public StaticTextSqlNode(String text) {
 5    this.text = text;
 6  }
 7  // 直接把 SQL 片段添加到 sqlBuilder 属性中即可
 8  @Override
 9  public boolean apply(DynamicContext context) {
10    context.appendSql(text);
11    return true;
12  }
13}

MixedSqlNode

MixedSqlNode 表示有多个 SqlNode节点,apply() 方法依次调用对应 SqlNode 节点的apply()方法:

 1public class MixedSqlNode implements SqlNode {
 2  private List<SqlNode> contents;
 3  public MixedSqlNode(List<SqlNode> contents) {
 4    this.contents = contents;
 5  }
 6  // 依次调用每个 SqlNode 的 apply 方法添加 SQL 片段
 7  @Override
 8  public boolean apply(DynamicContext context) {
 9    for (SqlNode sqlNode : contents) {
10      sqlNode.apply(context);
11    }
12    return true;
13  }
14}

TextSqlNode

TextSqlNode 表示的是包含有 ${} 占位符的动态 SQL 语句,它会调用 GenericTokenParser 工具类来解析 ${} 占位符,关于 GenericTokenParser 工具类,可以参考 Mybatis 解析配置文件的源码解析

比如有段 SQL 为 name=${name},参数为 name=zhangsan,则通过 TextSqlNode 解析后的 SQL 片段为 name=zhangsan,并把该 SQL 片段添加到 DynamicContext中。源码如下:

 1public class TextSqlNode implements SqlNode {
 2  // 要解析的动态SQL
 3  private String text;
 4  private Pattern injectionFilter;
 5  public TextSqlNode(String text, Pattern injectionFilter) {
 6    this.text = text;
 7    this.injectionFilter = injectionFilter;
 8  }
 9  // 解析SQL 
10  @Override
11  public boolean apply(DynamicContext context) {
12    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
13    // 实际上调用 BindingTokenParser 的handleToken方法进行解析
14    context.appendSql(parser.parse(text));
15    return true;
16  }
17  // 添加 ${ }
18  private GenericTokenParser createParser(TokenHandler handler) {
19    return new GenericTokenParser("${", "}", handler);
20  }
21
22  private static class BindingTokenParser implements TokenHandler {
23
24    private DynamicContext context;
25    private Pattern injectionFilter;
26
27    public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
28      this.context = context;
29      this.injectionFilter = injectionFilter;
30    }
31   // 解析 ${}
32    @Override
33    public String handleToken(String content) {
34      // 获取参数
35      Object parameter = context.getBindings().get("_parameter");
36      if (parameter == null) {
37        context.getBindings().put("value", null);
38      } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
39        context.getBindings().put("value", parameter);
40      }
41      // 获取值
42      Object value = OgnlCache.getValue(content, context.getBindings());
43      String srtValue = (value == null ? "" : String.valueOf(value)); 
44      checkInjection(srtValue); // 校验合法性
45      return srtValue;
46    }
47  }
48  // 判断是否是动态SQL
49  public boolean isDynamic() {
50    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
51    GenericTokenParser parser = createParser(checker);
52    parser.parse(text);
53    return checker.isDynamic();
54  }
55  private static class DynamicCheckerTokenParser implements TokenHandler {
56    private boolean isDynamic;
57    public DynamicCheckerTokenParser() {
58    }
59    public boolean isDynamic() {
60      return isDynamic;
61    }
62     // 调用该类就是动态SQL??
63    @Override
64    public String handleToken(String content) {
65      this.isDynamic = true;
66      return null;
67    }
68  }
69}

IfSqlNode

IfSqlNode 用来解析 <if> 标签的,先来看看 <if> 标签的用法:

1 <if test="username != null">
2    username=#{username}
3</if> 

解析如下:

 1public class IfSqlNode implements SqlNode {
 2  // 用来判断 test 条件true|false的,可以忽略不看
 3  private ExpressionEvaluator evaluator;
 4  // test 表达式
 5  private String test;
 6  // <if> 的子节点
 7  private SqlNode contents;
 8
 9  public IfSqlNode(SqlNode contents, String test) {
10    this.test = test;
11    this.contents = contents;
12    this.evaluator = new ExpressionEvaluator();
13  }
14
15  @Override
16  public boolean apply(DynamicContext context) {
17    // 如果 test 表达式为true,才会执行解析SQL
18    if (evaluator.evaluateBoolean(test, context.getBindings())) {
19      contents.apply(context);
20      return true;
21    }
22    return false;
23  }
24}

TrimSqlNode

TrimSqlNode 用来解析<trim>节点,它会根据子节点的解析结果添加或删除相应的前缀和后缀。

先来看下<trim>节点的使用场景,如果有如下SQL:

 1<select id="queryUser" resultType="User">
 2  SELECT * FROM user
 3  WHERE 
 4  <if test="name!= null">
 5    name= #{name}
 6  </if> 
 7  <if test="address!= null">
 8    AND address like #{address}
 9  </if>
10</select>

如果条件都不满足,或者只有 address 条件满足,则解析出来的SQL为 SELECT * FROM user WHERESELECT * FOMR user WHERE AND address ...

可以使用 <where>标签来解决该问题, <where> 标签只会在至少有一个子元素的条件返回 SQL 子句的情况下才去插入WHERE子句。而且,若语句的开头为ANDORwhere 元素也会将它们去除.

此外,我们还可以使用 <trim>来代替,如下所示:

1<trim prefix="WHERE" prefixOverrides="AND |OR ">
2  ... 
3</trim>

它的作用是移除所有指定在 prefixOverrides 属性中的内容,并且插入 prefix 属性中指定的内容

 1public class TrimSqlNode implements SqlNode {
 2  // trim 的子节点
 3  private SqlNode contents;
 4  //为 <trim> 节点包含的SQL添加的前缀字符串
 5  private String prefix;
 6  //为 <trim> 节点包含的SQL添加的后缀字符串
 7  private String suffix;
 8  // 删除指定的前缀
 9  private List<String> prefixesToOverride;
10  // 删除指定的后缀
11  private List<String> suffixesToOverride;
12  private Configuration configuration;
13
14  // 构造方法,同时解析删除的前缀和后缀字符串
15  public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
16    this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
17  }
18  // 解析删除的前缀和后缀字符串
19  private static List<String> parseOverrides(String overrides) {
20    if (overrides != null) {
21      // 按 | 分割,放到集合中
22      final StringTokenizer parser = new StringTokenizer(overrides, "|", false);
23      final List<String> list = new ArrayList<String>(parser.countTokens());
24      while (parser.hasMoreTokens()) {
25        list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
26      }
27      return list;
28    }
29
30  /...........
31  @Override
32  public boolean apply(DynamicContext context) {
33    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
34    // 解析子节点
35    boolean result = contents.apply(filteredDynamicContext);
36    // 处理前缀和后缀
37    filteredDynamicContext.applyAll();
38    return result;
39  }
40    // 内部类
41  private class FilteredDynamicContext extends DynamicContext {
42    private DynamicContext delegate;
43    // 是否已经处理过前缀,默认为false
44    private boolean prefixApplied;
45    // 是否已经处理过后缀,默认为false
46    private boolean suffixApplied;
47     // SQL
48    private StringBuilder sqlBuffer;
49    // 
50    public void applyAll() {
51      // 获取子节点解析结果
52      sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
53      String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
54      if (trimmedUppercaseSql.length() > 0) {
55        // 处理前缀
56        applyPrefix(sqlBuffer, trimmedUppercaseSql);
57        // 处理后缀
58        applySuffix(sqlBuffer, trimmedUppercaseSql);
59      }
60      // 最后拼接SQL
61      delegate.appendSql(sqlBuffer.toString());
62    }
63   // 处理前缀,处理后缀同理
64   private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
65      if (!prefixApplied) {
66        // 如果还没处理过,则处理
67        prefixApplied = true;
68        if (prefixesToOverride != null) {
69          for (String toRemove : prefixesToOverride) {
70            // 删除指定前缀
71            if (trimmedUppercaseSql.startsWith(toRemove)) {
72              sql.delete(0, toRemove.trim().length());
73              break;
74            }
75          }
76        }
77         // 添加指定前缀
78        if (prefix != null) {
79          sql.insert(0, " ");
80          sql.insert(0, prefix);
81        }
82      }
83    }
84}

WhereSqlNode

WhereSqlNode 用来处理<where>标签的,前面介绍 TrimSqlNode 的时候说过,<where> 标签会自动加上前缀where,去掉and的之类的,其实 <where> 标签使用 WhereSqlNode 类来解析,而 WhereSqlNodeTrimSqlNode 的子类,只不过是把 trim 标签的 prefix 属性设置为 where,而把 prefixToOverride 设置为 AND | OR 而已。

1public class WhereSqlNode extends TrimSqlNode {
2
3  private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
4
5  public WhereSqlNode(Configuration configuration, SqlNode contents) {
6    super(configuration, contents, "WHERE", prefixList, null, null);
7  }
8}

可以看到前缀 prefix 为 where,而需要删除的前缀为 AND | OR,而后缀和需要删除的后缀为null。

SetSqlNode

SetSqlNode 主要用来解析 <set> 标签,和 <where> 标签的解析类一样,也是继承了 TrimSqlNode 类,只不过把需要添加的前缀和需要删除的后缀设置为 SET 和 逗号即可。

1public class SetSqlNode extends TrimSqlNode {
2
3  private static List<String> suffixList = Arrays.asList(",");
4
5  public SetSqlNode(Configuration configuration,SqlNode contents) {
6    super(configuration, contents, "SET", null, null, suffixList);
7
8}

所以在 使用<set>标签的时候,如果最后一个条件不满足,转换为 SQL 的最后一个 逗号将会被自动去掉,如下所示:

1<update id="updateAuthorIfNecessary">
2  update Author
3    <set>
4      <if test="username != null">username=#{username},</if>
5      <if test="password != null">password=#{password},</if>
6      <if test="bio != null">bio=#{bio}</if>
7    </set>
8  where id=#{id}
9</update>

如果 bio 条件不满足,则最后一个逗号不会影响SQL的执行,应为它会被自动去掉。

ForeachSqlNode

ForeachSqlNode 主要是用来解析 <foreach> 节点的,先来看看 <foreach> 节点的用法:

1<select id="queryUsers" resultType="User">
2  SELECT * FROM user WHERE ID in
3  <foreach item="item" index="index" collection="list" open="(" separator="," close=")">
4        #{item}
5  </foreach>
6</select>

源码: 先来看看它的两个内部类:

PrefixedContext

 1  private class PrefixedContext extends DynamicContext {
 2
 3    private DynamicContext delegate;
 4    // 指定的前缀
 5    private String prefix;
 6    // 是否处理过前缀
 7    private boolean prefixApplied;
 8
 9    // .......
10
11    @Override
12    public void appendSql(String sql) {
13      // 如果还没有处理前缀,则添加前缀
14      if (!prefixApplied && sql != null && sql.trim().length() > 0) {
15        delegate.appendSql(prefix);
16        prefixApplied = true;
17      }
18       // 拼接SQL
19      delegate.appendSql(sql);
20    }
21}

FilteredDynamicContext

FilteredDynamicContext 是用来处理#{} 占位符的,但是并未绑定参数,只是把 #{item} 转换为#{_frch_item_1} 之类的占位符。

 1  private static class FilteredDynamicContext extends DynamicContext {
 2    private DynamicContext delegate;
 3    //对应集合项在集合的索引位置
 4    private int index;
 5    // item的索引
 6    private String itemIndex;
 7    // item的值
 8    private String item;
 9    //.............
10    // 解析 #{item}
11    @Override
12    public void appendSql(String sql) {
13      GenericTokenParser parser = new GenericTokenParser("#{", "}", new TokenHandler() {
14        @Override
15        public String handleToken(String content) {
16          // 把 #{itm} 转换为 #{__frch_item_1} 之类的
17          String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
18           // 把 #{itmIndex} 转换为 #{__frch_itemIndex_1} 之类的
19          if (itemIndex != null && newContent.equals(content)) {
20            newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
21          }
22          // 再返回 #{__frch_item_1} 或 #{__frch_itemIndex_1}
23          return new StringBuilder("#{").append(newContent).append("}").toString();
24        }
25      });
26      // 拼接SQL
27      delegate.appendSql(parser.parse(sql));
28    }
29  private static String itemizeItem(String item, int i) {
30    return new StringBuilder("__frch_").append(item).append("_").append(i).toString();
31  }
32}

ForeachSqlNode

了解了 ForeachSqlNode 它的两个内部类之后,再来看看它:

 1public class ForEachSqlNode implements SqlNode {
 2  public static final String ITEM_PREFIX = "__frch_";
 3  // 判断循环的终止条件
 4  private ExpressionEvaluator evaluator;
 5  // 循环的集合
 6  private String collectionExpression;
 7  // 子节点
 8  private SqlNode contents;
 9  // 开始字符
10  private String open;
11  // 结束字符
12  private String close;
13  // 分隔符
14  private String separator;
15  // 本次循环的元素,如果集合为 map,则index 为key,item为value
16  private String item;
17  // 本次循环的次数
18  private String index;
19  private Configuration configuration;
20
21  // ...............
22
23  @Override
24  public boolean apply(DynamicContext context) {
25    // 获取参数
26    Map<String, Object> bindings = context.getBindings();
27    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
28    if (!iterable.iterator().hasNext()) {
29      return true;
30    }
31    boolean first = true;
32    // 添加开始字符串
33    applyOpen(context);
34    int i = 0;
35    for (Object o : iterable) {
36      DynamicContext oldContext = context;
37      if (first) {
38        // 如果是集合的第一项,则前缀prefix为空字符串
39        context = new PrefixedContext(context, "");
40      } else if (separator != null) {
41        // 如果分隔符不为空,则指定分隔符
42        context = new PrefixedContext(context, separator);
43      } else {
44          // 不指定分隔符,在默认为空
45          context = new PrefixedContext(context, "");
46      }
47      int uniqueNumber = context.getUniqueNumber();  
48      if (o instanceof Map.Entry) {
49        // 如果集合是map类型,则将集合中的key和value添加到bindings参数集合中保存
50        Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
51        // 所以循环的集合为map类型,则index为key,item为value,就是在这里设置的
52        applyIndex(context, mapEntry.getKey(), uniqueNumber);
53        applyItem(context, mapEntry.getValue(), uniqueNumber);
54      } else {
55        // 不是map类型,则将集合中元素的索引和元素添加到 bindings集合中
56        applyIndex(context, i, uniqueNumber);
57        applyItem(context, o, uniqueNumber);
58      }
59      // 调用 FilteredDynamicContext 的apply方法进行处理
60      contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
61      if (first) {
62        first = !((PrefixedContext) context).isPrefixApplied();
63      }
64      context = oldContext;
65      i++;
66    }
67     // 添加结束字符串
68    applyClose(context);
69    return true;
70  }
71
72  private void applyIndex(DynamicContext context, Object o, int i) {
73    if (index != null) {
74      context.bind(index, o); // key为idnex,value为集合元素
75      context.bind(itemizeItem(index, i), o); // 为index添加前缀和后缀形成新的key
76    }
77  }
78
79  private void applyItem(DynamicContext context, Object o, int i) {
80    if (item != null) {
81      context.bind(item, o);
82      context.bind(itemizeItem(item, i), o);
83    }
84  }
85}

在开始的例子:

1<select id="queryUsers" resultType="User">
2  SELECT * FROM user WHERE ID in
3  <foreach item="item" index="index" collection="list" open="(" separator="," close=")">
4        #{item}
5  </foreach>
6</select>

解析后SQL如下:SELECT * FORM user WHERE ID in (#{__frch_item_0}, #{__frch_item_1})

ChooseSqlNode

ChooseSqlNode 用来解析 <choose> 节点的,比较简单:

 1public class ChooseSqlNode implements SqlNode {
 2  // 对应 <otherwise> 节点
 3  private SqlNode defaultSqlNode;
 4  // 对应<when> 节点
 5  private List<SqlNode> ifSqlNodes;
 6
 7  public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
 8    this.ifSqlNodes = ifSqlNodes;
 9    this.defaultSqlNode = defaultSqlNode;
10  }
11
12  @Override
13  public boolean apply(DynamicContext context) {
14    for (SqlNode sqlNode : ifSqlNodes) {
15      if (sqlNode.apply(context)) {
16        return true;
17      }
18    }
19    if (defaultSqlNode != null) {
20      defaultSqlNode.apply(context);
21      return true;
22    }
23    return false;
24  }
25}

SqlSourceBuilder

当SQL节点经过各个SqlNode.apply()解析后,SQL语句会被传到 SqlSourceBuilder 进一步解析。SqlSourceBuilder 主要完成两部:一是解析 #{} 占位符中的属性,格式类似于#{__frc_item_0, javaType=int, jdbcType=number, typeHandler=MyTypeHander},二是把SQL中的 #{} 替换为 ?

 1public class SqlSourceBuilder extends BaseBuilder {
 2  // 参数属性
 3  private static final String parameterProperties = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";
 4
 5  public SqlSourceBuilder(Configuration configuration) {
 6    super(configuration);
 7  }
 8  // 解析SQL
 9  // originalSql 经过 SqlNode.apply() 解析后的SQL
10  // parameterType 传入的参数类型
11  // additionalParameters 形参和实参的对应关系,即 DynamicContex.bindings 参数集合
12  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
13    // ParameterMappingTokenHandler 解析 #{}
14    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
15    // GenericTokenParser 与ParameterMappingTokenHandler 配合解析 #{}
16    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
17    // 得到含有 ? 占位符的SQL,
18    String sql = parser.parse(originalSql);
19    // 根据含有?占位符的SQL和参数创建StaticSqlSource对象
20    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
21  }
22  // 内部类,用来解析 #{}
23  private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
24    // parameterMappings 记录了 #{} 中的属性,可以忽略
25    private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
26    private Class<?> parameterType;
27    private MetaObject metaParameters;
28
29    public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) {
30      super(configuration);
31      this.parameterType = parameterType;
32      this.metaParameters = configuration.newMetaObject(additionalParameters);
33    }
34
35    public List<ParameterMapping> getParameterMappings() {
36      return parameterMappings;
37    }
38
39    @Override
40    public String handleToken(String content) {
41      parameterMappings.add(buildParameterMapping(content));
42      // 替换 ? 占位符
43      return "?";
44    }
45}

通过上述 SqlSourceBuilder 解析后得到 一个 StaticSqlSource 对象:

StaticSqlSource

 1public class StaticSqlSource implements SqlSource {
 2  // SQL
 3  private String sql;
 4  // 参数的属性集合
 5  private List<ParameterMapping> parameterMappings;
 6  private Configuration configuration;
 7
 8  public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
 9    this.sql = sql;
10    this.parameterMappings = parameterMappings;
11    this.configuration = configuration;
12  }
13  // 直接返回 BoundSql
14  @Override
15  public BoundSql getBoundSql(Object parameterObject) {
16    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
17  }
18}

BoundSql

 1public class BoundSql {
 2  // SQL ,可能含有 ? 占位符
 3  private String sql;
 4  // 参数的属性集合
 5  private List<ParameterMapping> parameterMappings;
 6  // 传入的实际参数
 7  private Object parameterObject;
 8  // 空的hashmap,之后中复制 DynamicContext.bindings 中的内容
 9  private Map<String, Object> additionalParameters;
10  //additionalParameters集合对象的 MetaObject 对象
11  private MetaObject metaParameters;
12}

DynamicSqlSource

最后一步使用 DynamicSqlSource 来解析动态的SQL语句:

 1public class DynamicSqlSource implements SqlSource {
 2
 3  private Configuration configuration;
 4  private SqlNode rootSqlNode;
 5
 6  public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
 7    this.configuration = configuration;
 8    this.rootSqlNode = rootSqlNode;
 9  }
10
11  @Override
12  public BoundSql getBoundSql(Object parameterObject) {
13    // 创建DynamicContext,parameterObject为传进来的参数
14    DynamicContext context = new DynamicContext(configuration, parameterObject);
15    //rootSqlNode.apply 方法会调用整个树形结构中全部的SqlNode的apply方法,每个SqlNode的apply方法解析得到的SQL片段会添加到 context中,最后调用 getSql 得到完整的SQL
16    rootSqlNode.apply(context);
17    // 解析 #{} 参数属性,并将 #{} 替换为 ?
18    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
19    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
20    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
21    // 创建 BoundSql 
22    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
23    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
24      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
25    }
26    return boundSql;
27  }
28}

RawSqlSource

除了 DynamicSqlSource 解析动态SQL,还有 RawSqlSource 来解析 静态SQL,原理差不多。

到这里,SQL就解析完了。

本文分享自微信公众号 - Java技术大杂烩(tsmyk0715),作者:TSMYK

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-03-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 源码分析 Mybatis 的 foreach 为什么会出现性能问题

    最近在做一个类似于综合报表之类的东西,需要查询所有的记录(数据库记录有限制),大概有1W条记录,该报表需要三个表的数据,也就是根据这 1W 个 ID 去执行查询...

    Java技术大杂烩
  • 公众号开发:获取用户消息和回复消息

    最近在看微信公众号的开发文档,觉得很有意思,可以自定义开发一些功能,比如有人关注了公众号之后,你可以做出稍微复杂点的回复(简单的回复在公众号后台配置就好啦);比...

    Java技术大杂烩
  • Mybatis 解析配置文件的源码解析

    使用过Mybatis 的都知道,Mybatis 有个配置文件,用来配置数据源,别名,一些全局的设置如开启缓存之类的, 在Mybatis 在初始化的时候,会加载该...

    Java技术大杂烩
  • [Android] Handler消息传递机制

    其实这块知识我都看过,但是读完这段话有些地方还是让我回想了一小会儿。想完就觉着既然回想了一遍,不如整理一篇博客出来好了。

    wOw
  • 【基础】通过反射访问private成员和方法,private设计的

    注意代码中的通过dm.setAccessible(true)这样在调用方法时他不会检查方法的修饰是public还是private。如果没有dm.setAcces...

    用户5640963
  • Java 14 开箱,它真香香香香

    Java 14 已经发布有一周时间了,我准备来开个箱,和小伙伴们一起来看看新特性里面都有哪些好玩的。我们程序员应该抱着尝鲜、猎奇的心态,否则就容易固步自封,技术...

    沉默王二
  • 做Java开发,你需要了解这些前言

    在开发中,我们写的代码肯定是越少越好,代码层次越清晰越好。那么下面就介绍一些可以减少代码量、可以让结构更清晰的好东西。本文涉及vo、dto的使用、全局异常处理、...

    贪挽懒月
  • Java常用的设计模式

    Creator:创建器,声明工厂方法,工厂方法通常会返回一个Product类的实例对象,而且多是抽象方法

    MiChong
  • [享学Feign] 三、原生Feign的核心API详解(一):UriTemplate、HardCodedTarget...

    代码下载地址:https://github.com/f641385712/feign-learning

    YourBatman
  • 快速学习-JPA中的一对多

    在一对多关系中,我们习惯把一的一方称之为主表,把多的一方称之为从表。在数据库中建立一对多的关系,需要使用数据库的外键约束。

    cwl_java

扫码关注云+社区

领取腾讯云代金券