MyBatis源码解析——动态SQL实现原理

如果读者有过JDBC编程经验,肯定能体会到SQL语句拼接的痛苦。在有些情况下,我们需要根据不同的查询条件动态地拼接SQL语句,拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号,这个过程非常容易出错,导致我们需要在调试SQL语句的正确性上花费一定的时间。MyBatis的动态SQL特性能够彻底解决我们的烦恼,本章我们就来学习MyBatis动态SQL的使用及它的实现原理。

动态SQL的使用

在介绍MyBatis动态SQL实现原理之前,我们先来了解一下MyBatis动态SQL的使用。顾名思义,动态SQL指的是事先无法预知具体的条件,需要在运行时根据具体的情况动态地生成SQL语句。

假设我们有一个获取用户信息查询操作,具体的查询条件是不确定的,取决于Web前端表单提交的数据,可能根据用户的Id进行查询,也可能根据用户手机号或姓名进行查询,还有可能是这几个条件的组合。这个时候就需要使用MyBatis的动态SQL特性了。下面是使用MyBatis动态SQL进行条件查询的一个案例,代码如下:

在上面的Mapper配置中,当我们不确定查询条件时,可以使用<where>和<if>标签,通过OGNL表达式判断参数内容是否为空,如果表达式结果为true,则MyBatis框架会自动拼接<if>标签内的SQL内容,否则会对<if>标签内的SQL片段进行忽略。

如上面配置中的<where>标签用于保证至少有一个查询条件时,才会在SQL语句中追加WHERE关键字,同时能够剔除WHERE关键字后相邻的OR和AND关键字。除了<if>和<where>标签外,MyBatis动态SQL相关的标签还有下面几个。<choose|when|otherwise>:这几个标签需要组合使用,类似于Java中的switch语法,使用如下:

这组标签与<if>标签不同的是,所有的<when>标签和<otherwise>标签是互斥的,当任何一个<when>标签满足条件时,其他标签均视为条件不成立。

<foreach>:该标签用于对集合参数进行遍历,通常用于构建IN条件语句或者INSERT批量插入语句。例如,当我们需要根据一组手机号查询用户信息时,可以使用如下配置:

<trim|set>:这两个标签的作用和<where>标签的作用类似,用于WHERE子句中因为不同的条件成立时导致AND或OR关键字多余,或者SET子句中出现多余的逗号问题。假如我们使用<if>标签进行动态SQL配置,具体配置内容如下:

当调用Mapper时传入的id参数和name参数都不为空时,生成的SQL是没问题的。但是当没有传入id参数或传入的id为空,而name参数不为空时,生成的SQL语句如下:

显然这种情况下生成的SQL语句是存在语法问题的,此时除了使用<where>标签外,还可以使用<trim>标签来解决这个问题。<trim>标签的使用如下:

<set>标签的作用和<trim>标签类似,用于避免SET子句中出现多余的逗号。这里就不做过多介绍了,读者可参考MyBatis官方文档。

SqlSource与BoundSql详解

MyBatis中的SqlSource用于描述SQL资源,通过前面章节的介绍,我们知道MyBatis可以通过两种方式配置SQL信息,一种是通过@Selelect、@Insert、@Delete、@Update或者@SelectProvider、@InsertProvider、@DeleteProvider、@UpdateProvider等注解;另一种是通过XML配置文件。SqlSource就代表Java注解或者XML文件配置的SQL资源。下面是SqlSource接口的定义:

/**
 * Represents the content of a mapped statement read from an XML file or an annotation.
 * It creates the SQL that will be passed to the database out of the input parameter received from the user.
 *
 * @author Clinton Begin
 */
public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

如上面的代码所示,SqlSource接口的定义非常简单,只有一个getBoundSql()方法,该方法返回一个BoundSql实例。BoundSql是对SQL语句及参数信息的封装,它是SqlSource解析后的结果。

 我们知道Executor组件与数据库交互,除了需要参数映射信息外,还需要参数信息。因此,Executor组件并不是直接通过StaticSqlSource对象完成数据库操作的,而是与BoundSql交互。BoundSql是对Executor组件执行SQL信息的封装,具体实现代码如下:

/**
 * An actual SQL String got from an {@link SqlSource} after having processed any dynamic content.
 * The SQL may have SQL placeholders "?" and a list (ordered) of a parameter mappings
 * with the additional information for each parameter (at least the property name of the input object to read
 * the value from).
 * <p>
 * Can also have additional parameters that are created by the dynamic language (for loops, bind...).
 *
 * @author Clinton Begin
 */
public class BoundSql {

  private final String sql;
  private final List<ParameterMapping> parameterMappings;
  private final Object parameterObject;
  private final Map<String, Object> additionalParameters;
  private final MetaObject metaParameters;

