MyBatis源码解读(六)

4.3、SqlNode

我们接着来看下一行。

arduino 复制代码
rootSqlNode.apply(context);

是一个叫SqlNode的类,SqlNode封装了XML中的sql节点,对于多个节点的封装,我们统一使用MixedSqlNode。我们按住ctrl+H可以看到他的继承关系。

SqlNode接口提供了apply方法,是一个函数式接口,接口会传递一个DynamicContext实例,循环遍历每一个标签,并且进行解析,并且在解析的时候都传入了我们的上下文,所以我们在解析一次就会拼接一次sql,最后生成一个可执行的sql,同时会将解析的结果保存在上下文中,特别是sql语句,该方法会针对不同类型的sqlNode进行特殊处理。

我们可以看到很多的老朋友,或许会比较眼熟。

这不就是我们动态sql里面的一些标签吗?常见的SqlNode有如下几个:

  1. StaticTextSqlNode负责拼接文本
  2. whereSqlnode处理and关键字,以及两边的空格
  3. IfSqlNode使用Ognl判断test中的表达式对标签进行过滤

4.3.1、MixedSqlNode

该节点的实现很简单,就是将内部维护的sqlNode集合全部遍历,调用其apply方法,这其实是一个递归的过程,当内部所有的node节点执行完成apply方法后,我们的DynamicContext中也会拼接出一个完整的sql。

java 复制代码
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;
  }
}

4.3.2、StaticTextSqlNode

静态文本节点的apply方法是最简单的实现,直接将文本进行拼接就可以了.

arduino 复制代码
public class StaticTextSqlNode implements SqlNode {
  private final String text;
​
  public StaticTextSqlNode(String text) {
    this.text = text;
  }
​
  @Override
  public boolean apply(DynamicContext context) {
    context.appendSql(text);
    return true;
  }

4.3.3、IfSqlNode

对于IfSqlNode我们需要进行判断,其test表达式是否满足条件,如果满足,则处理内部的sqlNode,否则丢弃节点,这也是if标签的作用。

arduino 复制代码
public class IfSqlNode implements SqlNode {
  private final ExpressionEvaluator evaluator;
  private final String test;
  private final SqlNode contents;
​
  public IfSqlNode(SqlNode contents, String test) {
    this.test = test;
    this.contents = contents;
    this.evaluator = new ExpressionEvaluator();
  }
​
  @Override
  public boolean apply(DynamicContext context) {
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }
}

他的解析过程是evaluateBoolean方法。

typescript 复制代码
//ExpressionEvaluator
public boolean evaluateBoolean(String expression, Object parameterObject) {
    // 使用Ognl表达式解析表达式
    Object value = OgnlCache.getValue(expression, parameterObject);
    if (value instanceof Boolean) {
        return (Boolean) value;
    }
    if (value instanceof Number) {
        return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
    }
    return value != null;
}
​
// OgnlCache
public static Object getValue(String expression, Object root) {
    try {
        Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null);
        return Ognl.getValue(parseExpression(expression), context, root);
    } catch (OgnlException e) {
        throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
    }
}

4.3.4、WhereSqlNode

WhereSqlNode的主要作用是将没用的前缀和后缀消除(掐头去尾),如and、or等。他和setSqlNode都是TrimSqlNode的子类,setSqlNode的作用是消除前后多余的逗号。

scala 复制代码
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,并不是他自己的方法,使用了父类的方法
        super(configuration, contents, "WHERE", prefixList, null, null);
    }
}

我们来看看他的父类TrimSqlNode的实现。

ini 复制代码
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) {
        this.contents = contents;
        this.prefix = prefix;
        this.prefixesToOverride = prefixesToOverride;
        this.suffix = suffix;
        this.suffixesToOverride = suffixesToOverride;
        this.configuration = configuration;
    }
​
    @Override
    public boolean apply(DynamicContext context) {
        FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
        
        // and a =1 and b =1
        boolean result = contents.apply(filteredDynamicContext);
        // 处理前后缀
        filteredDynamicContext.applyAll();
        return result;
    }
