Mybatis Mapper中多参数方法不使用@param注解报错的解决

目录
  • 问题描述
  • 寻求解决方案
  • 寻找原因
  • 拓展延伸

在使用低版本的Mybatis的时候,Mapper中的方法如果有多个参数时需要使用@param注解,才能在对应xml的sql语句中使用参数名称获取传入方法的参数值,否则就会报错。本文结合自身在真实开发环境中使用IDEA开发时遇到的问题来共同探讨一下不使用@Param注解报错背后的原因以及解决方案。

问题描述

最近使用IDEA进行开发,项目使用SpringBoot+Mybatis3.4.6,同样的代码检出到本地IDEA后运行,在一个业务查询模块报错,后台打印日志如下:

mybatis出现该错误的原因分析:我们正在调用一个具有多参数的mapper接口方法,对这个方法的调用其实是对mapper对应的xml中的一个sql的调用,并且我们在这个sql语句中使用#{方法参数名称}的方式构建动态SQL,但是要想在sql语句中使用参数名称获取参数值那么需要对mapper接口对应方法的每一个参数使用@Param注解,Param注解非常简单,源代码如下:

/**
 * @author Clinton Begin
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Param {
  String value();
}

它只有一个value属性,这里的value就等于mapper对应的xml文件中获取参数值时要使用的key。于是我找到了对应报错的代码发现正是因为多参数方法没有使用@Param注解,在我加上该注解后便没有错误了。

到这里事情看上去好像已经解决了,但是并没有这么简单,我查看了很多mapper发现,有很多具有多个参数的mapper方法都没有使用这个注解,按照这种修改方式,我岂不是要把几乎所有的mapper都修改一遍,并且我是刚刚检出的最新代码,代码不应该有问题才对,于是询问同事发现他们在自己的IDEA运行时并没有我这个错误,所以说并不是@Param注解的问题。

寻求解决方案

同样的代码,在不同的机器上运行出现了不同的结果,那么肯定有什么不一样的地方,首先JDK都一样,系统环境也一样,运行方式也一样,下来就是运行环境IDEA,那么IDEA是否有区别呢?

询问同事发现他们用的是比较新的版本2019.2.3,而我用的是2018.2.2版本,所以初步怀疑是IDEA的版本问题,但是好像按理来说不应该是IDEA的问题,真正运行JAVA字节码的是本地的JRE环境,貌似和IDEA关系不大,但是这是目前唯一的线索,无论如何都要试一下。

于是我下载了最新版本的IDEA,然后导入代码,运行,结果发现竟然真的没有报错!这时候问题虽然解决了,但是为什么会这样,背后的原因是什么,和IDEA版本有什么关系呢?这些问题如鲠在喉,让我茶不思,饭不想…

寻找原因

当一个问题无法知道背后的真正原因时,那么就算解决了也只是暂时的。为了寻求真正的答案,我决定使用调试代码的方式看一下mybatis执行查询过程中是如何处理mapper接口方法的参数名称的,最终找到了org.apache.ibatis.reflection.ParamNameResolver这个类,看类名就可以知道这是处理参数名称的类,主要逻辑集中在它的构造方法:

  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;
    // get names from @Param annotations
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
      if (isSpecialParameter(paramTypes[paramIndex])) {
        // skip special parameters
        continue;
      }
      String name = null;
      for (Annotation annotation : paramAnnotations[paramIndex]) {
        if (annotation instanceof Param) {
          hasParamAnnotation = true;
          name = ((Param) annotation).value();
          break;
        }
      }
      if (name == null) {
        // @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.put(paramIndex, name);
    }
    names = Collections.unmodifiableSortedMap(map);
  }

接下来分析一下主要逻辑,首先看到的是需要获取Param注解中的Value值:

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

这里的name变量就是后面构造动态sql时,用于获取方法参数值的key,也就是你在xml文件中通过#{ }的方式获取动态参数时的参数key。接下来看到的代码是:

      if (name == null) {
        // @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());
        }
      }

这里可以看到再次判断name是否为null,如果为null则判断config.isUseActualParamName()是否为true,如果是true则通过getActualParamName(method, paramIndex)方法获取name,这些都执行完成如果name还是null,那么就是最后的逻辑: name = String.valueOf(map.size());也就是说name等于当前方法参数的位置(“0”, “1”, …),源码的注释也说明了这一点:

use the parameter index as the name (“0”, “1”, …)

那么getActualParamName(method, paramIndex)方法获取name是什么逻辑呢?接下来继续看:

首先要进入这个方法的前提是config.isUseActualParamName()为true:

public boolean isUseActualParamName() {
    return useActualParamName;
  }

config其实是mybatis的配置对象,这里面的配置项目可以影响mybatis的行为,具体配置项目可以从mybatis官方文档查询,这里我们就看一下useActualParamName参数的含义,官方文档 是这样描述的:

设置名 描述 有效值 默认值
useActualParamName 允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。(新增于 3.4.1) true 或者 false true

所以说这个属性其实就是允许我们使用mapper接口方法的参数名称当作sql语句的参数名称,而且也不需要@Param注解,这个属性默认是开启的,使用这个特性还有以下几个要求:

①采用 Java 8 编译。

②编译时加上-parameters 选项。

③mybatis在3.4.1以上

到这里基本上可以确定真正的原因了,首先我和同事的JDK都是1.8,Mybatis的版本在文章开头也说过了是3.4.6,所以只剩下-parameters选项,所以我怀疑是低版本的IDEA没有这个选项,高版本的IDEA在编译时可能默认加了这个选项。于是对比两个版本的编译设置如下:

①老版本(2018.2.2):

②新版本(2019.2.3):

果然如我们所料,新版本的IDEA编译设置里面默认添加了-parameters选项,所以在mybatis的配置项useActualParamName为true的时候,对于多参数的mapper接口方法,可以不使用@Param注解,而在低版本的IDEA时并没有添加这个选项,所以会出错。

拓展延伸

在Java8之前,JAVA代码编译为class文件后,方法参数的类型固定,但是参数名称会丢失,所以当通过反射去获取方法参数名称的时候是不能够得到原本源代码中的参数名称的,Java编译器会丢掉这部分信息。从JDK1.8开始可以通过在编译时添加-parameters这个选项来明确告诉编译器我们需要保留方法参数的原本名称。

那么为什么不默认开启这个选项呢?可能是为了避免因为保留参数名而导致class文件过大或者占用更多的内存,又或者是有些参数可能会泄露安全信息吧。

最后我们亲自来写一段代码验证一下-parameters这个选项的作用:

public class Main {
    public static void main(String[] args) {
        Method[] methods = Main.class.getMethods();
        for (Method method:methods) {
            if ("parameterMethodTest".equals(method.getName())){
                Parameter[] parameters = method.getParameters();
                for (Parameter parameter:parameters) {
                    System.out.println(parameter.getName());
                }
            }
        }
    }
    public static void parameterMethodTest(int parameterOne,String parameterTwo,Object parameterThree){
        System.out.println("Hello World!");
    }
}

在以上这段代码中,通过反射获取parameterMethodTest的三个参数名称并打印出来,首先我们在IDEA的编译设置中去掉-parameters选项,运行结果如下:

可以看到这个时候参数名称变成了arg0,arg1…

加上-parameters选项后,再运行结果如下:

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Mybatis中传递多个参数的4种方法总结

    前言 现在大多项目都是使用Mybatis了,但也有些公司使用Hibernate.使用Mybatis最大的特性就是sql需要自己写,而写sql就需要传递多个参数.面对各种复杂的业务场景,传递参数也是一种学问. 下面给大家总结了以下几种多参数传递的方法. 方法1:顺序传参法 #{}里面的数字代表你传入参数的顺序. 这种方法不建议使用,sql层表达不直观,且一旦顺序调整容易出错. 方法2:@Param注解传参法 #{}里面的名称对应的是注解 @Param括号里面修饰的名称. 这种方法在参数不多的情况还

  • 解决MyBatis @param注解参数类型错误异常的问题

    问题现象 今天使用mybatis遇到个很奇怪的问题,我使用一个参数@param("threshold"),类型是java的double,但是很奇怪,一直告诉我参数转换错误,int不能转double,我就奇怪了,哪里来的int. 解决办法 我感觉可能使用用到了mybatis的关键字,所以就把threshold换了个名字,果然好了. 问题原因 某些关键词,mybatis会认为是某种类型,下面列出来一些,后面发现再补充. size, threshold, modCount是int类型 loa

  • mybatis多个接口参数的注解使用方式(@Param)

    1 简介 1.1 单参数 在 Mybatis 中, 很多时候, 我们传入接口的参数只有一个. 对应接口参数的类型有两种, 一种是基本的参数类型, 一种是 JavaBean . 例如在根据主键获取对象时, 我们只需要传入一个主键的参数即可. 而在插入, 更新等操作时, 一般会涉及到很多参数, 我们就使用 JavaBean . 1.2 多参数 但是, 在实际的情况中, 我们遇到类似这样的情况可能: 接口需要使用的参数多于一个: 接口需要使用的参数又远少于对应 JavaBean 的成员变量, 或者需要

  • Mybatis Mapper中多参数方法不使用@param注解报错的解决

    目录 问题描述 寻求解决方案 寻找原因 拓展延伸 在使用低版本的Mybatis的时候,Mapper中的方法如果有多个参数时需要使用@param注解,才能在对应xml的sql语句中使用参数名称获取传入方法的参数值,否则就会报错.本文结合自身在真实开发环境中使用IDEA开发时遇到的问题来共同探讨一下不使用@Param注解报错背后的原因以及解决方案. 问题描述 最近使用IDEA进行开发,项目使用SpringBoot+Mybatis3.4.6,同样的代码检出到本地IDEA后运行,在一个业务查询模块报错,

  • 在Vue中使用this.$store或者是$route一直报错的解决

    今天在引入路由参数和状态组件的时候发现一直报空找不到,但是我在控制台查看还能找得到 一直超费解,直到我在其他组件单独测试的时候又可以了 这是测试正常的 test1s : function(){ debugger var test1=this.$route.params.num; console.log(test1); return test1; } getRequest: (val)=> {//改变海黄学堂状态 debugger let thisRoute=this.$store.state.s

  • Spring Mvc中传递参数方法之url/requestMapping详解

    前言 相信大家在使用spring的项目中,前台传递参数到后台是经常遇到的事, 我们必须熟练掌握一些常用的参数传递方式和注解的使用,本文将给大家介绍关于Spring Mvc中传递参数方法之url/requestMapping的相关内容,分享出来供大家参考学习,话不多说,直接上正文. 方法如下 1. @requestMapping: 类级别和方法级别的注解, 指明前后台解析的路径. 有value属性(一个参数时默认)指定url路径解析,method属性指定提交方式(默认为get提交) @Reques

  • mybatis中使用InsertProvider注解报错解决全过程

    目录 使用InsertProvider注解报错解决 mybatis注解开发@InsertProvider Userprovider类 mapper的书写 使用InsertProvider注解报错解决 目前项目在使用mybatis,并且是使用注解的方式. 在使用InsertProvider注解的时候报了一下的错误: org.apache.ibatis.builder.BuilderException: Could not find value method on SQL annotation.  

  • idea springBoot项目自动注入mapper为空报错的解决方法

    在SpringBoot项目中,如果使用了MyBatis作为持久层框架,使用自动注入时可能会遇到mapper报空指针异常的问题.这是因为在自动注入时,SpringBoot无法正确识别MyBatis的Mapper接口,需要进行一些额外的配置.解决这个问题的方法有两种: 1.在Mapper接口上添加注解在Mapper接口上添加@Mapper注解,告诉SpringBoot这个接口是一个Mapper接口,需要进行代理.示例如下: @Mapper public interface UserMapper {

  • php中unable to fork报错简单解决方法

    今天小编遇到一个问题,当调用了system方法,并且执行了shell脚本,开始的时候,一切都非常正常,但是当程序运行后一段时间,出现了显示unable to fork的报错,这个是什么原因呢,后来小编排查了下,主要是因为达到用户的进程上限了,下面小编给大家介绍下解决方式. 限制linux用户的进程数 修改以下文件 vi /etc/security/limits.conf vpsee hard nproc 32 @student hard nproc 32 @faculty hard nproc

  • Android线程中设置控件的值提示报错的解决方法

    本文实例讲述了Android线程中设置控件的值提示报错的解决方法.分享给大家供大家参考,具体如下: 在Android线程中设置控件的值一般会与Handler联合使用,如下: package com.yarin.android.Examples_04_15; import android.app.Activity; import android.os.Bundle; import android.os.Handler; import android.os.Message; import andro

  • 基于php双引号中访问数组元素报错的解决方法

    最近在做微信公众号开发,在一个发送图文接口中,需要把数组元素拼接在XML字符串中 foreach ($itemArr as $key => $value){ $items .= "<item> <Title><![CDATA[$value['title']]]></Title> <Description><![CDATA[[$value['description']]]></Description> <

  • Vue2.x中利用@font-size引入字体图标报错的解决方法

    利用 vue-cli 搭建的项目平台 利用stylus写的css样式 有 css-loader 依赖包x 下图是 webpack.base.conf.js 关于字体文件的配置 有人这里会有重复的字体文件的配置,删除一项即可 出现的问题:引入字体图标出现问题 1.报错 将字体引入的相对路径改成绝对路径 相对路径 绝对路径 2.不报错,但是出现的字体图标是小方框 有警告信息: 小方块: 报错是因为重定向的问题 出现上述问题的原因 ①没在用到的地方引入字体的样式文件 ②使用的是后缀名为 .styl 文

  • vuex2中使用mapGetters/mapActions报错的解决方法

    解决方案 可以安装整个stage2的预置器或者安装 Object Rest Operator 的babel插件 babel-plugin-transform-object-rest-spread . 接着在babel的配置文件 .babelrc 中应用插件: { "presets": [ ["es2015", { "modules": false }] ], "plugins": ["transform-object

随机推荐