跳到主要内容
  1. 所有文章/
  2. 《Mybatis3源码深度解析》笔记/

Mybatis源码-动态SQL实现

·📄 9293 字·🍵 19 分钟

动态SQL指的是事先无法预知具体的条件,需要在运行时根据具体的情况动态地生成SQL语句。假设我们有一个获取用户信息查询操作,具体的查询条件是不确定的,取决于Web前端表单提交的数据,可能根据用户的Id进行查询,也可能根据用户手机号或姓名进行查询,还有可能是这几个条件的组合。

这个时候就需要使用MyBatis的动态SQL特性了。

<select id="getUserByEntity"  resultType="User">
    select
    <include refid="userAllField"/>
    from user
    <where>
        <if test="id != null">
            AND id = #{id}
        </if>
        <if test="name != null">
            AND name = #{name}
        </if>
        <if test="phone != null">
            AND phone = #{phone}
        </if>
    </where>
</select>

除了<if>还有<choose/when/otherwise>,<foreach>

<trimIset>:这两个标签的作用和<where>标签的作用类似,用于WHERE子句中因为不同的条件成立时导致AND或OR关键字多余,或者SET子句中出现多余的逗号问题。

SqlSource与BoundSql #

MyBatis 中和 SQL 语句有关的两个组件,即 SqlSource BoundSql

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

SqlSource就代表Java注解或者XML文件配置的 SQL资源。下面是SqlSource接口的定义:

public interface SqlSource {
  BoundSql getBoundSql(Object parameterObject);
}

这4种SqlSource实现类的作用如下。

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

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

public class StaticSqlSource implements SqlSource {
  // Mapper解析后的sql内容
  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);
  }

}

BoundSql是对Executor组件执行SQL信息的封装,具体实现代码如下:

public class BoundSql {

  // Mapper配置解析后的sql语句
  private final String sql;
  // Mapper参数映射信息
  private final List<ParameterMapping> parameterMappings;
  // Mapper参数对象
  private final Object parameterObject;
  // 额外参数信息,包括<bind>标签绑定的参数,内置参数
  private final Map<String, Object> additionalParameters;
  // 参数对象对应的MetaObject对象
  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<String, Object>();
    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配置中通过databaseld属性指定的数据库类型。

LanuageDriver #

怎么生成的BoundSql和SqlSource的?通过这个类LanuageDriver。SQL配置信息到SqlSource对象的转换是由LanguageDriver组件来完成的

public interface LanguageDriver {

  ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);

  SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);

  SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);

}

MyBatis 中为LanguageDriver 接口提供了两个实现类,分别为XMLLanguageDriver 和RawLanguageDriver。

  1. XMLLanguageDriver为XML语言驱动,为MyBatis提供了通过XML标签(我们常用的<if>、<where>等标签)结合OGNL表达式语法实现动态SQL的功能。
  2. 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);
  }
  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    // 该方法用于解析XML文件中配置的SQL信息
    // 创建XMLScriptBuilder对象
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    // 调用 XMLScriptBuilder对象parseScriptNode()方法解析SQL资源
    return builder.parseScriptNode();
  }

  @Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    // 该方法用于解析Java注解中配置的SQL信息
    // 字符串以<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类的createSqlSource()方法的实现来看,我们除了可以通过XML配置文件结合OGNL表达式配置动态SQL外,还可以通过Java注解的方式配置,只需要注解中的内容加上<script>标签。下面是使用Java注解配置动态SQL的案例代码:

@Select("<script>" +
            "select * from user\n" +
            "<where>\n" +
            "    <if test=\"name != null\">\n" +
            "        AND name = #{name}\n" +
            "    </if>\n" +
            "    <if test=\"phone != null\">\n" +
            "        AND phone = #{phone}\n" +
            "    </if>\n" +
            "</where>" +
            "</script>")
    UserEntity getUserByPhoneAndName(@Param("phone") String phone, @Param("name") String name);

自定义脚本驱动 #

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

<dependency>
    <groupId>org.mybatis.scripting</groupId>
    <artifactId>mybatis-velocity</artifactId>
    <version>2.0-SNAPSHOT</version>