  public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
    this.sql = sql;
    this.parameterMappings = parameterMappings;
    this.parameterObject = parameterObject;
    this.additionalParameters = new HashMap<>();
    this.metaParameters = configuration.newMetaObject(additionalParameters);
  }

  public String getSql() {
    return sql;
  }

  public List<ParameterMapping> getParameterMappings() {
    return parameterMappings;
  }

  public Object getParameterObject() {
    return parameterObject;
  }

  public boolean hasAdditionalParameter(String name) {
    String paramName = new PropertyTokenizer(name).getName();
    return additionalParameters.containsKey(paramName);
  }

  public void setAdditionalParameter(String name, Object value) {
    metaParameters.setValue(name, value);
  }

  public Object getAdditionalParameter(String name) {
    return metaParameters.getValue(name);
  }
}

如上面的代码所示,BoundSql除了封装了Mapper解析后的SQL语句和参数映射信息外,还封装了Mapper调用时传入的参数对象。另外,MyBatis任意一个Mapper都有两个内置的参数,即_parameter和_databaseId。_parameter代表整个参数,包括<bind>标签绑定的参数信息,这些参数存放在BoundSql对象的additionalParameters属性中。_databaseId为Mapper配置中通过databaseId属性指定的数据库类型。

SqlSource接口有4个不同的实现,分别为StaticSqlSource、DynamicSqlSource、RawSqlSource和ProviderSqlSource。这4种SqlSource实现类的作用如下。

  • ProviderSqlSource:用于描述通过@Select、@SelectProvider等注解配置的SQL资源信息。
  • DynamicSqlSource:用于描述Mapper XML文件中配置的SQL资源信息,这些SQL通常包含动态SQL配置或者${}参数占位符,需要在Mapper调用时才能确定具体的SQL语句。
  • RawSqlSource:用于描述Mapper XML文件中配置的SQL资源信息,与DynamicSqlSource不同的是,这些SQL语句在解析XML配置的时候就能确定,即不包含动态SQL相关配置。
  • StaticSqlSource:用于描述ProviderSqlSource、DynamicSqlSource及RawSqlSource解析后得到的静态SQL资源。

无论是Java注解还是XML文件配置的SQL信息,在Mapper调用时都会根据用户传入的参数将Mapper配置转换为StaticSqlSource类。我们不妨了解一下StaticSqlSource类的实现,代码如下:

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);
  }

}

如上面的代码所示,StaticSqlSource类的内容比较简单,只封装了Mapper解析后的SQL内容和Mapper参数映射信息。

LanguageDriver详解

实际上,SQL配置信息到SqlSource对象的转换是由LanguageDriver组件来完成的。下面来看一下LanguageDriver接口的定义,代码如下:

import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.Configuration;

public interface LanguageDriver {

  /**
   * Creates a {@link ParameterHandler} that passes the actual parameters to the the JDBC statement.
   *
   * @author Frank D. Martinez [mnesarco]
   * @param mappedStatement The mapped statement that is being executed
   * @param parameterObject The input parameter object (can be null)
   * @param boundSql The resulting SQL once the dynamic language has been executed.
   * @return the parameter handler
   * @see DefaultParameterHandler
   */
  ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);

  /**
   * Creates an {@link SqlSource} that will hold the statement read from a mapper xml file.
   * It is called during startup, when the mapped statement is read from a class or an xml file.
   *
   * @param configuration The MyBatis configuration
   * @param script XNode parsed from a XML file
   * @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null.
   * @return the sql source
   */
  SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);

  /**
   * Creates an {@link SqlSource} that will hold the statement read from an annotation.
   * It is called during startup, when the mapped statement is read from a class or an xml file.
   *
   * @param configuration The MyBatis configuration
   * @param script The content of the annotation
   * @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null.
   * @return the sql source
   */
  SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);

}

如上面的代码所示,LanguageDriver接口中一共有3个方法,其中createParameterHandler()方法用于创建ParameterHandler对象,另外还有两个重载的createSqlSource()方法,这两个重载的方法用于创建SqlSource对象。MyBatis中为LanguageDriver接口提供了两个实现类,分别为XMLLanguageDriver和RawLanguageDriver。XMLLanguageDriver为XML语言驱动,为MyBatis提供了通过XML标签(我们常用的<if>、<where>等标签)结合OGNL表达式语法实现动态SQL的功能。而RawLanguageDriver表示仅支持静态SQL配置,不支持动态SQL功能。

接下来我们重点了解一下XMLLanguageDriver实现类的内容,代码如下:

public class XMLLanguageDriver implements LanguageDriver {

