MyBatis原理系列(六)-手把手带你了解BoundSql的创建过程

2,150 阅读3分钟

这篇文章我们将继续探讨MyBatis的BoundSql对象。

系列文章

MyBatis原理系列(一)-手把手带你阅读MyBatis源码
MyBatis原理系列(二)-手把手带你了解MyBatis的启动流程
MyBatis原理系列(三)-手把手带你了解SqlSession,SqlSessionFactory,SqlSessionFactoryBuilder的关系
MyBatis原理系列(四)-手把手带你了解MyBatis的Executor执行器
MyBatis原理系列(五)-手把手带你了解Statement、StatementHandler、MappedStatement间的关系
MyBatis原理系列(六)-手把手带你了解BoundSql的创建过程
MyBatis原理系列(七)-手把手带你了解如何自定义插件
MyBatis原理系列(八)-手把手带你了解一级缓存和二级缓存
MyBatis原理系列(九)-手把手带你了解MyBatis事务管理机制

1. BoundSql 初识

在前几篇文章中我们从启动流程到SqlSession再到Executor执行器,以及在执行数据库操作的时候都会通过StatementHandler进行Statement处理,并且会用到BoundSql,这个BoundSql到底是何作用,我们将在此篇文章中讲解下去。

BoundSql 中就是对解析后的sql描述,包括对动态标签的解析,并且将 #{} 解析为占位符 ? ,还包含参数的描述信息。这个类没有什么复杂操作,可以看作是解析后的sql描述对象。

/**
 * An actual SQL String got from an {@link SqlSource} after having processed any dynamic content.
 * The SQL may have SQL placeholders "?" and an list (ordered) of an 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...).
 *
 * 经过处理一些动态sql的部分获取到的真实sql,这个sql可能还有占位符?和一个参数映射的有序集合,并且还有每个参数的额外信息
 * @author Clinton Begin
 */
public class BoundSql {

  // 最终解析的sql,Mybatis将#{}和${}解析后的sql,其中#{}会被解析为?
  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);
  }
}

2. BoundSql 创建流程

BaseExecutor的query方法为例,首先会从MappedStatement对象中获取BoundSql对象,而MappedStatement对象我们在上篇文章中讲过其实是sql标签的描述,BoundSql就是解析后的sql语句。

 // BaseExecutor 的query()方法
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 从MappedStatement对象中获取BoundSql对象
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }

获取BoundSql对象其实是从MappedStatement的成员变量sqlSource中获取的。 而SqlSource作为一个接口,它只有一个作用就是获取BoundSql对象。

// MappedStatement 的 getBoundSql() 方法
public BoundSql getBoundSql(Object parameterObject) {
    // 获取BoundSql对象,BoundSql对象是对动态sql的解析
    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;
  }

SqlSource 接口只有一个方法,就是获取BoundSql对象,SqlSource接口的设计满足单一职责原则。SqlSource有五个实现类:ProviderSqlSourceDynamicSqlSourceRawSqlSourceStaticSqlSourceStaticSqlSource 其中比较常用的就是DynamicSqlSourceRawSqlSourceStaticSqlSource。如果sql中只包含#{}参数,不包含${}或者其它动态标签,那么创建SqlSource对象时则会创建RawSqlSource,否则创建DynamicSqlSource对象。

/**
 * 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.
 * 表示一个从xml文件和注解中读取的一个映射语句
 * 可以创建一个包含用户输入的参数的,并可以发送到数据库sql
 * @author Clinton Begin
 */
public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

image.png

DynamicSqlSource 会优先解析{}标签,然后解析#{}标签。其中{}会被解析为参数内容,不会加上双引号,而#{}会被解析为?,并且参数会加上双引号。

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);
    rootSqlNode.apply(context);
    // 创建sqlSource对象,并且解析#{}为?
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

}

rootSqlNode有很多实现,我们看下TextSqlNode的实现。

image.png

TextSqlNode中对${}标签进行了解析,没有加上双引号。

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

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

  private static class BindingTokenParser implements TokenHandler {

    private DynamicContext context;
    private Pattern injectionFilter;

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

    @Override
    public String handleToken(String content) {
      Object parameter = context.getBindings().get("_parameter");
      if (parameter == null) {
        context.getBindings().put("value", null);
      } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
        context.getBindings().put("value", parameter);
      }
      Object value = OgnlCache.getValue(content, context.getBindings());
      String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
      checkInjection(srtValue);
      return srtValue;
    }

然后 sqlSourceParser.parse 解析的是#{}标签,实现如下会将#{}解析为?,具体实现就不看了,有点复杂。

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

综上,通过SqlSource接口获取BoundSql对象的时候,会解析动态sql。

3. 例子

3. 1 例1

我们以前面几篇文章的例子,在mapper/TTestUserMapper.xml中配置了以下sql标签

<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
    select 
    <include refid="Base_Column_List" />
    from t_test_user
    where id = #{id,jdbcType=BIGINT}
  </select>

通过打断点,我们看到#{}标签被解析为?占位符,参数的设置就交DBMS去做了。

image.png

ParameterMapping 的结构如下,是对参数的描述信息。

ParameterMapping{property='id', mode=IN, javaType=class java.lang.Long, jdbcType=BIGINT, numericScale=null, resultMapId='null', jdbcTypeName='null', expression='null'}
3. 2 例2

mapper/TTestUserMapper.xml中配置了以下sql标签,其中字段名为动态传入的,使用${}标签进行包裹。

  <select id="selectDynamicField" resultMap="BaseResultMap">
    select
    ${fieldName}
    from t_test_user
    where id = #{id,jdbcType=BIGINT}
  </select>

com/example/demo/dao/TTestUserMapper.java 下新增以下接口

TTestUser selectDynamicField(@Param("fieldName") String fieldName, @Param("id") Long id);

修改测试代码

public static void main(String[] args) {
        try {
            // 1. 读取配置
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            // 2. 创建SqlSessionFactory工厂
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. 获取sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
            // 4. 获取Mapper
            TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
            // 5. 执行接口方法
            TTestUser userInfo = userMapper.selectDynamicField("real_name", 12L);
            System.out.println("userInfo = " + JSONUtil.toJsonStr(userInfo));
            // 6. 提交事物
            sqlSession.commit();
            // 7. 关闭资源
            sqlSession.close();
            inputStream.close();
        } catch (Exception e){
            log.error(e.getMessage(), e);
        }
    }

调用此接口查询后,可发现fieldName直接被替换为参数,而id还是被解析为?。

image.png

可见${}安全隐患比较大,而#{}可以有效防御sql注入。

4. 总结

通过这篇文章,我们了解SqlSource的几个默认实现,以及{}和#{}的区别,能用#{}时,尽量不要使用{}。我们还了解了BoundSql的结构和创建过程。对此我们对MyBatis的sql执行有了更深层次的了解。