</dependency>
<typeAliases>
  <typeAlias alias="velocityDriver" type="org.mybatis.scripting.velocity.Driver"/>
  <typeAlias alias="User" type="com.blog4java.mybatis.example.entity.UserEntity"/>
</typeAliases>
<select id="getUserByNames" lang="velocityDriver" resultType="User">
    select * from user
    #where()
      #in( $_parameter.names $name "name" )
        @{name}
      #end
    #end
</select>

需要注意的是,在配置Mapper时,需要通过 lang属性指定velocity-scripting模块中定义LanguageDriver的别名。

velocity-scripting文档

velocity-scripting模块

SqlNode详解 #

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

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

每种标签对应不同的实现类其中:

  • MixedSqINode:用于描述一组SqlNode对象,通常一个Mapper配置是由多个SqlNode对象组成的,这些SqINode对象通过MixedSqlNode进行关联,组成一个完整的动态SQL配置。
  • StaticTextSqINode:用于描述动态SQL中的静态文本内容
  • TextSqINode:该类与StaticTextSqINode类不同的是,当静态文本中包含${}占位符时,说明${}需要在Mapper调用时将${}替换为具体的参数值。因此,使用TextSqINode类来描述。
  • VarDecISqINode:用于描述动态SQL中的<bind>标签,动态SQL解析时,会把<bind>标签配置信息转换为VarDeclSqINode对象。

SqINode与动态SQL配置之间的对应关系。

SqINode对象创建完毕后,我们就可以调用MixedSqINode的apply()方法根据参数内容动态地生成SQL内容了。该方法接收一个DynamicContext对象作为参数,DynamicContext对象中封装了Mapper 调用时的参数信息。

上面的代码中,我们创建了一个DynamicContext,然后调用MixedSqlNode对象的apply()方法,动态SQL的解析结果封装在DynamicContext对象中,我们只需要调用DynamicContext对象的getSql()方法即可获取动态SQL解析后的SQL语句。运行上面这段代码后,生成的SQL内容如下:select * from user where 1=l AND id= #{id}

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

public class MixedSqlNode implements SqlNode {
  // 通过一个List对象维护所有的SqINode对象
  private final List<SqlNode> contents;

  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }
  // MixedSqlNode 类的apply()方法中对所有SqINode对象进行遍历
  @Override
  public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : contents) {
      sqlNode.apply(context);
    }
    return true;
  }
}

//StaticTextSqINode的解析
public class StaticTextSqlNode implements SqlNode {
  // 静态SQL文本内容
  private final String text;
  public StaticTextSqlNode(String text) {
    this.text = text;
  }
  @Override
  public boolean apply(DynamicContext context) {
    // 追加SQL内容
    context.appendSql(text);
    return true;
  }
}

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

}

动态SQL的解析过程 #

从XML解析配置追踪:

// org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement
// 解析mapper标签的配置

// org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
// 开始mapper的每个部分