  @Override
  public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
  }

  // 该方法用于解析XML文件中配置的SQL信息
  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    // 创建XMLScriptBuilder对象
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    
    // 调用XMLScriptBuilder的parseScriptNode()方法解析SQL资源
    return builder.parseScriptNode();
  }

  // 该方法用于解析XML文件中配置的SQL信息
  @Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    // 若字符串以<script>标签开头,则以XML方式解析
    if (script.startsWith("<script>")) {
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
      // 解析SQL配置中的全局变量
      script = PropertyParser.parse(script, configuration.getVariables());
      TextSqlNode textSqlNode = new TextSqlNode(script);

      // 如果SQL中仍包含${}参数占位符,则返回DynamicSqlSource实例,否则返回RawSqlSource
      if (textSqlNode.isDynamic()) {
        return new DynamicSqlSource(configuration, textSqlNode);
      } else {
        return new RawSqlSource(configuration, script, parameterType);
      }
    }
  }

}

如上面的代码所示,XMLLanguageDriver类实现了LanguageDriver接口中两个重载的createSqlSource()方法,分别用于处理XML文件和Java注解中配置的SQL信息,将SQL配置转换为SqlSource对象。

  • 第一个重载的createSqlSource()方法用于处理XML文件中配置的SQL信息,该方法中创建了一个XMLScriptBuilder对象,然后调用XMLScriptBuilder对象的parseScriptNode()方法将SQL资源转换为SqlSource对象。
  • 第二个重载的createSqlSource()方法用于处理Java注解中配置的SQL信息,该方法中首先判断SQL配置是否以<script>标签开头,如果是,则以XML方式处理Java注解中配置的SQL信息,否则简单处理,替换SQL中的全局参数。如果SQL中仍然包含${}参数占位符,则SQL语句仍然需要根据传递的参数动态生成,所以使用DynamicSqlSource对象描述SQL资源,否则说明SQL语句不需要根据参数动态生成,使用RawSqlSource对象描述SQL资源。

从XMLLanguageDriver类的createSqlSource()方法的实现来看,我们除了可以通过XML配置文件结合OGNL表达式配置动态SQL外,还可以通过Java注解的方式配置,只需要注解中的内容加上<script>标签。下面是使用Java注解配置动态SQL的案例代码:

MyBatis从3.2版本开始支持可插拔脚本语言,这允许我们插入一种脚本语言驱动,并基于这种语言来编写动态SQL语句。例如,我们可以让MyBatis的Mapper配置支持Velocity(或者Freemaker)语法,并基于Velocity(或者Freemaker)语法编写动态SQL。

要实现自定义的脚本语言驱动,只需要实现LanguageDriver接口,创建自定义的SqlSource对象,然后对SqlSource对象进行解析,生成最终的BoundSql对象即可。有兴趣的读者可以参考velocity-scripting模块的源码,该模块为MyBatis的Mapper配置提供Velocity语法支持。

接下来笔者就以velocity-scripting模块为例介绍自定义LanguageDriver的使用。要使用velocity-scripting模块,首先需要在项目中添加该模块的依赖,如果是Maven项目,则只需要在pom.xml文件中增加如下内容:

为了简化LanguageDriver的类型限定名,便于在使用时引用,我们可以在MyBatis主配置文件中为velocity-scripting模块自定义的LanguageDriver指定一个别名,代码如下:

接下来就可以在配置Mapper时使用Velocity语法了,例如:

要注意的是,在配置Mapper时,需要通过lang属性指定velocity-scripting模块中定义的LanguageDriver的别名。上面代码中的#where()和#in()指令是velocity-scripting模块自定义的指令,更多细节读者可以参考velocity-scripting模块官方文档。

SqlNode详解

SqlNode用于描述Mapper SQL配置中的SQL节点,它是MyBatis框架实现动态SQL的基石。我们首先来看一下SqlNode接口的内容,代码如下:

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

如上面的代码所示,SqlNode接口的内容非常简单,只有一个apply()方法,该方法用于解析SQL节点,根据参数信息生成静态SQL内容。apply()方法需要接收一个DynamicContext对象作为参数,DynamicContext对象中封装了Mapper调用时传入的参数信息及MyBatis内置的_parameter和_databaseId参数。

在使用动态SQL时,我们可以使用<if>、<where>、<trim>等标签,这些标签都对应一种具体的SqlNode实现类,这些实现类如图9-2所示。