​
    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;
​
        // and a =1 and b =1
        public FilteredDynamicContext(DynamicContext delegate) {
            super(configuration, null);
            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);
            }
            delegate.appendSql(sqlBuffer.toString());
        }
​
​
        private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
            if (!prefixApplied) {
                prefixApplied = true;
                if (prefixesToOverride != null) {
                    for (String toRemove : prefixesToOverride) {
                        if (trimmedUppercaseSql.startsWith(toRemove)) {
                            // 删除前缀
                            sql.delete(0, toRemove.trim().length());
                            break;
                        }
                    }
                }
                if (prefix != null) {
                    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);
                }
            }
        }
    }
}

4.4、SqlSource

SqlSource为我们提供了一个SQL来源,通过该接口的实现我们可以获得一个SQL。这样的说法可能大家会觉得云里雾里,事实上MyBatis中的SQL是很复杂的,比如我们最长使用的动态SQL,该数据源为我们提供了一种解析SQL变量,如(#{id})以及转化动态sql的能力。MyBatis提供了StaticSqlSource、DynamicSqlSource、RawSqlSource等的实现

我们接下来要debug一下动态标签,所以我们得先伪造一条带有动态标签的sql,我们这里以模糊查询为例。我们先在mapper接口里面写上这个方法。

scss 复制代码
List<Dept> queryByCondition(Dept dept);

重点是sql的xml文件中我们需要去伪造一下动态标签。

bash 复制代码
<select id="queryByCondition" resultType="cn.linstudy.pojo.Dept">
        select * from dept
        <if test="1==1">
            where name = #{name}
        </if>
    </select>

这样的话debug的时候就可以进入到DynamicContext。我们来debug看一下。debug到执行完rootSqlNode.apply(context);方法后,我们context里面的sqlbuilder。

我们可以看到if标签全部没有了。我们来看一下context.getBindings的结果是什么?

他有两个参数:

  1. _parameter:表示我们传进来的参数。
  2. _databaseId:表示数据库标识,我们一般不传。

接着我们来看parse方法,看看里面做了啥骚操作。

ini 复制代码
  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);
    }
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

等到他判断isShrinkWhitespacesInSql这个条件的时候,可以发现为false,进入到sql = parser.parse(originalSql);这条sql语句。

我们点进去看这个parse方法。

ini 复制代码
public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    do {
        // 看看有没有转义符
      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);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if ((end <= offset) || (src[end - 1] != '\')) {
            expression.append(src, offset, end - offset);
            break;
          }
          // 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);
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    } while (start > -1);
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }
}

debug后我们可以看看BoundSql里面有啥玩意儿。

其实里面主要就是sql语句和我们传进来的参数映射。第0个参数的名字叫name,具体传进来的参数被封装在parameterObject里面。

4.4.1、StaticSqlSource

该实现类是最简单的实现,可以直接绑定一个静态的sql。

kotlin 复制代码
public class StaticSqlSource implements SqlSource {
​
    private final String sql;
    private final List<ParameterMapping> parameterMappings;
    private final Configuration configuration;
​
    public StaticSqlSource(Configuration configuration, String sql) {
        this(configuration, sql, null);
    }
​
    public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
        this.sql = sql;
        this.parameterMappings = parameterMappings;
        this.configuration = configuration;
    }
​
    @Override
    public BoundSql getBoundSql(Object parameterObject) {
        return new BoundSql(configuration, sql, parameterMappings, parameterObject);
    }
}

4.4.2、DynamicSqlSource

动态sql是我们使用最频繁也是相对来说较为复杂的sqlSource,我们通过解析sqlNode最终还是要将其以节点类型转化为一个sqlSource。

