MyBatis源码研究之$和#

没有什么新奇的东西.

1. 符号 $
  1. TextSqlNode 类中进行了解析.
  2. 具体逻辑参见其内部类 BindingTokenParser .

BindingTokenParser 类中我们可以发现这样的细节:

  • BindingTokenParser 内部有一个injectionFilter字段, 其值就是通过自身的构造函数, 从外部类TextSqlNode 的同名字段中复制过来的. 而BindingTokenParser 内部这个Pattern类型的injectionFilter字段, 其作用就是对${ }进行解析之后的结果进行判断, 判断这个解析出来结果是否合法? 不过可惜的是没有看到对其的应用. Mybatis也没有提供相应的接口. 不过Mybatis完全具备了提供这个功能的前提(生成TextSqlNode 实例的位置都能直接获取到configuration对象, 这使得复用配置信息变得非常简单).

    // 这个value就是对${ }进行解析之后的结果
    private void checkInjection(String value) {
    // 如果解析出来的值不满足过滤条件, 则抛出异常
     if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
       throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
     }
    }
    
  • 当我们向Mybatis的CRUD操作中传入的第二参数如果为简单类型(由Mybatis中定义的SimpleTypeRegistry.isSimpleType 决定), 或者第二个参数根本没有传入, 甚至显式传入null时 , Mybatis就会在这里(TextSqlNode.BindingTokenParser.handleToken)进行检测, 最终将其构建为形如 {value : someVal} 的键值对. 所以以后我们在执行Mybatis的映射SQL时, 可以使用 ${value} 引用传入的单个值.

    // ------------------------ 使用
    sqlSession.select(sqlId,"1");
    
    // 使用 ${value} 引用上的 '1'
    SELECT * FROM xxTable WHERE field1 = '${value}'
    
    // ------------------------ 相关源码
    // TextSqlNode.BindingTokenParser类
    private static class BindingTokenParser implements TokenHandler {
    
    	    private DynamicContext context;
    
    	    public BindingTokenParser(DynamicContext context) {
    	      this.context = context;
    	    }
    
    	    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());
    	      return (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
    	    }
    	  }
    

2. 符号 #

  1. SqlSourceBuilder 类中进行了解析.
  2. 具体逻辑参见其内部类 ParameterMappingTokenHandler .
  3. 每个#{ } 将被解析为一个 ParameterMapping
  4. 内部类 ParameterMappingTokenHandler 覆写的handleToken 方法负责完成将我们编写 #{xx} 转换为 ?
  5. 还有一个细节就是 虽然在xml映射文件编写时不再推荐使用ParameterMapping. 但在ParameterMappingTokenHandler 的实现里, 依然会将每个 #{ } 里的内容解析为一个ParameterMapping 实例. 在一段SQL中的多个 #{ }会按顺序解析之后存储到一个List中. 所以 #{ }出现的顺序等于这个List中元素的顺序.
  6. 这里补充一句: 每个ParameterMapping 实例真正被使用的位置则是位于ParameterHandler 接口唯一的实现DefaultParameterHandler 中的setParameters方法中. 所以我们在使用JDBC编程时进行的PreparedStatement.setXXX 操作是在每个 TypeHandler<T> 接口的实现类中完成的.
  7. 再多补充一句, 我们可以看到TypeHandler<T> 的每个实现类都是可以拿到全局配置configuration实例的. 这使得以后的扩展变得非常轻松。
  8. 下方的 《补充2》。

3. 补充1(2017/10/25)

偶然想起之前看过的一篇文章MyBatis 执行动态 SQL (题外话, 此作者的系列文章值得一看, 可以看出其对Mybatis的研究相当深入, 都出版了相应的纸质书籍.) 这篇文章里有这么一段

// ---------------------- java代码
Map map = new HashMap();
//这里的 sql 对应 XML 中的 ${sql}
map.put("sql", "select * from sysuser "
        + " where enabled = #{enabled} "
        + " and userName like concat('%',#{userName},'%')");
//#{enabled}
map.put("enabled", 1);
//#{userName}
map.put("userName", "admin");

// ---------------- 相应的XML映射内容
<select id="executeSql" resultType="map">
    ${sql}
</select>

这里就有一个疑问了: “为什么这段Mybatis映射SQL是可以正常运行的?”

  1. 按照Mybatis的默认配置, 在 XMLStatementBuilder 负责使用默认的 XMLLanguageDriver 来将我们的每个<select/>, <update/>,<insert/>,<delete/> 解析为一个 DynamicSqlSource( 以接口SqlSource 的形式进行暴露 ).

  2. 最终从SqlSource 中获取BoundSql时, 在接口SqlSource的实现类DynamicSqlSource中的逻辑是这样的 :

    public BoundSql getBoundSql(Object parameterObject) {
        DynamicContext context = new DynamicContext(configuration, parameterObject);
        // 以下这句负责解析 ${ }
        // 这里就会将上面的 ${sql} 使用指定的Context替换掉, 这样就会还只剩下 #{ } 包裹的需要替换.
        rootSqlNode.apply(context);
        // SqlSourceBuilder就是用来处理 #{ } 里的替换操作的.
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
        // 这里, context.getSql()得到的应该是这样的形式:
        // select * from sysuser where enabled = #{enabled} and userName like concat('%',#{userName},'%')
        // 另外注意这里的返回值也是SqlSource, 而本方法所在的类也是继承自 SqlSource的. 所以本类DynamicSqlSource类似于一个调度者的角色.并不负责实际的解析工作.
        SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
          boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
        }
        // 到了这里, #{ }, ${ } 就算是全部解析完毕了.
        return boundSql;
    }
    

所以以上疑问的解答就是, 因为一段SQL映射语句的解析中, ${ }#{ }的解析是有先后顺序的. ${ }的解析在先, 在完成对 ${ }的解析之后, 再对前面解析得到的结果进行第二次专门针对 #{ } 的解析动作.

4. 补充2(2018/04/26)

最近突然被问及这样一个问题:“ 在XML映射配置中, parameterType设置为基本类型时,映射SQL语句中, #{ }中的属性名应该如何填写? ”

经过一番探索,最终在DefaultParameterHandler类的setParameters方法中找到答案。(至于 “${ }中的属性名应该如何填写?”, 上面关于 $ 的讲解中已经解答过了。)

// DefaultParameterHandler.setParameters
public void setParameters(PreparedStatement ps) throws SQLException {
  ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings != null) {
    MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject);
    for (int i = 0; i < parameterMappings.size(); i++) {
      ParameterMapping parameterMapping = parameterMappings.get(i);
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
       // 如果传入的参数是基本类型, 则会进入这条分支 
       // 因为在TypeHandlerRegistry类型的构造函数中, Mybatis默认进行了一系列基本类型的注册
       // 逻辑是这样的, Mybatis发现外界对该类型有自定义的处理, 则会将这个值不进行任何处理得交给typeHandler的回调方法。
       // 也就是基本类型的时候, 准确点说是定义了typehandler时候, 你在#{ }随便写啥都可以.
          value = parameterObject;
        } else {
          value = metaObject == null ? null : metaObject.getValue(propertyName);
        }
        TypeHandler typeHandler = parameterMapping.getTypeHandler();
        JdbcType jdbcType = parameterMapping.getJdbcType();
        if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull();
        // 如果本次传入的基本类型是String, 则该typeHandler实际类型为内置的StringTypeHandler
        typeHandler.setParameter(ps, i + 1, value, jdbcType);
      }
    }
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值