这些SqlNode实现类的作用如下:

  • IfSqlNode:用于描述动态SQL中<if>标签的内容,XMLLanguageDriver在解析Mapper SQL配置生成SqlSource时,会对动态SQL中的<if>标签进行解析,将<if>标签转换为IfSqlNode对象。
  • ChooseSqlNode:用于描述动态SQL配置中的<choose>标签内容,Mapper解析时会把<choose>标签配置内容转换为ChooseSqlNode对象。
  • ForEachSqlNode:用于描述动态SQL配置中的<foreach>标签,<foreach>标签配置信息在Mapper解析时会转换为ForEachSqlNode对象。
  • MixedSqlNode:用于描述一组SqlNode对象,通常一个Mapper配置是由多个SqlNode对象组成的,这些SqlNode对象通过MixedSqlNode进行关联,组成一个完整的动态SQL配置。
  • SetSqlNode:用于描述动态SQL配置中的<set>标签,Mapper解析时会把<set>标签配置信息转换为SetSqlNode对象。
  • WhereSqlNode:用于描述动态SQL中的<where>标签,动态SQL解析时,会把<where>标签内容转换为WhereSqlNode对象。
  • TrimSqlNode:用于描述动态SQL中的<trim>标签,动态SQL解析时,会把<trim>标签内容转换为TrimSqlNode对象。<where>标签和<set>标签实际上是<trim>标签的一种特例,<where>标签和<set>标签实现的内容都可以使用<trim>标签来完成,因此WhereSqlNode和SetSqlNodel类设计为TrimSqlNode类的子类,属于特殊的TrimSqlNode。
  • StaticTextSqlNode:用于描述动态SQL中的静态文本内容。
  • TextSqlNode:该类与StaticTextSqlNode类不同的是,当静态文本中包含${}占位符时,说明${}需要在Mapper调用时将${}替换为具体的参数值。因此,使用TextSqlNode类来描述。
  • VarDeclSqlNode:用于描述动态SQL中的<bind>标签,动态SQL解析时,会把<bind>标签配置信息转换为VarDeclSqlNode对象。

了解了各个SqlNode实现类的作用后,接下来我们来了解一下SqlNode与动态SQL配置之间的对应关系。假如我们有如下Mapper配置:

上面是一个完整的Mapper SQL配置,从MyBatis动态SQL的角度来看,它是由4个SqlNode对象构成的。该Mapper配置转换为SqlNode代码如下。

在上面的代码中,我们创建了一个StaticTextSqlNode和三个IfSqlNode来描述Mapper中动态SQL的配置,其中IfSqlNode由一个StaticTextSqlNode和条件表达式组成。接着创建了一个MixedSqlNode将这些SqlNode组合起来,这样就完成了通过Java对象来描述动态SQL配置。

SqlNode对象创建完毕后,我们就可以调用MixedSqlNode的apply()方法根据参数内容动态地生成SQL内容了。该方法接收一个DynamicContext对象作为参数,DynamicContext对象中封装了Mapper调用时的参数信息。上面的代码中,我们创建了一个DynamicContext,然后调用MixedSqlNode对象的apply()方法,动态SQL的解析结果封装在DynamicContext对象中,我们只需要调用DynamicContext对象的getSql()方法即可获取动态SQL解析后的SQL语句。运行上面这段代码后,生成的SQL内容如下:

select * from user where 1=1 AND id = #{id)

接下来我们再来了解一下SqlNode解析生成SQL语句的过程。首先来看MixedSqlNode的实现,代码如下:

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;
  }
}

如上面的代码所示,MixedSqlNode类的实现比较简单,通过一个List对象维护所有的SqlNode对象,MixedSqlNode类的apply()方法中对所有SqlNode对象进行遍历,以当前DynamicContext对象作为参数,调用所有SqlNode对象的apply()方法。接下来我们再来看一下StaticTextSqlNode的实现,代码如下:

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;
  }

}

如上面的代码所示,StaticTextSqlNode实现类比较简单,该类中维护了Mapper配置中的静态SQL节点内容。调用apply()方法时,将静态SQL文本内容追加到DynamicContext对象中。

首先来看TrimSqlNode的实现,代码如下:

public class TrimSqlNode implements SqlNode {

  private final SqlNode contents;
  private final String prefix;  // SQL语句的前缀
  private final String suffix;  // SQL语句的后缀
  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);
    
    // 调用委托的SqlNode的apply()方法来解析FilteredDynamicContext
    boolean result = contents.apply(filteredDynamicContext);

    // 过滤掉脚本中的prefixesToOverride和suffixesToOverride部分
    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;

    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());
    }

    @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) {
          // 将sql中开头部分的prefixesToOverride删掉
          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) {
          // 将sql中结尾部分的suffixesToOverride删掉
          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);
        }
      }
    }

  }

}

然后来看WhereSqlNode的实现,代码如下:

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) {
    // SQL脚本中的前缀是WHERE,需要
    super(configuration, contents, "WHERE", prefixList, null, null);
  }

}

在WhereSqlNode对象的apply()函数中,先调用委托SqlNode对象的apply()函数,然后执行FilteredDynamicContext的函数applyAll()时,会将SQL脚本中开头的AND和OR删掉。

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);
  }

}

在SetSqlNode对象的apply()函数中,先调用委托SqlNode对象的apply()函数,然后执行FilteredDynamicContext的函数applyAll()时,会将SQL脚本中开头和结尾的","符号删掉。

最后我们了解一下实现动态SQL比较关键的SqlNode实现类之一——IfSqlNode的实现,代码如下:

public class IfSqlNode implements SqlNode {
  // evaluator属性用于解析OGNL表达式
  private final ExpressionEvaluator evaluator;

  // 保存<if>标签test属性内容
  private final String test;

  // <if>标签内的SQL内容
  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) {
    // 如果OGNL表达式值为true,则调用<if>标签内容对应的SqlNode的apply()方法
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }

}

如上面的代码所示,IfSqlNode中维护了一个ExpressionEvaluator类的实例,该实例用于根据当前参数对象解析OGNL表达式。另外,IfSqlNode维护了<if>标签test属性指定的表达式内容和<if>标签中的SQL内容对应的SqlNode对象。在IfSqlNode类的apply()方法中,首先解析test属性指定的OGNL表达式,只有当表达式值为true的情况下,才会执行<if>标签中SQL内容对应的SqlNode的apply()方法。这样就实现了只有当<if>标签test属性表达式值为true的情况下,才会追加<if>标签中配置的SQL信息。

动态SQL解析过程

SqlSource用于描述通过XML文件或者Java注解配置的SQL资源信息;SqlNode用于描述动态SQL中<if>、<where>等标签信息;LanguageDriver用于对Mapper SQL配置进行解析,将SQL配置转换为SqlSource对象。要了解MyBatis动态SQL的解析过程,我们可以从XMLLanguageDriver类的createSqlSource()方法出发进行分析,该方法代码如下:

  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }

如上面的代码所示,在XMLLanguageDriver类createSqlSource()方法中,Mapper SQL配置的解析实际上是委托给XMLScriptBuilder类来完成的,该方法中首先创建了一个XMLScriptBuilder对象,然后调用XMLScriptBuilder对象的parseScriptNode()方法完成解析工作。XMLScriptBuilder类的构造函数如下:

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());
  }

XMLScriptBuilder类的parseScriptNode()方法代码如下:

  public SqlSource parseScriptNode() {
    // 调用parseDynamicTags()方法将SQL配置转换为SqlNode对象
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    // 判断Mapper SQL配置中是否包含动态SQL元素,如果是,就创建DynamicSqlSource对象,否则创建RawSqlSource对象
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

如上面的代码所示,在XMLScriptBuilder类的parseScriptNode()方法中,调用parseDynamicTags()方法将SQL配置转换为SqlNode对象,然后判断SQL配置是否为动态SQL,如果为动态SQL,则创建DynamicSqlSource对象,否则创建RawSqlSource对象。需要注意的是,MyBatis中判断SQL配置是否属于动态SQL的标准是SQL配置是否包含<if>、<where>、<trim>等元素或者${}参数占位符。

接下来,我们再来看一下XMLScriptBuilder类的parseDynamicTags()方法的实现,代码如下:

  protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    NodeList children = node.getNode().getChildNodes();

    // 对XML子元素进行遍历
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));

      // 如果子元素为SQL文本内容,则使用TextSqlNode描述该节点
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);

        // 若SQL脚本中包含${}参数占位符,则为动态SQL
        if (textSqlNode.isDynamic()) {
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          // 如果SQL中不包含${}参数占位符,则不是动态SQL
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        // 如果子元素为<if>、<where>等标签,则使用对应的NodeHandler处理
        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);
  }

如上面的代码所示,XMLScriptBuilder类的parseDynamicTags()方法的逻辑相当复杂,在该方法中对SQL配置的所有子元素进行遍历,如果子元素类型为SQL文本,则使用TextSqlNode对象描述SQL节点信息,若SQL节点中存在${}参数占位符,则设置XMLScriptBuilder对象的isDynamic属性值为true;如果子元素为<if>、<where>等标签,则使用对应的NodeHandler处理。

XMLScriptBuilder类中定义了一个私有的NodeHandler接口,并为每种动态SQL标签提供了一个NodeHandler接口的实现类,通过实现类处理对应的动态SQL标签,把动态SQL标签转换为对应的SqlNode对象。

XMLScriptBuilder类中为NodeHandler接口提供了8个实现类,每个实现类用于处理对应的动态SQL标签,例如IfHandler用于处理动态SQL配置中的<if>标签,将<if>标签内容转换为IfSqlNode对象。

接下来我们来看一下NodeHandler接口的定义,代码如下:

  private interface NodeHandler {
    void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
  }

如上面的代码所示,NodeHandler接口中只有一个handleNode()方法,该方法接收一个动态SQL标签对应的XNode对象和一个存放SqlNode对象的List对象,handleNode()方法中对XML标签进行解析后,把生成的SqlNode对象添加到List对象中。我们可以参考一下IfHandler类的实现,代码如下:

  private class IfHandler implements NodeHandler {
    public IfHandler() {
      // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      // 继续调用parseDynamicTags方法解析<if>标签中的子节点
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);

      // 获取<if>标签test属性
      String test = nodeToHandle.getStringAttribute("test");
      IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);

      // 将IfSqlNode对象添加到targetContents集合中
      targetContents.add(ifSqlNode);
    }
  }