ini 复制代码
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);
        // 这个其中的过程我们已经学习过了,调用apply方法后,context已经保存了解析后的结果
        // select id, username, money from account where id = #{id}  and username = #{username}
        rootSqlNode.apply(context);
        // 构建一个sql解析器,重点解析#{},${}等
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        // 获得参数类型
        Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
        // 解析动态sql中的变量,如#{id}等
        // select id, username, money from account where id = ?  and username = ?
        SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        context.getBindings().forEach(boundSql::setAdditionalParameter);
        return boundSql;
    }
}

4.4.3、解析过程

首先我们可以通过测试用例来构建一个sqlSource,xmlLanguageDriver为我们提供了一个创建sqlsource的方法,我们随便写一个xml,在其中编写一个我们最常见的动态sql语句,这个动态sql可以写在任意文件中.

bash 复制代码
<select id="queryAll" resultMap="AccountMap">
    select id, name  from dept
    <where>
        <if test="id != null">
            and id = #{id}
        </if>
        <if test="name != null and name != ''">
            and name = #{name}
        </if>
    </where>
</select>
​

我们编写一个测试用例来测试一下。

ini 复制代码
@Test
public void testXMLLanguageDriver() {
    XMLLanguageDriver xmlLanguageDriver = new XMLLanguageDriver();
    // 1、获取配置类
    InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatis.xml");
    XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(inputStream);
    Configuration configuration = xmlConfigBuilder.parse();
​
    // 2、获取一个sql片段节点
    inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("sql.xml");
    XPathParser xPathParser = new XPathParser(inputStream,false,null,null);
    XNode xNode = xPathParser.evalNode("/select");
​
    // 3、设置查询条件
    Dept dept = new Dept();
    account.setId(1);
    account.setName("XiaoLin");
​
    // 4、创建一个sql源
    SqlSource sqlSource = xmlLanguageDriver.createSqlSource(configuration, xNode, Account.class);
    System.out.println(sqlSource.getBoundSql(account).getSql());
    System.out.println(sqlSource.getBoundSql(account).getParameterObject());
}

本质上,mybatis就是这样实现的,我们可以通过getBoundSql获取一个被绑定的sql,这其中需要一个解析的过程。

ini 复制代码
@Override
public BoundSql getBoundSql(Object parameterObject) {
    // 构建一个上下文
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 这个其中的过程我们已经学习过了,调用apply方法后,context已经保存了解析后的结果
    // select id, username, money from account where id = #{id}  and username = #{username}
    rootSqlNode.apply(context);
    // 构建一个sql解析器,重点解析#{},${}等
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    // 获得参数类型
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    // 解析动态sql中的变量,如#{id}等
    // select id, username, money from account where id = ?  and username = ?
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
}

而sqlSourceParser的解析过程大致长这样。

typescript 复制代码
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);
    }
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

还有一个移除额外的空格的方法。

ini 复制代码
public static String removeExtraWhitespaces(String original) {
    StringTokenizer tokenizer = new StringTokenizer(original);
    StringBuilder builder = new StringBuilder();
    boolean hasMoreTokens = tokenizer.hasMoreTokens();
    while (hasMoreTokens) {
        builder.append(tokenizer.nextToken());
        hasMoreTokens = tokenizer.hasMoreTokens();
        if (hasMoreTokens) {
            builder.append(' ');
        }
    }
    return builder.toString();
}

目前我们需要了解一下parser.parse(originalSql)的执行过程,该方法主要完成了以下工作:

  1. 将#{id}替换为?
  2. 解析其中的参数
