MyBatis 源码解析:ParamNameResolver 与 @Param 注解的参数名解析机制
2026/9/21 1:28:47 网站建设 项目流程
  • 文档
  • 教程
  • 知识库

【免费下载链接】source-code-hunter

😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等

项目地址:https://gitcode.com/doocs/source-code-hunter
点击查看免费下载

导读

Mapper 接口方法里写的参数,最终是如何变成 SQL 语句里可以引用的占位符名称的?为什么单参数可以不用@Param,多参数不写@Param时只能靠arg0/param1引用?答案都藏在 MyBatis 的org.apache.ibatis.reflection.ParamNameResolver中。本文以该类的完整源码为主线,结合 MapperMethod、MethodSignature 以及 binding 模块 的调用关系,讲透@Param注解的扫描与处理、参数名的生成规则、getNamedParams的参数组装策略,并给出可复现的 debug 验证过程。读完你将彻底理解 MyBatis 参数命名的三条规则(@Param值 > 真实参数名 > 参数索引),并能从容应对多参数、特殊参数、动态 SQL 引用等实战场景。

一、ParamNameResolver 在 Mapper 调用链中的位置

在理解ParamNameResolver之前,先看它处于哪条调用链上。用户调用 Mapper 接口方法时,MyBatis 通过动态代理进入org.apache.ibatis.binding.MapperMethod#execute,而execute无论执行 INSERT/UPDATE/DELETE/SELECT 哪种操作,第一件事都是把实参转换成 SQL 命令参数:

// org.apache.ibatis.binding.MapperMethod#execute 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: // ... 按返回类型分流,最终同样调用 convertArgsToSqlCommandParam(args) Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); break; // ... } return result; }

这里的methodMapperMethod.MethodSignature(方法签名对象),它持有paramNameResolver字段并在构造时完成初始化(详见 Mybatis-MethodSignature.md):

// MapperMethod.MethodSignature 构造方法片段 private final ParamNameResolver paramNameResolver; public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) { // ... 解析返回值类型、returnsMany、returnsMap、mapKey 等 this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); this.paramNameResolver = new ParamNameResolver(configuration, method); }

而参数转换的入口则是:

public Object convertArgsToSqlCommandParam(Object[] args) { return paramNameResolver.getNamedParams(args); }

也就是说,ParamNameResolver是 Mapper 方法实参 → SQL 绑定参数的唯一转换枢纽:构造方法负责建立"参数索引 → 参数名"的映射表,getNamedParams负责在运行时把实参数组按这张映射表组装成最终传给 SqlSession 的参数对象。调用链可以概括为:

Mapper 动态代理 └─> MapperMethod#execute(sqlSession, args) └─> MethodSignature#convertArgsToSqlCommandParam(args) └─> ParamNameResolver#getNamedParams(args) └─> SqlSession.insert / update / delete / select...

二、ParamNameResolver 类结构:两个字段与一个常量

ParamNameResolver位于org.apache.ibatis.reflection包,是@Param注解的扫描工具和处理工具,其完整源码如下(与 MyBatis 官方实现一致):