在IfHandler类的handleNode()方法中会继续调用XMLScriptBuilder类的parseDynamicTags()方法完成<if>标签子节点的解析,将子节点转换为MixedSqlNode对象,然后获取<if>标签test属性对应的OGNL表达式,接着创建IfSqlNode对象并添加到List对象中。parseDynamicTags()方法的内容前面我们已经分析过了,该方法中会获取当前节点的所有子节点,如果子节点内容为动态SQL标签,继续调用动态SQL标签对应的NodeHandler进行处理,这样就“递归”地完成了所有动态SQL标签的解析。

其他SqlNode实现类的处理逻辑与之类似。例如,下面是ForEachHandler类的实现代码:

  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);
    }
  }

如上面的代码所示,ForEachHandler类的handleNode()方法中也会调用XMLScriptBuilder类的parseDynamicTags()解析<foreach>标签所有子元素,如果子元素中包含<if>标签或<foreach>标签,则继续调用IfHandler或者ForEachHandler对象的handleNode()方法进行处理,直到所有的动态SQL元素全部被转换成SqlNode对象。

需要注意的是,XMLScriptBuilder类的构造方法中,会调用initNodeHandlerMap()方法将所有NodeHandler的实例注册到Map中,代码如下:

  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());
  }

需要解析动态SQL标签时,只需要根据标签名称获取对应的NodeHander对象进行处理即可,而不用每次都创建对应的NodeHandler实例,这也是享元思想的应用。上面是动态SQL配置转换为SqlNode对象的过程,那么SqlNode对象是如何根据调用Mapper时传入的参数动态生成SQL语句的呢?接下来我们回顾一下XMLScriptBuilder类的parseScriptNode()方法,代码如下:

  public SqlSource parseScriptNode() {
    // 调用parseDynamicTags()方法将SQL配置转换为SqlNode对象
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    // 判断Mapper SQL配置中是否包含动态SQL元素,如果是,就创建DynamicSqlSource对象,否则创建RawSqlSource对象
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

动态SQL标签解析完成后,将解析后生成的SqlNode对象封装在SqlSource对象中。通过前面的学习我们知道,MyBatis中的MappedStatement用于描述Mapper中的SQL配置,SqlSource创建完毕后,最终会存放在MappedStatement对象的sqlSource属性中,Executor组件操作数据库时,会调用MappedStatement对象的getBoundSql()方法获取BoundSql对象,代码如下:

public final class MappedStatement {
  private SqlSource sqlSource; // 解析SQL语句生成的SqlSource实例

  // 调用SqlSource类的getBoundSql()函数获取BoundSql对象实例
  public BoundSql getBoundSql(Object parameterObject) {
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings == null || parameterMappings.isEmpty()) {
      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
    }

    // check for nested result maps in parameter mappings (issue #30)
    for (ParameterMapping pm : boundSql.getParameterMappings()) {
      String rmId = pm.getResultMapId();
      if (rmId != null) {
        ResultMap rm = configuration.getResultMap(rmId);
        if (rm != null) {
          hasNestedResultMaps |= rm.hasNestedResultMaps();
        }
      }
    }

    return boundSql;
  }

如上面的代码所示,MappedStatement对象的getBoundSql()方法会调用SqlSource对象的getBoundSql()方法,这个过程就完成了SqlNode对象解析成SQL语句的过程。我们可以了解一下DynamicSqlSource类的getBoundSql()方法的实现,代码如下:

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) {
    // 通过参数对象创建动态SQL上下文对象
    DynamicContext context = new DynamicContext(configuration, parameterObject);

    // 以动态SQL上下文对象作为参数调用SqlNode的apply()函数
    rootSqlNode.apply(context);

    // 创建SqlSourceBuilder对象实例
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();

    // 调用DynamicContext的getSql()方法获取动态SQL解析后的SQL内容
    // 然后调用SqlSourceBuilder的parse()方法对SQL内容做进一步处理,生成StaticSqlSource对象
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());

    // 调用StaticSqlSource对象的getBoundSql()方法获得BoundSql实例
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

    // 将<bind>标签绑定的参数添加到BoundSql对象中
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

}

如上面的代码所示,在DynamicSqlSource类的getBoundSql()方法中,首先根据参数对象创建DynamicContext对象,然后调用SqlNode对象的apply()方法对动态SQL进行解析。动态SQL解析完成后,调用DynamicContext对象的getSql()方法获取动态SQL解析后的结果。接着调用SqlSourceBuilder对象的parse()方法对动态SQL解析后的结果进一步解析处理,该方法返回一个StaticSqlSource对象,StaticSqlSource用于描述动态SQL解析后的静态SQL资源。