ini 复制代码
// 解析 #{name}
public String parse(String text) {
    if (text == null || text.isEmpty()) {
        return "";
    }
    // search open token,如果没有 #{ 直接返回原始文本
    int start = text.indexOf(openToken);
    if (start == -1) {
        return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    // 表达式
    StringBuilder expression = null;
    
    // 处理#{},${}标签
    do {
        if (start > 0 && src[start - 1] == '\') {
            // 此打开令牌已转义。删除反斜杠并继续。
            builder.append(src, offset, start - offset - 1).append(openToken);
            offset = start + openToken.length();
        } else {
            // 找到了开启的标签,找关闭标签
            if (expression == null) {
                expression = new StringBuilder();
            } else {
                expression.setLength(0);
            }
            // select * from user where id = 
            builder.append(src, offset, start - offset);
            // 偏移量就是start+开启标签的长度,  #{id}就是#{标签的偏移量+2
            offset = start + openToken.length();
            // 查询offset之后的closeToken(})的位置作为end点
            int end = text.indexOf(closeToken, offset);
            
            while (end > -1) {
                if (end > offset && src[end - 1] == '\') {
                    // 这个关闭标记被转义。删除反斜杠并继续。
                    expression.append(src, offset, end - offset - 1).append(closeToken);
                    offset = end + closeToken.length();
                    end = text.indexOf(closeToken, offset);
                } else {
                    // 生成ognl表达式,#{id}---->id
                    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 {
                // 或拼接一个?,同时设置一个具有映射关系的参数
                builder.append(handler.handleToken(expression.toString()));
                offset = end + closeToken.length();
            }
        }
        start = text.indexOf(openToken, offset);
    } while (start > -1);
    
    // 将尾部的文本添加进来
    if (offset < src.length) {
        builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
}

handleToken的方法如下。

typescript 复制代码
// content是表达式 ,也就是我们#{}的变量,id和name
public String handleToken(String content) {
    parameterMappings.add(buildParameterMapping(content));
    return "?";
}

建立与?的映射关系的参数。

vbnet 复制代码
private ParameterMapping buildParameterMapping(String content) {
    Map<String, String> propertiesMap = parseParameterMapping(content);
    // 获取id,就是属性的名字id
    String property = propertiesMap.get("property");
    Class<?> propertyType;
    if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
        propertyType = metaParameters.getGetterType(property);
    } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
        propertyType = parameterType;
    } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
        propertyType = java.sql.ResultSet.class;
    } else if (property == null || Map.class.isAssignableFrom(parameterType)) {
        propertyType = Object.class;
    } else {
        MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
        if (metaClass.hasGetter(property)) {
            propertyType = metaClass.getGetterType(property);
        } else {
            propertyType = Object.class;
        }
    }
    // property-->"id", propertyType-->"java.lang.Integer"
    ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
    Class<?> javaType = propertyType;
    String typeHandlerAlias = null;
    // 设置其他属性,如果没有配置则不设置
    for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
        String name = entry.getKey();
        String value = entry.getValue();
        if ("javaType".equals(name)) {
            javaType = resolveClass(value);
            builder.javaType(javaType);
        } else if ("jdbcType".equals(name)) {
            builder.jdbcType(resolveJdbcType(value));
        } else if ("mode".equals(name)) {
            builder.mode(resolveParameterMode(value));
        } else if ("numericScale".equals(name)) {
            builder.numericScale(Integer.valueOf(value));
        } else if ("resultMap".equals(name)) {
            builder.resultMapId(value);
        } else if ("typeHandler".equals(name)) {
            typeHandlerAlias = value;
        } else if ("jdbcTypeName".equals(name)) {
            builder.jdbcTypeName(value);
        } else if ("property".equals(name)) {
            // Do Nothing
        } else if ("expression".equals(name)) {
            throw new BuilderException("Expression based parameters are not supported yet");
        } else {
            throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}.  Valid properties are " + PARAMETER_PROPERTIES);
        }
    }
    // 解析使用的typeHandler,最终选择(IntegerTypeHandler)
    if (typeHandlerAlias != null) {
        builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
    }
    return builder.build();
}

每次解析一个#{}标签都会将其替换成?,同时构建一个参数映射ParameterMapping,这样就保证了可以正常使用preparedStatement进行对数据库的操作了。

4.5、XMLLanguageDriver

java 复制代码
public class XMLLanguageDriver implements LanguageDriver {
​
    // 第二个参数是script节点
    @Override
    public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
    }
