没有什么新奇的东西.
1. 符号 $
- 在
TextSqlNode
类中进行了解析. - 具体逻辑参见其内部类
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. 符号 #
- 在
SqlSourceBuilder
类中进行了解析. - 具体逻辑参见其内部类
ParameterMappingTokenHandler
. - 每个
#{ }
将被解析为一个ParameterMapping
。 - 内部类
ParameterMappingTokenHandler
覆写的handleToken
方法负责完成将我们编写 #{xx} 转换为 ? - 还有一个细节就是 虽然在xml映射文件编写时不再推荐使用
ParameterMapping
. 但在ParameterMappingTokenHandler
的实现里, 依然会将每个 #{ } 里的内容解析为一个ParameterMapping
实例. 在一段SQL中的多个#{ }
会按顺序解析之后存储到一个List中. 所以 #{ }出现的顺序等于这个List中元素的顺序. - 这里补充一句: 每个
ParameterMapping
实例真正被使用的位置则是位于ParameterHandler
接口唯一的实现DefaultParameterHandler
中的setParameters
方法中. 所以我们在使用JDBC编程时进行的PreparedStatement.setXXX
操作是在每个TypeHandler<T>
接口的实现类中完成的. - 再多补充一句, 我们可以看到
TypeHandler<T>
的每个实现类都是可以拿到全局配置configuration实例的. 这使得以后的扩展变得非常轻松。 - 下方的 《补充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是可以正常运行的?”
-
按照Mybatis的默认配置, 在
XMLStatementBuilder
负责使用默认的XMLLanguageDriver
来将我们的每个<select/>
,<update/>
,<insert/>
,<delete/>
解析为一个DynamicSqlSource
( 以接口SqlSource
的形式进行暴露 ). -
最终从
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);
}
}
}
}