- 文档
- 教程
- 知识库
【免费下载链接】source-code-hunter
😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等
导读
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; }这里的method是MapperMethod.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_PREFIX | static final String | 通用参数名前缀,值为"param",用于生成param1、param2... |
names | SortedMap<Integer, String> | 参数索引 → 参数名的有序映射(TreeMap实现,构造完成后包装为不可变),参数名优先取@Param的 value,否则取真实参数名,兜底用参数索引字符串 |
hasParamAnnotation | boolean | 方法参数中是否存在@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 时的降级策略(真实参数名 → 参数索引)
如果该参数没有标注@Param(name == 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()); }这里存在两级兜底:
- 真实参数名:当
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。需要说明的是:要拿到"真实参数名"(如id、name),必须在编译 Mapper 接口时保留参数名信息——即 javac 编译时增加-parameters参数(或 IDE 中勾选"Store information about method parameters"),否则获取到的只是arg0、arg1这类默认名。
- 参数索引兜底:如果
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注解的方法,实参都会被包装进一个ParamMap(Map<String, Object>的容器类)。每个参数会以两种 key 进入 Map:
- 具名 key:
entry.getValue(),即@Param的 value(如ID)或真实参数名(如id); - 通用 key:
param1、param2...,规则是前缀GENERIC_NAME_PREFIX("param")+ 从 1 开始递增的序号i。
其中有一个值得注意的保护逻辑:
if (!names.containsValue(genericParamName)) { param.put(genericParamName, args[entry.getKey()]); }如果用户用@Param("param1")显式命名了某个参数,names中已经包含值"param1",此时不再覆盖写入通用名,确保@Param指定的名字具有最高优先级、不被通用名冲掉。
因此,无论你是否写@Param,只要方法多于一个参数,XML 动态 SQL 里就总能使用param1、param2... 引用参数,这是 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 时观察ParamNameResolver的names字段:由于没有@Param注解,hasParamAnnotation = false,参数名按useActualParamName规则生成(或降级为索引),得到{0 -> "arg0"}之类的映射:
5.2 场景二:写 @Param,观察构造结果
改为:
List<HsSell> list(@Param("ID") Integer id);此时注解扫描分支命中@Param,hasParamAnnotation = true,names中该参数的名称直接取注解的 value"ID":
5.3 场景三:对比 getNamedParams 的返回结果
在getNamedParams上打断点,对比两种写法的最终产物:
不写 @Param(list(Integer id)):hasParamAnnotation = false且paramCount == 1,命中分支二,直接返回实参本身(args[0]),而不是 Map:
写 @Param(list(@Param("ID") Integer id)):虽然只有一个参数,但hasParamAnnotation = true,命中分支三,返回的param是ParamMap,包含{"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 绑定参数之间的"翻译官",其设计要点可以浓缩为一张规则表:
| 场景 | hasParamAnnotation | names 内容 | getNamedParams 返回 |
|---|---|---|---|
| 无参数 | false | 空 | null |
| 单参数、无 @Param | false | {0 -> "arg0"}等 | 裸实参args[0] |
| 多参数或存在 @Param | true | @Paramvalue / 真实参数名 / 索引 | ParamMap(具名 key +param1/param2...) |
从调用链看,MapperMethod#execute→MethodSignature#convertArgsToSqlCommandParam→ParamNameResolver#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 中间件等
相关推荐
MyBatis 参数名解析器 ParamNameResolver 源码解析:从 @Param 注解到多参数绑定的底层原理
MyBatis 参数名解析器 ParamNameResolver 源码解析:从 @Param 注解到多参数绑定的底层原理 导读 Mapper 接口方法中的形参是
文档教程技术博客知识库MyBatis Alias 别名机制源码解析:从 TypeAliasRegistry 到 @Alias 注解的完整实现
MyBatis Alias 别名机制源码解析:从 TypeAliasRegistry 到 @Alias 注解的完整实现 MyBatis 允许开发者在 mybat
文档教程技术博客知识库MyBatis Alias 别名机制源码剖析:从注解到 TypeAliasRegistry 的完整注册链路
MyBatis Alias 别名机制源码剖析:从注解到 TypeAliasRegistry 的完整注册链路 导读 本文以 doocs/source code h
文档教程知识库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考