​
    // 第二个参数是script文本
    @Override
    public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
        // issue #3,判断是否是script标签
        // 要在带注解的映射器接口类中使用动态 SQL,可以使用 script 元素。
        //  @Update({"<script>select ...<where> ....  </script>"})
        if (script.startsWith("<script>")) {
            XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
            return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
        } else {
            // issue #127 当做文本处理
            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);
            }
        }
    }
​
}

动态sql的解析,这个过程是将sql标签的文本解析成为具有嵌套关系的sql节点实例。

ini 复制代码
public class XMLScriptBuilder extends BaseBuilder {
​
    private final XNode context;
    private boolean isDynamic;
    private final Class<?> parameterType;
    private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();
​
    public XMLScriptBuilder(Configuration configuration, XNode context) {
        this(configuration, context, null);
    }
​
    public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
        super(configuration);
        this.context = context;
        this.parameterType = parameterType;
        initNodeHandlerMap();
    }
​
​
    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
        MixedSqlNode rootSqlNode = parseDynamicTags(context);
        SqlSource sqlSource;
        if (isDynamic) {
            sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
            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));
            // <![CDATA[  ... ]]> 或者 文本标签
            if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
                String data = child.getStringBody("");
                TextSqlNode textSqlNode = new TextSqlNode(data);
                if (textSqlNode.isDynamic()) {
                    contents.add(textSqlNode);
                    isDynamic = true;
                } else {
                    contents.add(new StaticTextSqlNode(data));
                }
                // 元素标签的处理过程
            } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
                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);
    }
​
    private interface NodeHandler {
        void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
    }
​
​
    private class TrimHandler implements NodeHandler {
        public TrimHandler() {
            // Prevent Synthetic Access
        }
​
        @Override
        public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
            MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
            String prefix = nodeToHandle.getStringAttribute("prefix");
            String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
            String suffix = nodeToHandle.getStringAttribute("suffix");
            String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
            TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
            targetContents.add(trim);
        }
    }
​
    private class WhereHandler implements NodeHandler {
        public WhereHandler() {
            // Prevent Synthetic Access
        }
​
        @Override
        public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
            // 复杂的标签内会递归调用,知道解析到子元素为纯文本
            MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
            WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
            targetContents.add(where);
        }
    }
​
    private class ForEachHandler implements NodeHandler {
        public ForEachHandler() {
            // Prevent Synthetic Access
        }
​
        @Override
        public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
            MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
            String collection = nodeToHandle.getStringAttribute("collection");
            Boolean nullable = nodeToHandle.getBooleanAttribute("nullable");
            String item = nodeToHandle.getStringAttribute("item");
            String index = nodeToHandle.getStringAttribute("index");
            String open = nodeToHandle.getStringAttribute("open");
            String close = nodeToHandle.getStringAttribute("close");
            String separator = nodeToHandle.getStringAttribute("separator");
            ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, nullable, index, item, open, close, separator);
            targetContents.add(forEachSqlNode);
        }
    }
​
    private class IfHandler implements NodeHandler {
        public IfHandler() {
            // Prevent Synthetic Access
        }
​
        @Override
        public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
            MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
            String test = nodeToHandle.getStringAttribute("test");
            IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
            targetContents.add(ifSqlNode);
        }
    }
    // ...省略一些handler实现
   
}

目前为止,我们已经将一个sql标签解析为mapperstatement的全部过程有了详细的了解,mybatis工程启动时会扫描所有的mapper文件,并将其中的sql标签解析成一个个的mapperstatement,然后进行注册,并保存在了mappedStatements中,其中,mappedStatements其也是Configuration类的一个成员变量。

arduino 复制代码
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection").conflictMessageProducer((savedValue, targetValue) -> ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
相关推荐
小码编匠1 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#
AskHarries1 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_1 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
许野平3 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
BiteCode_咬一口代码4 小时前
信息泄露!默认密码的危害,记一次网络安全研究
后端
齐 飞4 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod4 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。5 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man6 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*6 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go