private void configurationElement(XNode context) {
  try {
    // 获取命名空间
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    // 设置当前正在解析的Mapper配置的命名空间
    builderAssistant.setCurrentNamespace(namespace);
    // 解析<cache-ref>标签
    cacheRefElement(context.evalNode("cache-ref"));
    // 解析<cache>标签
    cacheElement(context.evalNode("cache"));
    // 解析所有的<parameterMap>标签
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    // 解析所有的<resultMap>标签
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    // 解析所有的<sql>标签
    sqlElement(context.evalNodes("/mapper/sql"));
    // 解析所有的<select|insert|update|delete>标签
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
  }
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    // 通过XMLStatementBuilder对象,对<select|update|insert|delete>标签进行解析
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      // 调用parseStatementNode()方法解析
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

public void parseStatementNode() {
 // ...
  // 获取LanguageDriver对象
  String lang = context.getStringAttribute("lang");
  LanguageDriver langDriver = getLanguageDriver(lang);  
  // 通过LanguageDriver解析SQL内容,生成SqlSource对象
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  // ...
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered, 
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

// 调用的是 org.apache.ibatis.scripting.xmltags.XMLLanguageDriver#createSqlSource(org.apache.ibatis.session.Configuration, org.apache.ibatis.parsing.XNode, java.lang.Class<?>)
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  // 该方法用于解析XML文件中配置的SQL信息
  // 创建XMLScriptBuilder对象
  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  // 调用 XMLScriptBuilder对象parseScriptNode()方法解析SQL资源
  return builder.parseScriptNode();
}

 public SqlSource parseScriptNode() {
  // 调用parseDynamicTags()方法將SQL配置转换为SqlNode对象
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource = null;
  // 判断Mapper SQL配置中是否包含动态SQL元素,如果是创建DynamicSqlSource对象,否则创建RawSqlSource对象
  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<SqlNode>();
    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) {
        // 如果子元素为<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文本,则使用TextSqINode对象描述SQL节点信息,若SQL节点中存在${}参数占位符,则设置XMLScriptBuilder对象的isDynamic属性值为true;

如果子元素为等标签,则使用对应的NodeHandler处理。XMLScriptBuilder类中定义了一个私有的NodeHandler接口 ,并为每种动态SQL标签提供了一个NodeHandler接口的实现类,通过实现类处理对应的动态SQL标签,把动态SQL标签转换为对应的SqINode对象。

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

SqINode对象是如何根据调用Mapper时传入的参数动态生成SQL语句的呢?

  1. 动态SQL标签解析完成后,将解析后生成的SqlINode对象封装在 SqlSource对象中。

  2. MyBatis中的MappedStatement用于描述Mapper中的 SQL配置,SqlSource创建完毕后,最终会存放在MappedStatement对象的sqlSource属性中。

  3. Executor组件操作数据库时,会调用MappedStatement对象的getBoundSql()方法获取BoundSql对象,代码如下:

    // org.apache.ibatis.mapping.MappedStatement#getBoundSql
    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;
    }
    // org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql
    public BoundSql getBoundSql(Object parameterObject) {
      // 通过参数对象,创建动态SQL上下文对象
      DynamicContext context = new DynamicContext(configuration, parameterObject);
      // 以DynamicContext对象作为参数调用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对象中
      for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
        boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
      }
      // 最终生成的SQL语句
      return boundSql;
    }
    
    
    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);
      // 调用GenericTokenParser对象的parse()方法將#{}参数占位符转换为?
      String sql = parser.parse(originalSql);
      return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
    }
    
    
    // GenericTokenParser对象的parse()
    public String parse(String text) {
      if (text == null || text.isEmpty()) {
        return "";
      }
      // 获取第一个openToken在SQL中的位置
      int start = text.indexOf(openToken, 0);
      // start为-1说明SQL中不存在任何参数占位符
      if (start == -1) {
        return text;
      }
      // 將SQL转换为char数组
      char[] src = text.toCharArray();
      // offset用于记录已解析的#{或者}的偏移量,避免重复解析
      int offset = 0;
    
      final StringBuilder builder = new StringBuilder();
      // expression为参数占位符中的内容
      StringBuilder expression = null;
      // 遍历获取所有参数占位符的内容,然后调用TokenHandler的handleToken()方法替换参数占位符
      while (start > -1) {
        if (start > 0 && src[start - 1] == '\\') {
          // this open token is escaped. remove the backslash and continue.
          builder.append(src, offset, start - offset - 1).append(openToken);
          offset = start + openToken.length();
        } else {
          // found open token. let's search close token.
          if (expression == null) {
            expression = new StringBuilder();
          } else {
            expression.setLength(0);
          }
          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] == '\\') {
              // this close token is escaped. remove the backslash and continue.
              expression.append(src, offset, end - offset - 1).append(closeToken);
              offset = end + closeToken.length();
              end = text.indexOf(closeToken, offset);
            } else {
              expression.append(src, offset, end - offset);
              offset = end + closeToken.length();
              break;
            }
          }
          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);
      }
      if (offset < src.length) {
        builder.append(src, offset, src.length - offset);
      }
    
      return builder.toString();
    }
    

    在GenericTokenParser的parse()方法中,对SQL配中的所有#{}参数占位符进行解析,获取参数占位符的内容,然后调用ParameterMappingTokenHandler的handleToken()方法对参数占位符内容进行替换。

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

    从上面的代码可以看出,SQL配置中的所有#{}参数占位符内容都被替换成了“?”字符,MyBatis默认情况下会使用PreparedStatement对象与数据库进行交互,因此#{}参数占位符内容被替换成了问号,然后调用PreparedStatement对象的setXXX()方法为参数占位符设置值。

    除此之外,handleToken()方法中还做了另一件事情,就是调用buildParameterMapping()方法对占位符内容进行解析,将占位符内容转换为ParameterMapping对象。

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

    private ParameterMapping buildParameterMapping(String content) {
      // 將#{}占位符内容转换为Map对象
      Map<String, String> propertiesMap = parseParameterMapping(content);
      // property 对应的值为参数占位符名称,例如userId
      String property = propertiesMap.get("property");
      // 推断参数类型
      Class<?> propertyType;
      // 如果内置参数,或<bind>标签绑定的参数包含该属性,则参数类型为Getter方法返回值类型
      if (metaParameters.hasGetter(property)) {
        propertyType = metaParameters.getGetterType(property);
        // 判读该参数类型是否注册了TypeHandler,如果注册了则使用参数类型
      } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
        propertyType = parameterType;
        // 如果指定了jdbcType属性,并且为CURSOR类型,则使用ResultSet类型
      } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
        propertyType = java.sql.ResultSet.class;
        // 如果参数类型为Map接口的子类型,则使用Object类型
      } else if (property == null || Map.class.isAssignableFrom(parameterType)) {
        propertyType = Object.class;
      } else {
        // 获取parameterType对应的MetaClass对象,方便获取参数类型的反射信息
        MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
        // 如果参数类型中包含该属性,则使用Getter方法返回类型
        if (metaClass.hasGetter(property)) {
          propertyType = metaClass.getGetterType(property);
        } else {
          propertyType = Object.class;
        }
      }
      // 使用建造者模式构建ParameterMapping对象
      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();
        // 指定ParameterMapping对象的属性
        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 " + parameterProperties);
        }
      }
      if (typeHandlerAlias != null) {
        builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
      }
      // 返回ParameterMapping对象
      return builder.build();
    }
    

    在ParameterMappingTokenHandler类的buildParameterMapping()方法中首先将参数占位符内容转换为Map对象,然后构建者模式创建对象。

