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

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

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

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有五个实现类:ProviderSqlSource,DynamicSqlSource,RawSqlSource,StaticSqlSource,StaticSqlSource 其中比较常用的就是DynamicSqlSource,RawSqlSource和StaticSqlSource。如果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);

}

SqlSource继承关系

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的实现。

rootSqlNode实现

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去做了。

SqlSource结构

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还是被解析为?。

BoundSql对象

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

总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容