/** * {@link Param} 注解的扫描工具和处理工具 */ public class ParamNameResolver { public static final String GENERIC_NAME_PREFIX = "param"; /** * <p> * The key is the index and the value is the name of the parameter.<br /> * The name is obtained from {@link Param} if specified. When {@link 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. {@link RowBounds} or {@link ResultHandler}). * </p> * * {@link ParamNameResolver#ParamNameResolver(org.apache.ibatis.session.Configuration, java.lang.reflect.Method)} 中的map 变量值转换而得 * {参数索引: 参数名称(arg0,Param注解的value)} */ private final SortedMap<Integer, String> names; private boolean hasParamAnnotation; public ParamNameResolver(Configuration config, Method method) { // 方法参数类型 final Class<?>[] paramTypes = method.getParameterTypes(); // 参数上的注解 final Annotation[][] paramAnnotations = method.getParameterAnnotations(); // 参数索引和参数名称 // {参数索引:参数名称} final SortedMap<Integer, String> map = new TreeMap<>(); int paramCount = paramAnnotations.length; // get names from @Param annotations for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) { if (isSpecialParameter(paramTypes[paramIndex])) { // skip special parameters // 如果是特殊类型跳过 continue; } String name = null; // 注解扫描@Param for (Annotation annotation : paramAnnotations[paramIndex]) { // 是否为 Param 注解的下级 if (annotation instanceof Param) { hasParamAnnotation = true; // 获取 value 属性值 name = ((Param) annotation).value(); break; } } if (name == null) { // 如果没有写 @param 处理方式如下 // @Param was not specified. if (config.isUseActualParamName()) { 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 对象 map.put(paramIndex, name); } names = Collections.unmodifiableSortedMap(map); } /** * 是否为特殊参数 , 依据 是否是 {@link RowBounds} 或者 {@link ResultHandler} * @param clazz * @return */ private static boolean isSpecialParameter(Class<?> clazz) { return RowBounds.class.isAssignableFrom(clazz) || ResultHandler.class.isAssignableFrom(clazz); } /** * 返回方法名 参数索引 * @param method * @param paramIndex * @return */ private String getActualParamName(Method method, int paramIndex) { return ParamNameUtil.getParamNames(method).get(paramIndex); } /** * Returns parameter names referenced by SQL providers. */ public String[] getNames() { return names.values().toArray(new String[0]); } /** * <p> * A single non-special parameter is returned without a name. * Multiple parameters are named using the naming rule. * In addition to the default names, this method also adds the generic names (param1, param2, * ...). * </p> * <p> * 通常参数异常在这个地方抛出 param ... 异常 * 获取参数名称,和参数传递的真实数据 */ public Object getNamedParams(Object[] args) { final int paramCount = names.size(); if (args == null || paramCount == 0) { // 是否有参数 return null; } else if (!hasParamAnnotation && paramCount == 1) { // 没有使用 @param 注解 参数只有一个 return args[names.firstKey()]; } else { // 根据索引创建 final Map<String, Object> param = new ParamMap<>(); int i = 0; for (Map.Entry<Integer, String> entry : names.entrySet()) { param.put(entry.getValue(), args[entry.getKey()]); // add generic param names (param1, param2, ...) // param + 当前索引位置 final String genericParamName = GENERIC_NAME_PREFIX + (i + 1); // ensure not to overwrite parameter named with @Param if (!names.containsValue(genericParamName)) { param.put(genericParamName, args[entry.getKey()]); } i++; } return param; } } }

梳理类结构,核心成员只有三个:

成员类型含义
GENERIC_NAME_PREFIXstatic final String通用参数名前缀,值为"param",用于生成param1param2...
namesSortedMap<Integer, String>参数索引 → 参数名的有序映射(TreeMap实现,构造完成后包装为不可变),参数名优先取@Param的 value,否则取真实参数名,兜底用参数索引字符串
hasParamAnnotationboolean方法参数中是否存在@Param注解,它直接决定getNamedParams的分支走向

三、构造方法:参数名是如何一步步解析出来的

构造方法ParamNameResolver(Configuration config, Method method)是整个参数命名规则的核心,其执行流程可以分为五个步骤。

3.1 第一步:获取参数类型与参数注解

final Class<?>[] paramTypes = method.getParameterTypes(); final Annotation[][] paramAnnotations = method.getParameterAnnotations(); final SortedMap<Integer, String> map = new TreeMap<>(); int paramCount = paramAnnotations.length;

通过反射拿到方法的所有参数类型和每个参数上的注解二维数组(paramAnnotations[i]表示第 i 个参数上的全部注解),并用TreeMap暂存结果,保证最终按参数索引有序输出。

3.2 第二步:跳过特殊参数(RowBounds / ResultHandler)

if (isSpecialParameter(paramTypes[paramIndex])) { // skip special parameters continue; }

isSpecialParameter的实现非常直观:

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

RowBounds(分页游标)和ResultHandler(结果处理器)是 MyBatis 预留的"框架级参数",它们不参与业务参数命名,因此直接跳过、不进入names映射。这也是为什么文档注释里特别强调names中的索引可能与方法参数的实际索引不一致:比如list(ResultHandler handler, Integer id)中,id的实际索引是 1,但在names中可能排在第 0 位。

3.3 第三步:扫描 @Param 注解

String name = null; for (Annotation annotation : paramAnnotations[paramIndex]) { if (annotation instanceof Param) { hasParamAnnotation = true; name = ((Param) annotation).value(); break; } }

遍历当前参数上的所有注解,一旦发现org.apache.ibatis.annotations.Param类型的注解:

  • 将全局标记hasParamAnnotation置为true(只要有一个参数标注了@Param即为 true);
  • 取出注解的value()属性作为该参数的名称,随后break结束扫描。

3.4 第四步:无 @Param 时的降级策略(真实参数名 → 参数索引)

如果该参数没有标注@Paramname == null),则进入降级逻辑:

if (config.isUseActualParamName()) { name = getActualParamName(method, paramIndex); } if (name == null) { // use the parameter index as the name ("0", "1", ...) // gcode issue #71 name = String.valueOf(map.size()); }

这里存在两级兜底:

  1. 真实参数名:当Configuration.isUseActualParamName()true(对应 MyBatis 配置项<setting name="useActualParamName" value="true"/>,3.4.1 之后默认开启)时,调用getActualParamName获取方法签名中的真实参数名:
private String getActualParamName(Method method, int paramIndex) { return ParamNameUtil.getParamNames(method).get(paramIndex); }

ParamNameUtil底层依赖 Java 反射的Parameter#getName()ParameterNameDiscoverer。需要说明的是:要拿到"真实参数名"(如idname),必须在编译 Mapper 接口时保留参数名信息——即 javac 编译时增加-parameters参数(或 IDE 中勾选"Store information about method parameters"),否则获取到的只是arg0arg1这类默认名。

  1. 参数索引兜底:如果useActualParamName关闭,或仍然取不到名字(name == null),则直接用map.size()作为字符串形式的名称("0"、"1"、"2"...)。源码注释中的gcode issue #71正是指这一历史问题(早期 MyBatis 在特殊场景下索引计算有缺陷,后改为基于map.size()计数)。

3.5 第五步:构建不可变映射并收尾

map.put(paramIndex, name); // ... names = Collections.unmodifiableSortedMap(map);

每解析完一个参数就放入map,最终包装成不可变的SortedMap赋值给names。此外类中还有一个辅助方法getNames(),返回 SQL Provider(注解式 SQL)引用的参数名数组:

public String[] getNames() { return names.values().toArray(new String[0]); }

四、getNamedParams:运行时参数组装的三分支策略

构造方法解决的是"名字从哪来",getNamedParams(Object[] args)解决的是"实参怎么组装"。它依据names.size()hasParamAnnotation分成三种情况:

4.1 分支一:无参数,直接返回 null

if (args == null || paramCount == 0) { return null; }

方法没有业务参数(names为空),或者实参为null,直接返回null,SQL 不需要任何绑定参数。

4.2 分支二:无 @Param 且仅一个参数,直接返回实参本身

} else if (!hasParamAnnotation && paramCount == 1) { // 没有使用 @param 注解 参数只有一个 return args[names.firstKey()]; }

这是最常见的"单参数免注解"场景:没有@Param注解、且业务参数只有一个时,不包装成 Map,直接把实参对象本身返回。此时 SQL 中的#{}占位符名称不参与匹配,MyBatis 直接按位置绑定该唯一参数。names.firstKey()取出的是唯一一个业务参数在实参数组中的真实索引(因为RowBounds/ResultHandler已被剔除,所以可能不是 0)。

4.3 分支三:多参数或存在 @Param,组装成 ParamMap

} else { // 根据索引创建 final Map<String, Object> param = new ParamMap<>(); int i = 0; for (Map.Entry<Integer, String> entry : names.entrySet()) { param.put(entry.getValue(), args[entry.getKey()]); // add generic param names (param1, param2, ...) final String genericParamName = GENERIC_NAME_PREFIX + (i + 1); // ensure not to overwrite parameter named with @Param if (!names.containsValue(genericParamName)) { param.put(genericParamName, args[entry.getKey()]); } i++; } return param; }

凡是参数个数大于 1使用了@Param注解的方法,实参都会被包装进一个ParamMapMap<String, Object>的容器类)。每个参数会以两种 key 进入 Map:

  • 具名 keyentry.getValue(),即@Param的 value(如ID)或真实参数名(如id);
  • 通用 keyparam1param2...,规则是前缀GENERIC_NAME_PREFIX"param")+ 从 1 开始递增的序号i

其中有一个值得注意的保护逻辑:

if (!names.containsValue(genericParamName)) { param.put(genericParamName, args[entry.getKey()]); }

如果用户用@Param("param1")显式命名了某个参数,names中已经包含值"param1",此时不再覆盖写入通用名,确保@Param指定的名字具有最高优先级、不被通用名冲掉。

因此,无论你是否写@Param,只要方法多于一个参数,XML 动态 SQL 里就总能使用param1param2... 引用参数,这是 MyBatis 提供给使用者的"保底命名"。

五、debug 验证:有 @Param 与无 @Param 的行为差异

原文档作者使用同一个测试用例,通过反复修改 Mapper 方法参数来 debug 验证上述逻辑。测试用例核心代码如下(加载 XML 配置并执行 Mapper 方法):

@Test void testXmlConfigurationLoad() throws IOException { Reader reader = Resources.getResourceAsReader("mybatis-config-demo.xml"); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader); Configuration configuration = factory.getConfiguration(); SqlSession sqlSession = factory.openSession(); HsSellMapper mapper = sqlSession.getMapper(HsSellMapper.class); List<HsSell> list = mapper.list(2); List<Object> objects = sqlSession.selectList("com.huifer.mybatis.mapper.HsSellMapper.list"); assertEquals(list.size(), objects.size()); }

