在使用低版本的Mybatis的时候,Mapper中的方法如果有多个参数时需要使用@param注解,才能在对应xml的sql语句中使用参数名称获取传入方法的参数值,否则就会报错。本文结合自身在真实开发环境中使用IDEA开发时遇到的问题来共同探讨一下不使用@Param注解报错背后的原因以及解决方案。
问题描述
最近使用IDEA进行开发,项目使用SpringBoot+Mybatis3.4.6,同样的代码检出到本地IDEA后运行,在一个业务查询模块报错,后台打印日志如下:
mybatis出现该错误的原因分析:我们正在调用一个具有多参数的mapper接口方法,对这个方法的调用其实是对mapper对应的xml中的一个sql的调用,并且我们在这个sql语句中使用#{方法参数名称}的方式构建动态SQL,但是要想在sql语句中使用参数名称获取参数值那么需要对mapper接口对应方法的每一个参数使用@Param注解,Param注解非常简单,源代码如下:
@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选项后,再运行结果如下:
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。