MyBatis Mapper 方法参数设置之 ParamNameResolver
MyBatis 中将 Mapper 接口中的方法封装为 MapperMethod 对象。调用 Mapper 接口中的如下方法:
List<User> findList(User user);
最终会调用org.apache.ibatis.binding.MapperMethod#executeForMany
,其内部会调用org.apache.ibatis.binding.MapperMethod.MethodSignature#convertArgsToSqlCommandParam
来将 Mapper 方法参数转为后续用来执行 SQL 的参数:
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) { List<E> result; Object param = method.convertArgsToSqlCommandParam(args); if (method.hasRowBounds()) { RowBounds rowBounds = method.extractRowBounds(args); result = sqlSession.<E>selectList(command.getName(), param, rowBounds); } else { result = sqlSession.<E>selectList(command.getName(), param); } ... }
convertArgsToSqlCommandParam 方法会调用org.apache.ibatis.reflection.ParamNameResolver#getNamedParams
:
public Object convertArgsToSqlCommandParam(Object[] args) { return paramNameResolver.getNamedParams(args); }
paramNameResolver 属性在创建 MethodSignature 时实例化:
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) { ... this.paramNameResolver = new ParamNameResolver(configuration, method); }
在 ParamNameResolver 的构造方法中会解析方法参数信息,保存到 names 属性中,names 属性类型为SortedMap<Integer, String>
:
public ParamNameResolver(Configuration config, Method method) { final Class<?>[] paramTypes = method.getParameterTypes(); final Annotation[][] paramAnnotations = method.getParameterAnnotations(); final SortedMap<Integer, String> map = new TreeMap<Integer, String>(); int paramCount = paramAnnotations.length; // 遍历所有参数,从 @Param 注解获取参数名称 for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) { if (isSpecialParameter(paramTypes[paramIndex])) { // 跳过特殊参数 continue; } String name = null; for (Annotation annotation : paramAnnotations[paramIndex]) { if (annotation instanceof Param) { hasParamAnnotation = true; name = ((Param) annotation).value(); break; } } if (name == null) { // 没有标注 @Param if (config.isUseActualParamName()) { name = getActualParamName(method, paramIndex); } if (name == null) { // 使用参数索引作为参数名称 ("0", "1", ...) name = String.valueOf(map.size()); } } map.put(paramIndex, name); } names = Collections.unmodifiableSortedMap(map); }
方法先获取方法参数的注解(注意 paramAnnotations 类型是Annotation[][]
,没有注解的参数会对应一个空数组),然后遍历参数。
for 循环内部,通过isSpecialParameter
方法判断参数是否是特殊参数:
private static boolean isSpecialParameter(Class<?> clazz) { return RowBounds.class.isAssignableFrom(clazz) || ResultHandler.class.isAssignableFrom(clazz); }
从上面的代码可见特殊参数包括:
- RowBounds
- ResultHandler
然后遍历参数的注解,如果注解是@Param
,则获取注解的 value 属性作为参数名。
如果参数没有被@Param
标注(name == null
)且开启了 useActualParamName 配置,则通过反射获取方法参数名,如果还是未获取到(name == null
),则使用参数索引(注意是map.size()
,而不是方法形参列表索引)作为参数名。
以参数索引(这里就是方法形参列表索引了)为键,以得到的参数名为值,保存到 map 集合中,而且注意 map 的类型为TreeMap<Integer, String>
,数据会按参数索引从小到大排序。
在循环结束后,将 map 转换为SortedMap<Integer, String>
,并赋值给 names 属性。所以最终的 names 满足下面的条件:
The key is the index and the value is the name of the parameter. The name is obtained from Param if specified. When Param is not specified, the parameter index is used. Note that this index could be different from the actual index when the method has special parameters (i. e. RowBounds or ResultHandler). - aMethod(@Param("M") int a, @Param("N") int b) -> {{0, "M"}, {1, "N"}} - aMethod(int a, int b) -> {{0, "0"}, {1, "1"}} - aMethod(int a, RowBounds rb, int b) -> {{0, "0"}, {2, "1"}}
也就是:
names 的 key 为参数在形参列表中的索引,value 为参数名。 参数名来自 Param 注解,如果没有被 Param 标注,则使用参数索引作为参数名。注意是参数索引,而不是方法形参列表索引。 比如 aMethod(int a, RowBounds rb, int b) 得到的 names 为 {{0, "0"}, {2, "1"}}
回过头看 convertArgsToSqlCommandParam 方法调用的 getNamedParams 方法,其内部会用到 names 属性:
public Object getNamedParams(Object[] args) { final int paramCount = names.size(); if (args == null || paramCount == 0) { return null; } else if (!hasParamAnnotation && paramCount == 1) { return args[names.firstKey()]; } else { final Map<String, Object> param = new ParamMap<Object>(); int i = 0; for (Map.Entry<Integer, String> entry : names.entrySet()) { param.put(entry.getValue(), args[entry.getKey()]); // 添加通用的参数名 (param1, param2, ...) final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1); // 确保不覆盖 @Param 指明的参数名 if (!names.containsValue(genericParamName)) { param.put(genericParamName, args[entry.getKey()]); } i++; } return param; } }
getNamedParams 方法会按如下规则返回一个参数对象:
-
如果没有参数,则返回 null
-
如果参数数量为 1,且该参数没有被 Param 注解标注,则直接返回参数列表中的唯一参数
-
否则返回一个 Map 对象(param),其中 key 为参数名,value 为参数值。参数名从 names 属性中获取,即 names 属性的值。此外,会在 param 中添加通用参数名(param1,param2,...)
convertArgsToSqlCommandParam 方法调用 getNamedParams 方法将结果返回,下一步会将参数传给 SqlSession 的 selectList 等方法。
有时候会看到,可以在 XML 中使用 param1 之类来做占位符,或者不使用 Param 注解标注参数,通常也还是能在 XML 中以参数名称来获取参数。从这里可知其背后原理。
另外:
- 如果上面得到的参数对象为 Collection 类型,则可以在 XML 中通过
collection
来引用 - 如果这个 Collection 类型对象是 List 类型,则也可以通过
list
来引用 - 如果上面得到的参数对象为数组类型参数,则需要通过
array
来引用
这是因为在 SqlSession 的 selectList 等方法中,对参数进行了包装:
@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) { StrictMap<Object> map = new StrictMap<Object>(); map.put("collection", object); if (object instanceof List) { map.put("list", object); } return map; } else if (object != null && object.getClass().isArray()) { StrictMap<Object> map = new StrictMap<Object>(); map.put("array", object); return map; } return object; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端