接下来,我们再来了解一下SqlSourceBuilder类的parse()方法对动态SQL解析后的结果到底做了什么操作。该方法的代码如下:

public class SqlSourceBuilder extends BaseBuilder {

  private static final String PARAMETER_PROPERTIES = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";

  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    // ParameterMappingTokenHandler为Mybatis参数映射处理器,用于处理SQL中的#{}参数占位符
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType,
        additionalParameters);

    // Token解析器,用于解析#{}参数
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql;

    // 调用GenericTokenParser对象的parse()方法将#{}参数占位符替换为?
    if (configuration.isShrinkWhitespacesInSql()) {
      sql = parser.parse(removeExtraWhitespaces(originalSql));
    } else {
      sql = parser.parse(originalSql);
    }
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

如上面的代码所示,在SqlSourceBuilder类的parse()方法中,首先创建了一个ParameterMappingTokenHandler对象,ParameterMappingTokenHandler为MyBatis参数映射处理器,用于处理SQL中的#{}参数占位符。接着创建了一个GenericTokenParser对象,GenericTokenParser用于对SQL中的#{}参数占位符进行解析,获取#{}参数占位符中的内容。

针对上述函数,如果输入的原始SQL脚本是

INSERT INTO BLOG (ID, NAME, NOTE, COMMENT) 
VALUES (  #{uuu.u}, #{__frch_u_0.id}, #{__frch_u_0,typeHandler=org.apache.ibatis.type.StringTypeHandler}, #{__frch_u_0:VARCHAR,typeHandler=org.apache.ibatis.type.StringTypeHandler} )

则GenericTokenParser对象调用函数parse()后解析结果如下:

INSERT INTO BLOG (ID, NAME, NOTE, COMMENT) VALUES (  ?, ?, ?, ? )

SqlSourceBuilder类的parse()方法调试过程中,局部变量和参数的值如下:

 我们首先来看GenericTokenParser类解析#{}参数占位符的过程,代码如下:

public class GenericTokenParser {

  private final String openToken;     // 上述样例中为#{
  private final String closeToken;     // 上述样例中为}
  private final TokenHandler handler;    // TokenHandler对象实例

  public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
    this.openToken = openToken;
    this.closeToken = closeToken;
    this.handler = handler;
  }

  public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // 获取第一个#{在SQL中的位置
    int start = text.indexOf(openToken);

    start为-1说明SQL中不存在任何#{}参数占位符
    if (start == -1) {
      return text;
    }

    // 将SQL转换为char数组
    char[] src = text.toCharArray();
    int offset = 0; // 用offset记录已解析的#{或者}的偏移量,避免重复解析
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;  // expression为#{}中的参数内容

    // 遍历获取所有#{}参数占位符的内容,然后调用TokenHandler对象的handleToken()方法替换参数占位符
    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 {
          // 调用TokenHandler对象的handleToken()方法替换参数占位符
          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();
  }
}

 从上面的代码可以看出,SQL配置中的所有#{}参数占位符内容都被替换成了“?”字符,为什么要替换成一个“?”字符呢?读者可能会联想到JDBC中的PreparedStatement,MyBatis默认情况下会使用PreparedStatement对象与数据库进行交互,因此#{}参数占位符内容被替换成了问号,然后调用PreparedStatement对象的setXXX()方法为参数占位符设置值。除此之外,ParameterMappingTokenHandler的handleToken()方法中还做了另一件事情,就是调用buildParameterMapping()方法对占位符内容进行解析,将占位符内容转换为ParameterMapping对象。

  private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {

    private final List<ParameterMapping> parameterMappings = new ArrayList<>();
    private final Class<?> parameterType;
    private final MetaObject metaParameters;

    public ParameterMappingTokenHandler(Configuration configuration
        , Class<?> parameterType,Map<String, Object> additionalParameters) {
      super(configuration);
      this.parameterType = parameterType;
      this.metaParameters = configuration.newMetaObject(additionalParameters);
    }

    @Override
    public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }

ParameterMapping对象用于描述MyBatis参数映射信息,便于后续根据参数映射信息获取对应的TypeHandler为PreparedStatement对象设置值。buildParameterMapping()方法解析参数占位符生成ParameterMapping对象的过程如下:

    private ParameterMapping buildParameterMapping(String content) {
      Map<String, String> propertiesMap = parseParameterMapping(content);
      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;
        }
      }
      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);
        }
      }
      if (typeHandlerAlias != null) {
        builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
      }
      return builder.build();
    }

    private Map<String, String> parseParameterMapping(String content) {
      try {
        return new ParameterExpression(content);
      } catch (BuilderException ex) {
        throw ex;
      } catch (Exception ex) {
        throw new BuilderException("Parsing error was found in mapping #{" + content
            + "}.  Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex);
      }
    }
  }

