2. MyBatis 参数处理机制:从 execute 方法到参数流转全解析

MyBatis 参数处理机制:从 execute 方法到参数流转全解析

上一篇文章,介绍了Mybatis Mapper是如何使用JDK的动态代理的创建,简单了解了MapperMethod如何通过SQLSession执行具体的增删改查。本文我们更进一步,详细分析下Mybatis是如何处理参数的。

一、参数处理的起点:MapperMethod.execute 方法

我们先回顾下MapperMethod.execute方法:

java 复制代码
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
        case INSERT: {
            // 转换参数并执行插入
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.insert(command.getName(), param));
            break;
        }
        case UPDATE: {
            // 转换参数并执行更新
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.update(command.getName(), param));
            break;
        }
        case DELETE: {
            // 转换参数并执行删除
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.delete(command.getName(), param));
            break;
        }
        case SELECT:
            // executeWithResultHandler、executeForMany等方法的内部,也会调用convertArgsToSqlCommandParam方法转换参数。
            if (methodSignature.returnsVoid() && methodSignature.hasResultHandler()) {
                // 处理带ResultHandler的查询
                executeWithResultHandler(sqlSession, args);
                result = null;
            } else if (methodSignature.returnsMany()) {
                // 处理返回集合的查询(selectList)
                result = executeForMany(sqlSession, args);
            } else if (methodSignature.returnsMap()) {
                // 处理返回Map的查询
                result = executeForMap(sqlSession, args);
            } else {
                // 处理返回单个对象的查询(selectOne)
                Object param = methodSignature.convertArgsToSqlCommandParam(args);
                result = sqlSession.selectOne(command.getName(), param);
            }
            break;
        // 其他类型处理...
        default:
            throw new BindingException("Unknown execution method for: " + command.getName());
    }
    return result;
}

解析:

无论执行哪种SQL操作(INSERT/UPDATE/SELECT 等),都需要通过method.convertArgsToSqlCommandParam(args)完成参数转换。这一步是参数处理的核心,其内部委托ParamNameResolver实现具体逻辑。

java 复制代码
private final ParamNameResolver paramNameResolver;
public Object convertArgsToSqlCommandParam(Object[] args) {  
    return paramNameResolver.getNamedParams(args);  
}

二、ParamNameResolver:参数解析的核心实现

ParamNameResolver是参数处理的核心类,负责解析方法参数的名称、注解信息,并将参数数组转换为合适的格式。其工作流程分为构造阶段 (解析参数元信息)和运行阶段(转换参数数组)。

1. 构造阶段:解析参数元信息

ParamNameResolver在初始化时(与MethodSignature一同创建)会解析方法参数的名称和@Param注解,存储在names集合中。源码如下:

java 复制代码
// 方法的参数名字,key是序号,value是名字
private final SortedMap<Integer, String> names;

public ParamNameResolver(Configuration config, Method method) {
    this.useActualParamName = config.isUseActualParamName();
    final Class<?>[] paramTypes = method.getParameterTypes();
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<>();
    int paramCount = paramAnnotations.length;
    // 遍历所有参数,解析参数名
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
        // 跳过RowBounds和ResultHandler等特殊参数
        if (isSpecialParameter(paramTypes[paramIndex])) {
            continue;
        }
        String name = null;
        // 查找@Param注解
        for (Annotation annotation : paramAnnotations[paramIndex]) {
            if (annotation instanceof Param) {
                hasParamAnnotation = true;
                name = ((Param) annotation).value();
                break;
            }
        }
        if (name == null) {
            // 无@Param注解时,使用参数名(需编译时保留参数名)或默认名
            if (useActualParamName) {
                name = getActualParamName(method, paramIndex);
            }
            if (name == null) {
                // use the parameter index as the name ("0", "1", ...)  
                // gcode issue #71  
                name = String.valueOf(map.size());
            }
        }
        map.put(paramIndex, name);
    }
    names = Collections.unmodifiableSortedMap(map);
}

解析 :构造方法的核心是遍历方法参数,确定每个参数的名称,记录在names这个SortedMap中。

  • 跳过RowBounds和ResultHandler等特殊参数
  • 若参数有@Param("name")注解,则直接使用注解值作为参数名(如@Param("userId") Long id的参数名为"userId")。
  • 若无注解但启用了useActualParamName(默认开启),则使用方法声明的参数名(如selectById(Long id)的参数名为"id"),不过注意:这需要Java编译时添加-parameters参数。
  • 若上述条件均不满足,则生成默认名为("0", "1", ...),但这不是最终的名字 ,例如,对于方法User selectByUserAndDept(@Param("user") User user, Long deptId):
    • 参数 0(User user)有@Param("user"),参数名为"user"。
    • 参数 1(Long deptId)无注解,若保留参数名则为"deptId",否则为"1",但这不是最终的名字。

2.运行阶段:转换参数数组

getNamedParams方法根据构造阶段解析的names映射,将Object\[\] args转换为可执行的参数对象。源码如下:

java 复制代码
public Object getNamedParams(Object[] args) {  
    final int paramCount = names.size();  
    if (args == null || paramCount == 0) {  
        // 没有参数,返回null
        return null;  
    } else if (!hasParamAnnotation && paramCount == 1) {  
        // 只有一个参数,返回参数本身,注意这里不能写成return args[0],因为可能有特殊的RowBound等参数
        return args[names.firstKey()];  
    } else {  
        // 多个参数,返回ParamMap
        final Map<String, Object> param = new ParamMap<>();  
        int i = 0;  
        for (Map.Entry<Integer, String> entry : names.entrySet()) {
            // 根据已经解析的名字,把参数放进map中
            param.put(entry.getValue(), args[entry.getKey()]);  
            // 同时根据通用规则向Map中加参数,名字为 (param1, param2, ...)  
            final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);  
            // 但不要和指定的名字冲突
            if (!names.containsValue(genericParamName)) {  
                param.put(genericParamName, args[entry.getKey()]);  
            }  
        i++;  
    }  
    return param;  
}  

解析:该方法根据参数数量和注解情况,返回不同类型的参数对象:

  • 无参数:返回null(如selectAll())。
  • 单参数:直接返回参数值(如selectById(Long id)调用时返回100L);
  • 多参数:返回ParamMap(HashMap子类),键为参数名(或默认名),值为参数值,同时添加param1、param2等通用键(如selectByUserAndDept方法调用时,args = new User(), 5L会转换为{"user": User, "deptId": 5L, "param1": User, "param2": 5L})。

三、SqlSession 对集合参数的二次处理:wrapCollection 方法

当参数是集合或数组时,SqlSession的实现类(如DefaultSqlSession)会通过wrapCollection方法二次封装,确保底层执行器能正确识别参数类型。源码如下:

java 复制代码
@Override  
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {  
    try {  
        MappedStatement ms = configuration.getMappedStatement(statement);  
        // 这里额外调用了wrapCollection处理参数
        return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);  
    } catch (Exception e) {  
        throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);  
    } finally {  
        ErrorContext.instance().reset();  
    }  
}
    
private Object wrapCollection(final Object object) {
    if (object instanceof Collection) {
        // 处理Collection类型(List、Set等)
        ParamMap<Object> map = new StrictMap<>();
        map.put("collection", object);
        if (object instanceof List) {
            map.put("list", object);  // List额外添加"list"键
        }
        return map;
    } else if (object != null && object.getClass().isArray()) {
        // 处理数组类型
        ParamMap<Object> map = new StrictMap<>();
        map.put("array", object);
        return map;
    }
    return object;
}

解析:该方法针对集合和数组类型进行特殊处理,注意,当仅有一个参数,且是集合或者数组,才会处理,因为多个参数时,参数类型一定是Map.

  • 若参数是Collection(如List),封装为包含"collection"键的ParamMap;若是List,额外添加"list"键(如List ids参数会被转换为{"collection": ids, "list": ids})。
  • 若参数是数组(如Long\[\] ids),封装为包含"array"键的ParamMap(如Long\[\] ids会转换为{"array": ids})。

四、特殊参数处理:RowBounds 与 ResultHandler

ParamNameResolver的isSpecialParameter方法将RowBounds和ResultHandler排除在常规参数之外,由框架单独处理:

arduino 复制代码
private static boolean isSpecialParameter(Class<?> clazz) {
    return RowBounds.class.isAssignableFrom(clazz) || ResultHandler.class.isAssignableFrom(clazz);
}

解析:

  • RowBounds :用于内存分页,指定查询的起始位置(offset)和条数(limit)。例如selectAll(RowBounds rowBounds)调用时,new RowBounds(10, 20)表示从第 10 条记录开始查询 20 条,MyBatis 会在结果返回后进行内存截取。注:这个功能是内存分页,有点鸡肋
  • ResultHandler:用于自定义结果处理,可实时处理每条记录。例如void selectAll(ResultHandler handler)调用时,handler的handleResult方法会逐条处理查询结果,避免一次性加载大量数据到内存。

五、MyBatis 参数处理机制的优点

Mybatis对参数处理,进行了统一的封装,无论入参有几个,都会被统一封装为Map(当然,无参数是null),为后续在JDBC中处理参数提供了便利。

相关推荐
IT研究室几秒前
最新计算机毕业设计选题推荐-基于spring boot的美食配送平台-网站-文档指导-Java-springboot
java·spring boot·课程设计
打工仔折腾 AI2 分钟前
Qt网络编程实战:UDP、TCP到HTTP的落地经验与选型
网络·人工智能·后端·python·qt·udp·ai agent 实战
打工仔折腾 AI2 分钟前
SQL 编辑器不是高级记事本:KStudio 与 KSQL Developer 的建表与增删改查手法
数据库·人工智能·后端·python·sql·编辑器·ai agent 实战
霸道流氓气质17 分钟前
Saga模式入门与实战:编排与协同模式、补偿设计原则与Java生产级实现
java·开发语言
江湖有缘23 分钟前
Docker实战 | 使用Docker部署Readeck书签管理工具
java·docker·eureka
小毛驴85026 分钟前
mvn help:effective-settings 与 mvn help:effective-pom
java·linux·前端
silvamux_39 分钟前
MiniMax API 调用教程:兼容 OpenAI 写法
java·数据库·elasticsearch
拖孩43 分钟前
# AI 帮我写的小程序,上线不到一个月流量主突然爆了
前端·后端·微信小程序
染指111044 分钟前
145.Agent-LangGraph基础概念和安装和简单代码示例
java·服务器·数据库·langchain·agents
Doker 多克1 小时前
ForkJoinPool 详解
java·多线程