MyBatis 学习笔记(八)---源码分析篇--SQL 执行过程详细分析

前言

在面试中我们经常会被到MyBatis中 #{} 占位符与${}占位符的区别。大多数的小伙伴都可以脱口而出#{} 会对值进行转义,防止SQL注入。而${}则会原样输出传入值,不会对传入值做任何处理。本文将通过源码层面分析为啥#{} 可以防止SQL注入。

源码解析

首先我们来看看MyBatis 中SQL的解析过程,MyBatis 会将映射文件中的SQL拆分成一个个SQL分片段,然后在将这些分片段拼接起来。
例如:在映射文件中有如下SQL

 SELECT * FROM student
        <where>
            <if test="id!=null">
                id=${id}
            </if>
            <if test="name!=null">
                AND name =${name}
            </if>
        </where>

MyBatis 会将该SQL 拆分成如下几部分进行解析
第一部分 SELECT * FROM Author 由StaticTextSqlNode存储
第二部分 <where> 由WhereSqlNode 存储
第三部分 <if></if> 由IfSqlNode存储
第四部分 ${id} ${name} 占位符里的文本由TextSqlNode存储。

获取BoundSql

BoundSql 是用来存储一个完整的SQL 语句,存储参数映射列表以及运行时参数

public class BoundSql {

  /**
   * 一个完整的SQL语句,可能会包含问号?占位符
   */
  private String sql;
  /**
   * 参数映射列表,SQL中的每个#{xxx}
   * 占位符都会被解析成相应的ParameterMapping对象
   */
  private List<ParameterMapping> parameterMappings;
  /**
   * 运行时参数,即用户传入的参数,比如Article对象,
   * 或是其他的参数
   */
  private Object parameterObject;
    /**
   * 附加参数集合,用户存储一些额外的信息,比如databaseId等
   */
  private Map<String, Object> additionalParameters;
  /**
   * additionalParameters的元信息对象
   */
  private MetaObject metaParameters;
    .... 省略部分代码
  }

分析SQL的解析,首先从获取BoundSql说起。其代码源头在MappedStatement。

  public BoundSql getBoundSql(Object parameterObject) {
	//其实就是调用sqlSource.getBoundSql
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    //剩下的可以暂时忽略,故省略代码
    return boundSql;
  }

如上,可以看出其内部就是调用的sqlSource.getBoundSql。 而我们sqlSource 接口又有如下几个实现类。
DynamicSqlSource
RawSqlSource
StaticSqlSource
ProviderSqlSource
VelocitySqlSource
其中DynamicSqlSource 是对动态SQL进行解析,当SQL配置中包含${}或者<if>,<set> 等标签时,会被认定为是动态SQL,此时使用 DynamicSqlSource 存储 SQL 片段,而RawSqlSource 是对原始的SQL 进行解析,而StaticSqlSource 是对静态SQL进行解析。这里我们重点介绍下DynamicSqlSource。话不多说,直接看源码。

  public BoundSql getBoundSql(Object parameterObject) {
    //生成一个动态上下文
    DynamicContext context = new DynamicContext(configuration, parameterObject);
	//这里SqlNode.apply只是将${}这种参数替换掉,并没有替换#{}这种参数
    rootSqlNode.apply(context);
	//调用SqlSourceBuilder
    SqlSourceBuilder sqlSourceParser =  new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
	//SqlSourceBuilder.parse,注意这里返回的是StaticSqlSource,解析完了就把那些参数都替换成?了,也就是最基本的JDBC的SQL写法
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
	//看似是又去递归调用SqlSource.getBoundSql,其实因为是StaticSqlSource,所以没问题,不是递归调用
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
//  将DynamicContext的ContextMap中的内容拷贝到BoundSql中
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }

如上,该方法主要有如下几个过程:

  1. 生成一个动态上下文
  2. 解析SQL片段,替换${}类型的参数
  3. 解析SQL语句,并将参数都替换成?
  4. 调用StaticSqlSource的getBoundSql获取BoundSql
  5. 将DynamicContext的ContextMap中的内容拷贝到BoundSql中。
    下面通过两个单元测试用例理解下。
  @Test
  public void shouldMapNullStringsToNotEmptyStrings() {
    final String expected = "id=${id}";
    final MixedSqlNode sqlNode = mixedContents(new TextSqlNode(expected));
    final DynamicSqlSource source = new DynamicSqlSource(new Configuration(), sqlNode);
    String sql = source.getBoundSql(new Bean("12")).getSql();
    Assert.assertEquals("id=12", sql);
  }
 
    @Test
  public void shouldMapNullStringsToJINHAOEmptyStrings() {
    final String expected = "id=#{id}";
    final MixedSqlNode sqlNode = mixedContents(new TextSqlNode(expected));
    final DynamicSqlSource source = new DynamicSqlSource(new Configuration(), sqlNode);
    String sql = source.getBoundSql(new Bean("12")).getSql();
    Assert.assertEquals("id=?", sql);
  }

如上,${} 占位符经过DynamicSqlSource的getBoundSql 方法之后直接替换成立用户传入值,而#{} 占位符则仅仅只是只会被替换成?号,不会被设值。

DynamicContext

DynamicContext 是SQL语句的上下文,每个SQL片段解析完成之后会存入DynamicContext中。让我们来看看DynamicContext的相关代码。

  public DynamicContext(Configuration configuration, Object parameterObject) {
	//绝大多数调用的地方parameterObject为null
    if (parameterObject != null && !(parameterObject instanceof Map)) {
      //如果不是map型
      MetaObject metaObject = configuration.newMetaObject(parameterObject);
      bindings = new ContextMap(metaObject);
    } else {
      bindings = new ContextMap(null);
    }
	//存储额外信息,如databaseId
    bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
  }

如上,在DynamicContext的构造函数中,根据传入的参数对象是否是Map类型,有两个不同构造ContextMap的方式,而ContextMap作为一个继承了HashMap的对象,作用就是用于统一参数的访问方式:用Map接口方法来访问数据。具体磊说,当传入的参数对象不是Map类型时,MyBatis会将传入的POJO对象用MetaObject 对象来封装,当动态计算sql过程需要获取数据时,用Map 接口的get方法包装 MetaObject对象的取值过程。


。。。。。。。。。。。。。。。。。


版权原因,完整文章,请参考如下:

MyBatis 学习笔记(八)---源码分析篇--SQL 执行过程详细分析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值