5.1 场景一:不写 @Param,观察构造结果

Mapper 方法定义为:

List<HsSell> list(Integer id);

debug 时观察ParamNameResolvernames字段:由于没有@Param注解,hasParamAnnotation = false,参数名按useActualParamName规则生成(或降级为索引),得到{0 -> "arg0"}之类的映射:

5.2 场景二:写 @Param,观察构造结果

改为:

List<HsSell> list(@Param("ID") Integer id);

此时注解扫描分支命中@ParamhasParamAnnotation = truenames中该参数的名称直接取注解的 value"ID"

5.3 场景三:对比 getNamedParams 的返回结果

getNamedParams上打断点,对比两种写法的最终产物:

不写 @Param(list(Integer id)hasParamAnnotation = falseparamCount == 1,命中分支二,直接返回实参本身args[0]),而不是 Map:

写 @Param(list(@Param("ID") Integer id):虽然只有一个参数,但hasParamAnnotation = true,命中分支三,返回的paramParamMap,包含{"ID": 2, "param1": 2}两个键值对——具名 key 与通用 key 并存:

两组 debug 截图清晰地印证了源码逻辑:@Param的存在与否,决定了返回的是裸实参还是ParamMap;而在 ParamMap 模式下,@Param的 value 和paramN通用名会同时写入

六、实战要点与常见问题

结合上面的源码行为,整理出以下可直接指导日常编码的要点:

1. 单参数(无 @Param)时的最小写法

List<HsSell> list(Integer id);

此时 SQL 中#{id}#{value}#{anything}都无所谓,因为返回的是裸实参,MyBatis 按位置绑定。

2. 多参数必须用 @Param 或通用名引用

List<HsSell> list(@Param("id") Integer id, @Param("name") String name);

多参数场景下返回的是ParamMap,SQL 中可以写#{id}#{name},也可以写#{param1}#{param2}。若不加@Param且编译未保留参数名,则只能用#{param1}#{param2}(或#{arg0}#{arg1},取决于useActualParamName的开启情况)。

3. @Param 与通用名的优先级@Param的 value 永远优先;当@Param("param1")与自动生成的param1冲突时,代码中的names.containsValue(genericParamName)保护逻辑会阻止通用名覆盖,避免出现歧义。

4. useActualParamName 开关的影响Configuration.isUseActualParamName()控制是否启用 Java 反射真实参数名。要拿到id而非arg0,需要编译时带-parameters参数;否则应显式编写@Param,这也是很多团队"多参数一律写 @Param"规范背后的源码依据。

5. RowBounds / ResultHandler 不参与命名它们是框架级参数,会被isSpecialParameter跳过,names中的索引与实参数组索引可能不一致,但getNamedParams通过args[entry.getKey()]始终用真实索引取值,因此不会取错参数。

6. 动态 SQL 中的参数引用<if test="id != null">test表达式、#{}占位符中的名称,最终解析时面对的就是getNamedParams组装出的ParamMap或裸实参,因此命名规则与上述一致。相关动态 SQL 解析可进一步参考 Mybatis-DynamicSqlSource.md 与 2、SqlNode和SqlSource.md。

七、总结

ParamNameResolver是 MyBatis Mapper 方法参数到 SQL 绑定参数之间的"翻译官",其设计要点可以浓缩为一张规则表:

场景hasParamAnnotationnames 内容getNamedParams 返回
无参数falsenull
单参数、无 @Paramfalse{0 -> "arg0"}裸实参args[0]
多参数或存在 @Paramtrue@Paramvalue / 真实参数名 / 索引ParamMap(具名 key +param1/param2...)

从调用链看,MapperMethod#executeMethodSignature#convertArgsToSqlCommandParamParamNameResolver#getNamedParams三者环环相扣(对应文档 Mybatis-MapperMethod.md、Mybatis-MethodSignature.md、3、binding模块.md),而ParamNameResolver正是其中负责"参数命名与组装"的关键一环。理解它之后,无论是排查There is no getter for property named 'xxx'这类参数绑定异常,还是编写多参数 Mapper 方法,都能做到心中有数。

  • 文档
  • 教程
  • 知识库

【免费下载链接】source-code-hunter

😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等

项目地址:https://gitcode.com/doocs/source-code-hunter
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询