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中处理参数提供了便利。