#{}和${}的区别 #

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

@Override
public boolean apply(DynamicContext context) {
  // 通过GenericTokenParser对象解析${}参数占位符,使用BindingTokenParser对象处理参数占位符内容
  GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
  // 调用GenericTokenParser对象的parse()方法解析
  context.appendSql(parser.parse(text));
  return true;
}

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

public class GenericTokenParser {

  private final String openToken;
  private final String closeToken;
  private final TokenHandler handler;

  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 "";
    }
    // 获取第一个openToken在SQL中的位置
    int start = text.indexOf(openToken, 0);
    // start为-1说明SQL中不存在任何参数占位符
    if (start == -1) {
      return text;
    }
    // 將SQL转换为char数组
    char[] src = text.toCharArray();
    // offset用于记录已解析的#{或者}的偏移量,避免重复解析
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    // expression为参数占位符中的内容
    StringBuilder expression = null;
    // 遍历获取所有参数占位符的内容,然后调用TokenHandler的handleToken()方法替换参数占位符
    while (start > -1) {
      if (start > 0 && src[start - 1] == '\\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        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] == '\\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            offset = end + closeToken.length();
            break;
          }
        }
        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);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }

    return builder.toString();
  }
}

上面代码的核心内容是遍历获取所有SO参数占位符的内容,然后调用BindingTokenParser对象的handleToken()方法对参数占位符内容进行替换。BindingTokenParser类的handleToken()方法实现如下:

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

上面的内容是一条不合法的SQL语句,因此会执行失败,如果希望${}参数占位符解析后生成正确的SQL语句,则可以在参数内容前后加上一个单引号,具体如下:

public void testGetUserByName() {
    String userName = "'Test4'";
    UserEntity userEntity = userMapper.getUserByName(userName);
    System.out.println(userEntity);
}

最后我们再来总结一下#{}和${}参数占位符的区别。使用#{}参数占位符时,占位符内容会被替换成"?",然后通过PreparedStatement对象的setXXX()方法为参数占位符设置值;而${}参数占位符内容会被直接替换为参数值。使用#{}参数占位符能够有效避免SQL注入问题,所以我们可以优先考虑使用#占位符,当#{}参数占位符无法满足需求时,才考虑使用${}参数占位符。