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 接口的实现类中完成的.
  7. 再多补充一句, 我们可以看到TypeHandler 的每个实现类都是可以拿到全局配置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 来将我们的每个