源码解析之 Mybatis 对 Integer 参数做了什么手脚?
title: 源码解析之 Mybatis 对 Integer 参数做了什么手脚?
date: 2021-03-11
updated: 2021-03-11
categories:
- Mybatis
- 源码解析
tags: - Mybatis
- 源码解析
解决方案放在第二节,急需解决问题,可直接查看解决方案。
本文为深度长文,请耐心阅读!
问题描述
在 Mybatis 中,Integer 的入参为 0 时,发现判断条件的非空判断没有生效,原本应该存在的判断条件丢失了。
那么,Mybatis 到底对 Integer 参数做了什么手脚呢?下面我们来举例说明:
环境示例:
该问题只与 Mybatis 的实现机制有关,与版本基本无关(如果说相关性,可能只与源码中实现代码所在的行数有关)。
不过,为了养成良好的习惯,还是稍微提一下,我使用的 Mybatis 版本是 3.5.2。
接口示例:
@GetMapping("/queryByAgeGroup")
public HttpStatus queryByAgeGroup(@RequestParams("ageGroup") Integer ageGroup) {
// ageGroup 年龄段:0 代表幼儿,1 代表青年,2 代表中年,3 代表老年,-1 代表未知
IndexTestService.queryByAgeGroup(ageGroup);
return HttpStatus.HTTP_OK;
}
测试用例属于参数透传,没有业务逻辑,故省略 Service 和 Dao 层。
查询 SQL 示例:
<select id="queryByAgeGroup" text="">
select *
from `people_info`
where 1 = 1
<if test="ageGroup != null and ageGroup != ''">
and age_group = #{ageGroup}
</if>
</select>
数据表结构示例:
CREATE TABLE `people_info` (
`id` varchar(64) NOT NULL COMMENT '主键',
`name` varchar(255) DEFAULT NULL COMMENT '名称',
`age_group` int(11) DEFAULT NULL COMMENT '年龄',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
当入参为 0 时,
发现控制台打印 SQL 如下:
select *
from `people_info`
where 1 = 1
-- 本该存在的 ageGroup 判断消失了!
解决方案
首先提供该问题的几种解决方案。
方案一
如果是数据字典类型的字段,在定义数据字典时,避免使用 0 作为枚举值,从根源杜绝该问题。
方案二
如果是非法数据,可在 Controller 层入参增加参数校验,如果传 0,提示“参数无效”。
方案三
如果是合法数据,可在 SQL 判断条件上增加 or ageGroup == 0 判断。
<if test="ageGroup != null and ageGroup != '' or ageGroup == 0">
and age_group = #{ageGroup}
</if>
方案四
如果是合法数据,可将 Integer 转为 String,按 String 参数处理。
String ageGroupStr = String.valueOf(1);
源码解析
下面,我们就通过分析源码,一起来看一下 Mybatis 不为人知的“小动作”。
解析
首先,让我们来到 DefaultSqlSession#select(statement, parameter, rowBounds, handler) 方法。
// 第 165 行
@Override
public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
// 拿到映射的 sql 语句
MappedStatement ms = configuration.getMappedStatement(statement);
// 执行器执行查询 sql -- 重点!!!
executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
此时拿到传入 sql,那么有“小动作”的相想必是执行器,下面进入 BaseExecutor#query(ms, parameter, rowBounds, resultHandler) 方法。
// 第 132 行
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// sql 绑定 -- 重点!!!
BoundSql boundSql = ms.getBoundSql(parameter);
// 创建缓存 key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 执行查询
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
在 sql 绑定过程中都做了什么?下面进入 MappedStatement#getBoundSql(parameterObject) 方法。
// 第 296 行
public BoundSql getBoundSql(Object parameterObject) {
// 获取 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;
}
可以看到 sql 是在 sqlSource 中被绑定的,下面进入 SqlSource#getBoundSql(parameterObject) 方法。
// 第 24 行
public interface SqlSource { BoundSql getBoundSql(Object parameterObject);
}
这是个接口方法,有 4 种默认实现:DynamicSqlSource、ProviderSqlSource、RawSqlSource 和 StaticSqlSource。
以为 DynamicSqlSource 为例,进入 DynamicSqlSource#getBoundSql(parameterObject) 方法。
// 第 36 行
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 获取上下文对象
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 装载 sql 标签节点中的语句 -- 重点!!!
rootSqlNode.apply(context);
// 创建 sql 构造器
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
// 设置参数类型
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 解析 sql
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// 获取已绑定的 sql
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 循环绑定参数
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
进入 SqlNode#apply(context) 方法。
// 第 21 行
public interface SqlNode {
boolean apply(DynamicContext context);
}
这也是个接口方法,有 8 种默认实现:ChooseSqlNode、ForEachSqlNode、IfSqlNode、MixedSqlNode、StaticTextSqlNode、TextSqlNode、TrimSqlNode 和 VarDeclSqlNode,分别对应不同类型的标签节点处理方式。
这里我们要找的是 if 标签节点的处理逻辑,因此进入 IfSqlNode#apply(context) 方法。
// 第 32 行
@Override
public boolean apply(DynamicContext context) {
// 判断 -- 重点!!!
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// 继续解析后续标签节点
contents.apply(context);
return true;
}
return false;
}
进入 ExpressionEvaluator#evaluateBoolean(expression, parameterObject) 方法。
// 第 31 行
public boolean evaluateBoolean(String expression, Object parameterObject) {
// 获取参数值 -- 重点!!!
// 注意!value 不管之前是 '' 还是 0,这里返回一定是 0,具体逻辑咱们继续向下看。
Object value = OgnlCache.getValue(expression, parameterObject);
// 若参数值为布尔类型
if (value instanceof Boolean) {
// 强转为布尔值并返回
return (Boolean) value;
}
// 若参数值为数值类型
if (value instanceof Number) {
// 强转为字符串后,再转为 BigDecimal,然后与 BigDecimal.ZERO 比较,判断是否不为 0,并返回判断结果
// 根据上面的结论,当传入 '' 和 0 走到这一步时,value 值必然为 0,那么返回结果为 false,因此该 if 标签被跳过
return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
}
// 判断参数值是否为空,并返回判断结果
return value != null;
}
这里我们会发现Mybatis 实际上是使用 OGNL 表达式来处理参数的,下面进入 OgnlCache#getValue(expression, root) 静态方法。
// 第 43 行
public static Object getValue(String expression, Object root) {
try {
// 获取上下文对象
Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null);
// 获取参数值 -- 重点!!!
return Ognl.getValue(parseExpression(expression), context, root);
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
继续跟进去,进入 Ognl#getValue(tree, context, root) 静态方法。
// 第 454 行
public static Object getValue(Object tree, Map context, Object root)
throws OgnlException
{
// 没什么好解释的,继续跟进
return getValue(tree, context, root, null);
} // 第 482 行
public static Object getValue(Object tree, Map context, Object root, Class resultType)
throws OgnlException
{
Object result;
// 构建 OGNL 上下文对象
OgnlContext ognlContext = (OgnlContext) addDefaultContext(root, context); // 强转节点对象
Node node = (Node)tree; // 若寄存器不为空
if (node.getAccessor() != null)
// 从寄存器中获取参数值
result = node.getAccessor().get(ognlContext, root);
else
// 否则,从节点对象获取参数值
result = node.getValue(ognlContext, root); // 若参数类型不为空
if (resultType != null) {
// 获取类型转换器,转换参数值 -- 重点!!!
result = getTypeConverter(context).convertValue(context, root, null, null, result, resultType);
}
return result;
}
这不是参数转换的方法吗?进入 TypeConverter#convertValue(context, target, member, propertyName, value, toType) 方法。
public interface TypeConverter
{
public Object convertValue(Map context, Object target, Member member, String propertyName, Object value, Class toType);
}
又是接口方法,下面进入实现类 DefaultTypeConverter#convertValue(context, target, member, propertyName, value, toType) 方法。
// 第 48 行 -- 再进入这里
public Object convertValue(Map context, Object value, Class toType)
{
// 转换参数值 -- 重点!!!
return OgnlOps.convertValue(value, toType);
} // 第 53 行 -- 先进入这里
public Object convertValue(Map context, Object target, Member member, String propertyName, Object value, Class toType)
{
return convertValue(context, value, toType);
}
离真相越来越近了,进入 OgnlOps#convertValue(Object value, Class toType) 方法。
// 第 508 行 -- 先进入这里
public static Object convertValue(Object value, Class toType)
{
return convertValue(value, toType, false);
} // 第 553 行 -- 再进入这里
public static Object convertValue(Object value, Class toType, boolean preventNulls)
{
Object result = null; // 如果参数值不为空,且与传入类型相同,返回参数值
if (value != null && toType.isAssignableFrom(value.getClass()))
return value; if (value != null) {
// 如果参数值不为空
/* If array -> array then convert components of array individually */
if (value.getClass().isArray() && toType.isArray()) {
// 如果参数值与参数类型都为数组
// 获取类型
Class componentType = toType.getComponentType(); // 根据类型和长度,创建 Array 对象
result = Array.newInstance(componentType, Array.getLength(value));
// 循环 Array,对 Array 每一个参数值进行转换并赋值
for(int i = 0, icount = Array.getLength(value); i < icount; i++) {
Array.set(result, i, convertValue(Array.get(value, i), componentType));
}
} else if (value.getClass().isArray() && !toType.isArray()) { // 如果参数值为数组,参数类型非数组
// 对参数值进行转换并赋值
return convertValue(Array.get(value, 0), toType);
} else if (!value.getClass().isArray() && toType.isArray()){ // 如果参数值非数组,参数类型为数组
if (toType.getComponentType() == Character.TYPE) { // 如果参数类型为 char
// 将参数值转为字符串后,转为 char 数组
result = stringValue(value).toCharArray();
} else if (toType.getComponentType() == Object.class) {
// 如果参数类型为 Obejct
if (value instanceof Collection) {
// 如果参数类型为集合
// 强转为集合
Collection vc = (Collection) value;
// 转换为数组
return vc.toArray(new Object[0]);
} else
// 创建一个新的 Object 对象并返回
return new Object[] { value };
}
} else {
// 如果参数类型为 Integer -- 重点!!!
if ((toType == Integer.class) || (toType == Integer.TYPE)) {
// 参数值转换,强转为 int 并赋值 -- 重点!!!
result = new Integer((int) longValue(value));
}
if ((toType == Double.class) || (toType == Double.TYPE)) result = new Double(doubleValue(value));
if ((toType == Boolean.class) || (toType == Boolean.TYPE))
result = booleanValue(value) ? Boolean.TRUE : Boolean.FALSE;
if ((toType == Byte.class) || (toType == Byte.TYPE)) result = new Byte((byte) longValue(value));
if ((toType == Character.class) || (toType == Character.TYPE))
result = new Character((char) longValue(value));
if ((toType == Short.class) || (toType == Short.TYPE)) result = new Short((short) longValue(value));
if ((toType == Long.class) || (toType == Long.TYPE)) result = new Long(longValue(value));
if ((toType == Float.class) || (toType == Float.TYPE)) result = new Float(doubleValue(value));
if (toType == BigInteger.class) result = bigIntValue(value);
if (toType == BigDecimal.class) result = bigDecValue(value);
if (toType == String.class) result = stringValue(value);
}
} else {
if (toType.isPrimitive()) {
result = OgnlRuntime.getPrimitiveDefaultValue(toType);
} else if (preventNulls && toType == Boolean.class) {
result = Boolean.FALSE;
} else if (preventNulls && Number.class.isAssignableFrom(toType)){
result = OgnlRuntime.getNumericDefaultValue(toType);
}
} if (result == null && preventNulls)
return value; if (value != null && result == null) { throw new IllegalArgumentException("Unable to convert type " + value.getClass().getName() + " of " + value + " to type of " + toType.getName());
} return result;
}
我们看到了 Integer 参数实际会走到 OgnlOps#longValue(value) 方法。
// 第 213 行
public static long longValue(Object value)
throws NumberFormatException
{
// 参数值若为 null,返回 0
if (value == null) return 0L;
// 获取参数值对应类
Class c = value.getClass();
// 若参数值为数值类型,强转为数值类型后返回
if (c.getSuperclass() == Number.class) return ((Number) value).longValue();
// 若参数值为布尔类型,强转为布尔类型后,转为 0 或 1 后返回
if (c == Boolean.class) return ((Boolean) value).booleanValue() ? 1 : 0;
// 若参数值为数值类型,强转为字节类型后返回
if (c == Character.class) return ((Character) value).charValue();
// 若未匹配到参数类型,转为字符串后,再转为长整型类型后返回
return Long.parseLong(stringValue(value, true));
/* 这里可以看出,不管是 '' 还是 0,都统一做 0 处理 */
}
结论
在 Mybatis 的 if 标签中,为了兼容错误的类型传参(如:参数为 Integer 类型,却传入 String 类型),在数值转换的处理上,非数值类型的参数值会转换为数值类型的值。然后通过判断是否为 0 ,决定 if 标签中的 sql 语句是否生效。
因此,当我们使用 ageGroup != null and ageGroup != '' 条件时,如果入参为 0,会被 Mybatis 忽略。
会出现类似情况的类型还包括其他数值类型:Double、Byte、Character、Short、Long、Float、BigInteger、BigDecimal。
源码解析之 Mybatis 对 Integer 参数做了什么手脚?的更多相关文章
- Mybatis源码解析(一) —— mybatis与Spring是如何整合的?
Mybatis源码解析(一) -- mybatis与Spring是如何整合的? 从大学开始接触mybatis到现在差不多快3年了吧,最近寻思着使用3年了,我却还不清楚其内部实现细节,比如: 它是如 ...
- 【MyBatis源码解析】MyBatis一二级缓存
MyBatis缓存 我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相 ...
- JDK源码及其他框架源码解析随笔地址导航
置顶一篇文章,主要是整理一下写过的JDK中各个类的源码及其他框架源码解析的文章,方便自己随时阅读也方便网友朋友们阅读与指正 基础篇 从为什么String=String谈到StringBuilder和S ...
- Mybatis 系列5-结合源码解析TypeHandler
[Mybatis 系列10-结合源码解析mybatis 执行流程] [Mybatis 系列9-强大的动态sql 语句] [Mybatis 系列8-结合源码解析select.resultMap的用法] ...
- mybatis源码-解析配置文件(三)之配置文件Configuration解析
目录 1. 简介 1.1 系列内容 1.2 适合对象 1.3 本文内容 2. 配置文件 2.1 mysql.properties 2.2 mybatis-config.xml 3. Configura ...
- Mybatis源码解析,一步一步从浅入深(三):实例化xml配置解析器(XMLConfigBuilder)
在上一篇文章:Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码 ,中我们看到 代码:XMLConfigBuilder parser = new XMLConfigBuilder(read ...
- Mybatis源码解析,一步一步从浅入深(五):mapper节点的解析
在上一篇文章Mybatis源码解析,一步一步从浅入深(四):将configuration.xml的解析到Configuration对象实例中我们谈到了properties,settings,envir ...
- Mybatis源码解析,一步一步从浅入深(七):执行查询
一,前言 我们在文章:Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码的最后一步说到执行查询的关键代码: result = sqlSession.selectOne(command.ge ...
- Mybatis源码解析(二) —— 加载 Configuration
Mybatis源码解析(二) -- 加载 Configuration 正如上文所看到的 Configuration 对象保存了所有Mybatis的配置信息,也就是说mybatis-config. ...
随机推荐
- Tensorflow2的基本用法
张量表示数据,用计算图搭建神经网络,用会话执行计算图,优化线上的权重(参数)->得到模型. 张量(tensor):多维数组(列表) 阶:张量的维数. 数据类型: ...
- hdu1228双指针
#include <iostream> #include <cstdio> #include <cstring> using namespace std; char ...
- tensorflow加载ckpt出错
Issue链接 问题: tensorflow加载ckpt出错 此处原因: 该ckpt文件对应的tensorflow版本过老, 其中的部分内置变量名发生了改变. 提示: Key lstm_o/bidir ...
- React Gatsby 最新教程
React Gatsby 最新教程 https://www.gatsbyjs.com/docs/quick-start/ https://www.gatsbyjs.com/docs/tutorial/ ...
- npm fetch All In One
npm fetch All In One fetch for TypeScript { "compilerOptions": { "lib": ["D ...
- 经济学,金融学:资产证券化 ABS
经济学,金融学:资产证券化 ABS ABS 资产支持证券 蚂蚁金服如何把30亿变成3000亿?资产证券化 前几天,花呗借呗的东家蚂蚁集团在上市前夕被监管部门叫停,因为这则新闻广大网民都听说了一个概念: ...
- how to stop MongoDB from the command line
how to stop MongoDB from the command line stop mongod https://docs.mongodb.com/manual/tutorial/manag ...
- bob and brad physical therapy knee exercise
bob and brad physical therapy knee exercise 鲍勃和布拉德物理治疗膝关节运动 https://bobandbrad.com/ youtube https:// ...
- deep copy & deep merge
deep copy & deep merge JSON.parse(JSON.stringify(obj)); lodash https://lodash.com/docs/ https:// ...
- Flutter: Draggable和DragTarget 可拖动小部件
API class _MyHomeState extends State<MyHome> { List<Map<String, String>> _data1 = ...