尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MyBatis 源码解析:ParamNameResolver 与 @Param 注解的参数名解析机制

MyBatis 源码解析:ParamNameResolver 与 @Param 注解的参数名解析机制 文档教程知识库【免费下载链接】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; }这里的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 SortedMapInteger, String names; private boolean hasParamAnnotation; public ParamNameResolver(Configuration config, Method method) { // 方法参数类型 final Class?[] paramTypes method.getParameterTypes(); // 参数上的注解 final Annotation[][] paramAnnotations method.getParameterAnnotations(); // 参数索引和参数名称 // {参数索引:参数名称} final SortedMapInteger, 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 MapString, Object param new ParamMap(); int i 0; for (Map.EntryInteger, 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用于生成param1、param2...namesSortedMapInteger, 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 SortedMapInteger, String map new TreeMap(); int paramCount paramAnnotations.length;通过反射拿到方法的所有参数类型和每个参数上的注解二维数组paramAnnotations[i]表示第 i 个参数上的全部注解并用TreeMap暂存结果保证最终按参数索引有序输出。3.2 第二步跳过特殊参数RowBounds / ResultHandlerif (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()); }这里存在两级兜底真实参数名当Configuration.isUseActualParamName()为true对应 MyBatis 配置项setting nameuseActualParamName valuetrue/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 分支一无参数直接返回 nullif (args null || paramCount 0) { return null; }方法没有业务参数names为空或者实参为null直接返回nullSQL 不需要任何绑定参数。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 MapString, Object param new ParamMap(); int i 0; for (Map.EntryInteger, 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注解的方法实参都会被包装进一个ParamMapMapString, Object的容器类。每个参数会以两种 key 进入 Map具名 keyentry.getValue()即Param的 value如ID或真实参数名如id通用 keyparam1、param2...规则是前缀GENERIC_NAME_PREFIXparam 从 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); ListHsSell list mapper.list(2); ListObject objects sqlSession.selectList(com.huifer.mybatis.mapper.HsSellMapper.list); assertEquals(list.size(), objects.size()); }5.1 场景一不写 Param观察构造结果Mapper 方法定义为ListHsSell list(Integer id);debug 时观察ParamNameResolver的names字段由于没有Param注解hasParamAnnotation false参数名按useActualParamName规则生成或降级为索引得到{0 - arg0}之类的映射5.2 场景二写 Param观察构造结果改为ListHsSell list(Param(ID) Integer id);此时注解扫描分支命中ParamhasParamAnnotation truenames中该参数的名称直接取注解的 valueID5.3 场景三对比 getNamedParams 的返回结果在getNamedParams上打断点对比两种写法的最终产物不写 Paramlist(Integer id)hasParamAnnotation false且paramCount 1命中分支二直接返回实参本身args[0]而不是 Map写 Paramlist(Param(ID) Integer id)虽然只有一个参数但hasParamAnnotation true命中分支三返回的param是ParamMap包含{ID: 2, param1: 2}两个键值对——具名 key 与通用 key 并存两组 debug 截图清晰地印证了源码逻辑Param的存在与否决定了返回的是裸实参还是ParamMap而在 ParamMap 模式下Param的 value 和paramN通用名会同时写入。六、实战要点与常见问题结合上面的源码行为整理出以下可直接指导日常编码的要点1. 单参数无 Param时的最小写法ListHsSell list(Integer id);此时 SQL 中#{id}、#{value}、#{anything}都无所谓因为返回的是裸实参MyBatis 按位置绑定。2. 多参数必须用 Param 或通用名引用ListHsSell list(Param(id) Integer id, Param(name) String name);多参数场景下返回的是ParamMapSQL 中可以写#{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 testid ! null等test表达式、#{}占位符中的名称最终解析时面对的就是getNamedParams组装出的ParamMap或裸实参因此命名规则与上述一致。相关动态 SQL 解析可进一步参考 Mybatis-DynamicSqlSource.md 与 2、SqlNode和SqlSource.md。七、总结ParamNameResolver是 MyBatis Mapper 方法参数到 SQL 绑定参数之间的翻译官其设计要点可以浓缩为一张规则表场景hasParamAnnotationnames 内容getNamedParams 返回无参数false空null单参数、无 Paramfalse{0 - arg0}等裸实参args[0]多参数或存在 ParamtrueParamvalue / 真实参数名 / 索引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 中间件等项目地址https://gitcode.com/doocs/source-code-hunter点击查看免费下载相关推荐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),仅供参考
返回列表