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 不为人知的“小动作”。

解析

  1. 首先,让我们来到 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();
    }
    }
  2. 此时拿到传入 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);
    }
  3. 在 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;
    }
  4. 可以看到 sql 是在 sqlSource 中被绑定的,下面进入 SqlSource#getBoundSql(parameterObject) 方法。

    // 第 24 行
    public interface SqlSource { BoundSql getBoundSql(Object parameterObject);
    }
  5. 这是个接口方法,有 4 种默认实现:DynamicSqlSource、ProviderSqlSource、RawSqlSource 和 StaticSqlSource。

  6. 以为 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;
    }
  7. 进入 SqlNode#apply(context) 方法。

    // 第 21 行
    public interface SqlNode {
    boolean apply(DynamicContext context);
    }
  8. 这也是个接口方法,有 8 种默认实现:ChooseSqlNode、ForEachSqlNode、IfSqlNode、MixedSqlNode、StaticTextSqlNode、TextSqlNode、TrimSqlNode 和 VarDeclSqlNode,分别对应不同类型的标签节点处理方式。

  9. 这里我们要找的是 if 标签节点的处理逻辑,因此进入 IfSqlNode#apply(context) 方法。

    // 第 32 行
    @Override
    public boolean apply(DynamicContext context) {
    // 判断 -- 重点!!!
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
    // 继续解析后续标签节点
    contents.apply(context);
    return true;
    }
    return false;
    }
  10. 进入 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;
    }
  11. 这里我们会发现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);
    }
    }
  12. 继续跟进去,进入 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;
    }
  13. 这不是参数转换的方法吗?进入 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);
    }
  14. 又是接口方法,下面进入实现类 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);
    }
  15. 离真相越来越近了,进入 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;
    }
  16. 我们看到了 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 参数做了什么手脚?的更多相关文章

  1. Mybatis源码解析(一) —— mybatis与Spring是如何整合的?

    Mybatis源码解析(一) -- mybatis与Spring是如何整合的?   从大学开始接触mybatis到现在差不多快3年了吧,最近寻思着使用3年了,我却还不清楚其内部实现细节,比如: 它是如 ...

  2. 【MyBatis源码解析】MyBatis一二级缓存

    MyBatis缓存 我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相 ...

  3. JDK源码及其他框架源码解析随笔地址导航

    置顶一篇文章,主要是整理一下写过的JDK中各个类的源码及其他框架源码解析的文章,方便自己随时阅读也方便网友朋友们阅读与指正 基础篇 从为什么String=String谈到StringBuilder和S ...

  4. Mybatis 系列5-结合源码解析TypeHandler

    [Mybatis 系列10-结合源码解析mybatis 执行流程] [Mybatis 系列9-强大的动态sql 语句] [Mybatis 系列8-结合源码解析select.resultMap的用法] ...

  5. mybatis源码-解析配置文件(三)之配置文件Configuration解析

    目录 1. 简介 1.1 系列内容 1.2 适合对象 1.3 本文内容 2. 配置文件 2.1 mysql.properties 2.2 mybatis-config.xml 3. Configura ...

  6. Mybatis源码解析,一步一步从浅入深(三):实例化xml配置解析器(XMLConfigBuilder)

    在上一篇文章:Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码 ,中我们看到 代码:XMLConfigBuilder parser = new XMLConfigBuilder(read ...

  7. Mybatis源码解析,一步一步从浅入深(五):mapper节点的解析

    在上一篇文章Mybatis源码解析,一步一步从浅入深(四):将configuration.xml的解析到Configuration对象实例中我们谈到了properties,settings,envir ...

  8. Mybatis源码解析,一步一步从浅入深(七):执行查询

    一,前言 我们在文章:Mybatis源码解析,一步一步从浅入深(二):按步骤解析源码的最后一步说到执行查询的关键代码: result = sqlSession.selectOne(command.ge ...

  9. Mybatis源码解析(二) —— 加载 Configuration

    Mybatis源码解析(二) -- 加载 Configuration    正如上文所看到的 Configuration 对象保存了所有Mybatis的配置信息,也就是说mybatis-config. ...

随机推荐

  1. [Angular] 删除旧版本,升级安装最新版本

    目录 删除旧版本 清除未卸载干净的angular-cli缓存 对于Linux 对于Windows 安装最新版本 查看安装版本 创建新项目 删除旧版本 npm uninstall -g angular- ...

  2. VMware ESXi 开启嵌套虚拟化

    VMware ESXi 默认不支持嵌套虚拟化功能,需要修改相关配置文件才能支持. 1.Esxi主机开启ssh,修改 /etc/vmware/config 配置文件,在配置文件后面加入如下配置:vhv. ...

  3. 2018ACM上海大都会赛 F Color it【基础的扫描线】

    题目:戳这里 题意:有n*m个点全为白色,q个圆,将q个圆内所有的点都染成黑色,问最后剩下多少白色的点. 解题思路:每一行当做一个扫描线,扫描所有的圆,记录每一行在圆中的点即可,O(n*q). 附ac ...

  4. codeforces 2C(非原创)

    C. Commentator problem time limit per test 1 second memory limit per test 64 megabytes input standar ...

  5. HDU 6155 Subsequence Count(矩阵 + DP + 线段树)题解

    题意:01串,操作1:把l r区间的0变1,1变0:操作2:求出l r区间的子序列种数 思路:设DP[i][j]为到i为止以j结尾的种数,假设j为0,那么dp[i][0] = dp[i - 1][1] ...

  6. HTML5 Canvas 2D library All In One

    HTML5 Canvas 2D library All In One https://github.com/search?q=Javascript+Canvas+Library https://git ...

  7. Learning JavaScript with MDN (call, apply, bind)

    Learning JavaScript with MDN (call, apply, bind) call, apply, bind Object.prototype.toString() 检测 js ...

  8. Top 10 JavaScript errors

    Top 10 JavaScript errors javascript errors https://rollbar.com/blog/tags/top-errors https://rollbar. ...

  9. npm published cli package's default install missing the `-g` flag

    npm published cli package's default install missing the -g flag https://npm.community/t/npm-publishe ...

  10. Build your own React

    Build your own React https://pomb.us/build-your-own-react/ https://github.com/pomber/didact demo htt ...