如上面的代码所示,在ParameterMappingTokenHandler类的buildParameterMapping()方法中首先将参数占位符内容转换为Map对象,例如参数占位符内容如下:#{userId,javaType=long,jdbcType=NUMERIC,typeHandler=MyTypeHandler}

将会转换成如下Map对象:

然后通过一系列的逻辑判断参数的类型(javaType属性值),具体逻辑读者可参考上面代码中的注释内容。最后通过建造者模式构建ParameterMapping对象。到此为止,动态SQL的解析已经全部完成。

我们首先来看${}参数占位符的解析过程。当动态SQL配置中存在${}参数占位符时,MyBatis会使用TextSqlNode对象描述对应的SQL节点,在调用TextSqlNode对象的apply()方法时会完成动态SQL的解析。也就是说,${}参数占位符的解析是在TextSqlNode类的apply()方法中完成的,下面是该方法的实现:

public class TextSqlNode implements SqlNode {
  private final String text;
  private final Pattern injectionFilter;

  @Override
  public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    context.appendSql(parser.parse(text));
    return true;
  }

如上面的代码所示,在TextSqlNode类的apply()方法中,首先调用createParser()方法创建一个GenericTokenParser对象,通过GenericTokenParser对象解析${}参数占位符,然后通过BindingTokenParser对象处理参数占位符的内容。createParser()内容如下:

  private GenericTokenParser createParser(TokenHandler handler) {
    return new GenericTokenParser("${", "}", handler);
  }

该方法返回一个GenericTokenParser对象,指定openToken属性为“${”,closeToken属性为“}”,TokenHandler为BindingTokenParser对象。GenericTokenParser解析时,遍历获取所有${}参数占位符的内容,然后调用BindingTokenParser对象的handleToken()方法对参数占位符内容进行替换。BindingTokenParser类的handleToken()方法实现如下:

private static class BindingTokenParser implements TokenHandler {

    private final DynamicContext context;
    private final Pattern injectionFilter;

    public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
      this.context = context;
      this.injectionFilter = injectionFilter;
    }

    @Override
    public String handleToken(String content) {
      // 获取Mybatis内置函数_parameter,_parameter属性中保存所有参数信息
      Object parameter = context.getBindings().get("_parameter");
      if (parameter == null) {
        context.getBindings().put("value", null);
      } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
        // 将参数对象添加到ContextMap对象中
        context.getBindings().put("value", parameter);
      }

      // 通过OGNL表达式获取参数值
      Object value = OgnlCache.getValue(content, context.getBindings());
      String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
      checkInjection(srtValue);

      // 返回参数值
      return srtValue;
    }

    private void checkInjection(String value) {
      if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
        throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
      }
    }
  }

如上面的代码所示,在BindingTokenParser类的handleToken()方法中,根据参数占位符名称获取对应的参数值,然后替换为对应的参数值。假设我们的SQL配置如下:

如果Mapper调用时传入的参数值如下:

 上面的Mapper调用将会抛出异常,原因是TextSqlNode类的apply()方法中解析${}参数占位符时,只是对参数占位符内容进行替换,将参数占位符替换为对应的参数值,因此SQL配置解析后的内容如下:

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Mybatis是一个轻量级的Java持久层开源框架,它封装了JDBC操作数据库的底层细节,提供了一个简单易用的数据库访问方式。 Mybatis源码分为核心模块和附加模块两部分,核心模块主要包括配置解析SQL解析SQL执行等功能,附加模块包括连接池、缓存、事务管理等功能。 在Mybatis源码中,配置解析是其中的关键部分。通过解析mybatis-config.xml配置文件,可以获取到数据库连接信息、映射器配置、插件配置等。在配置解析过程中,Mybatis会对配置文件进行校验,确保配置的正确性。 SQL解析Mybatis的另一个重要功能。Mybatis通过解析Mapper接口中的注解或XML配置文件中的SQL语句,将SQL语句解析为ParameterMapping、BoundSql等对象,并将其封装成一个MappedStatement对象,供后续的SQL执行使用。 SQL执行是Mybatis的核心功能之一。在SQL执行阶段,Mybatis会根据MappedStatement中的信息,获取数据库连接,并执行对应的SQL语句。在执行过程中,Mybatis会通过TypeHandler对参数进行类型转换,并使用ResultSetHandler将查询结果封装成Java对象。 除了核心模块,Mybatis源码还包括了连接池、缓存、事务管理等附加模块的实现。连接池模块负责管理数据库连接的获取和释放,缓存模块负责缓存查询结果以提高性能,而事务管理模块则负责管理数据库的事务处理。 总之,Mybatis源码解析涉及多个关键模块的实现,包括配置解析SQL解析SQL执行、连接池、缓存、事务管理等。通过了解这些模块的实现原理,我们可以更好地理解和使用Mybatis框架。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值