背景
最近一个小学弟问我,他使用mybatis进行多参数查询时,在本地明明可以正常测试通过的代码,到服务器了就不能正常执行了,后来别人告诉他要在mybatis的接口加上@Param,后来他问我为什么本地不加注解就可以过,服务器就不行,而且springmvc可以直接使用参数名进行映射,mybatis就必须要使用类似@Param这种方式对参数名进行声明,不用@Param修饰行不行?大概的场景如下:
// SpringMVC Controller
@RequestMapping("/getUsers")
public List<User> getUsers(String adCode, String userName) {
.....
return list;
}
//MyBatis Dao
public List<User> selectUsers(@Param("adCode")String adCode, @Param("userName")String userName);
public List<User> selectUsers(String adCode, String userName);
//MyBatis mapper.xml
<select id="selectUsers" resultMap="xxx">
select * from user where adCode = #{adCode} and userName = #{userName}
</select>
当时我给他细致的解答了一下,解答完成后,我让他再自己动手实验一下,加深一下印象。后来闲下来了之后我思考了一下,这个看似简单的问题,其实包含了很多的知识。下面给大家分享一下这个问题查找原因的一种思路。
Mybatis的传参方式
首先要弄清楚这个问题,我们需要知道mybatis有哪些传参的方式以及其中的原理。我们逐个进行分析,看看每种传参的方式有何异同。
单个参数的传参方式
单个参数的方式一般我们可以定义单个参数为如下类型:
1、基本类型及其包装类:long、Long等
2、POJO对象:xxx.User
3、Map
4、集合
5、数组
参数为基本类型或者包装类时
正常的书写方式:
//MyBatis Dao
public User selectUser(Long userId);
<select id="selectUser" parameterType="java.lang.Long" resultMap="xxx">
select * from user where userId = #{userId}
</select>
其实这种参数的场景,和变量名没有关系,你甚至都可以用下面的方式进行接收
<select id="selectUser" parameterType="java.lang.Long" resultMap="xxx">
select * from user where userId = #{XXXXX}
</select>
原因是mybatis在针对这种包装类型的变量时,使用的是parameterType表示的Java类型进行设置的。
参数为POJO对象
正常的书写方式:
//MyBatis Dao
public User selectUser(User user);
<select id="selectUser" parameterType="xxxx.User" resultMap="xxx">
select * from user where userId = #{userId}
</select>
这个时候,我们在sql中并没有使用接口上定义的“user”这个参数,而是使用user对象中的属性,这是因为mybatis将这种类型参数或者值的方式使用的是反射方式,直接获取其属性值。
参数为Map
正常的书写方式:
//MyBatis Dao
public User selectUser(Map<String,Object> map);
<select id="selectUser" parameterType="java.lang.Map" resultMap="xxx">
select * from user where userId = #{userId}
</select>
这种方式接口中定义的名称为“map”也没有实际用途,和POJO类似,只不过是内置的MapWrapper方式包装了原始参数,使用get方法获取的。
参数为集合
正常的书写方式:
//MyBatis Dao
public User selectUsers(List<Long> userIds);
<select id="selectUsers" parameterType="java.util.List" resultMap="xxx">
select * from user where userId in
<foreach collection="list" item="pk" index="index" open="(" close=")" separator=",">
#{pk}
</foreach>
</select>
//或者
<select id="selectUsers" parameterType="java.util.List" resultMap="xxx">
select * from user where userId in
<foreach collection="collection" item="pk" index="index" open="(" close=")" separator=",">
#{pk}
</foreach>
</select>
大家平常习惯都是按照这两种方式书写,但是有认真思考过xml定义中为什么是“list” 和 “collection”,原因是mybatis在针对集合类的单个入参进行了包装,将参数改装成为了Map结构,并且向Map中添加了key为“collection”,值为原始集合的Entry,而且还判断如果是List类型的入参,还向Map中添加了key为“list”,值为原始集合的Entry。
实际还有一个变量,JDK8为接口中定义的名称userIds(Java编译使用了 -parameters参数),JDK8之前或者JDK8未开启-parameters编译时为参数名为 “arg0”,也是放在上文的Map结构中的。key为“arg0” 或者 “userIds”,值为原始集合的Entry。
但是上面这种方式在实际执行的时候和“list” 和 “collection”一点关系都没有,而是使用了附加参数,附加参数是一个Map结构,在参数进行绑定时,将集合参数转化为附加参数,实际执行过程中使用附加参数进行替换,比如上述场景userIds的值为 [8001,8002,8003]时,附加参数的结构为:
{
"__frch_pk_0": 8001,
"__frch_pk_1": 8002,
"__frch_pk_2": 8003,
}
// 其中key的规则为:__frch_ + xml中foreach中的item对应的值 + 序号
// __frch_ 是foreach标签默认的特殊字符标记
实际执行中SQL形式也会发生变化,所需要的参数被转换成“foreach特殊字符 + item值 + 序号”的特殊格式,你可以像这样进行理解:
<select id="selectUsers" parameterType="java.util.List" resultMap="xxx">
select * from user where userId in (__frch_pk_0, __frch_pk_1, __frch_pk_2)
</select>
参数为数组
这种方式和参数为集合的方式类似,只不过使用的名称为 "array" 或者 “arg0” 或者 “userIds”
多个参数的传参方式
我们来看看多参数的场景
//MyBatis Dao
public List<User> selectUsers(String adCode, String userName);
//MyBatis mapper.xml
<select id="selectUsers" resultMap="xxx">
select * from user where adCode = #{adCode} and userName = #{userName}
</select>
如果你这样写了代码,并且能正常在本地测试通过,那么请尝试将JDK调到JDK7或者关闭掉编译参数 -parameters参数。你会发现,你本来好好的代码,确不能执行通过了,所以这种情况我们可以使用以下几种方式:
使用内置参数名
//MyBatis Dao
public List<User> selectUsers(String adCode, String userName);
//MyBatis mapper.xml
<select id="selectUsers" resultMap="xxx">
select * from user where adCode = #{param1} and userName = #{param2}
</select>
这种方式是mybatis为了解决不使用注解方式进行多参数传递的默认方法。当然在JDK8之前或者JDK8未开启 -parameter参数时还可以使用arg0,arg1代替param1,param2,这种方式和使用 “adCode”、“userName”一样,可能会踩JDK的坑
使用@Param
这种方式是比较常见的场景,就不在叙述了,方式如下:
//MyBatis Dao
public List<User> selectUsers(@Param("adCode")String adCode, @Param("userName")String userName);
//MyBatis mapper.xml
<select id="selectUsers" resultMap="xxx">
select * from user where adCode = #{param1} and userName = #{param1}
</select>
使用Map或者POJO
这个就不再细说了,这个和单个参数类类似。可以传递多个参数。
问题回顾
到这里,学弟的前半个问题已知其原因,就是他本地使用了JDK8的版本,并且常见的IDE都开启了 -parameters编译参数。但是生产环境发布前编译并没有使用-parameters参数,另外一种的情况就是生产环境使用了JDK8之前版本,不过这种可能性基本没有。
那么针对这种场景,解决的办法除了加注解外,还可以在编译阶段加上-parameters即可,maven插件中可以加额外的编译参数
那么-parameters到底是干什么的,为什么springmvc就可以正常获取到参数名。
问题本质
-parameters作用
我们再来看看问题的本质,首先是-parameters到底有个什么作用。
这个参数的作用官方文档已经说的够清楚了,就是将构造函数和方法的形式参数名称存储在生成的类文件中,方便在反射API中的方法 java.lang.reflect.Executable.getParameters可以检索它们。
Stores formal parameter names of constructors and methods in the generated class file so that the method java.lang.reflect.Executable.getParameters from the Reflection API can retrieve them.
官方文档:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/javac.html
尝试一下:
public interface IUserDao {
public Object selectUser(Long userId);
public List<Object> selectUsers(String adCode, String userName);
}
不开启-parameters进行编译后查看:
javac IUserDao.java
javap -v IUserDao.class
Classfile /src/main/java/IUserDao.class
Last modified 2021-7-9; size 333 bytes
MD5 checksum 21159b20eb5387be668d483226afac74
Compiled from "IUserDao.java"
public interface IUserDao
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
#1 = Class #11 // IUserDao
#2 = Class #12 // java/lang/Object
#3 = Utf8 selectUser
#4 = Utf8 (Ljava/lang/Long;)Ljava/lang/Object;
#5 = Utf8 selectUsers
#6 = Utf8 (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List;
#7 = Utf8 Signature
#8 = Utf8 (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List<Ljava/lang/Object;>;
#9 = Utf8 SourceFile
#10 = Utf8 IUserDao.java
#11 = Utf8 IUserDao
#12 = Utf8 java/lang/Object
{
public abstract java.lang.Object selectUser(java.lang.Long);
descriptor: (Ljava/lang/Long;)Ljava/lang/Object;
flags: ACC_PUBLIC, ACC_ABSTRACT
public abstract java.util.List<java.lang.Object> selectUsers(java.lang.String, java.lang.String);
descriptor: (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List;
flags: ACC_PUBLIC, ACC_ABSTRACT
Signature: #8 // (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List<Ljava/lang/Object;>;
}
SourceFile: "IUserDao.java"
开启-parameters后进行编译查看:
javac -parameters IUserDao.java
javap -v IUserDao.class
Classfile /src/main/java/IUserDao.class
Last modified 2021-7-9; size 407 bytes
MD5 checksum 46eb2a444b5f018082d9c8d6bb8c4ded
Compiled from "IUserDao.java"
public interface IUserDao
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
#1 = Class #15 // IUserDao
#2 = Class #16 // java/lang/Object
#3 = Utf8 selectUser
#4 = Utf8 (Ljava/lang/Long;)Ljava/lang/Object;
#5 = Utf8 MethodParameters
#6 = Utf8 userId
#7 = Utf8 selectUsers
#8 = Utf8 (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List;
#9 = Utf8 adCode
#10 = Utf8 userName
#11 = Utf8 Signature
#12 = Utf8 (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List<Ljava/lang/Object;>;
#13 = Utf8 SourceFile
#14 = Utf8 IUserDao.java
#15 = Utf8 IUserDao
#16 = Utf8 java/lang/Object
{
public abstract java.lang.Object selectUser(java.lang.Long);
descriptor: (Ljava/lang/Long;)Ljava/lang/Object;
flags: ACC_PUBLIC, ACC_ABSTRACT
MethodParameters:
Name Flags
userId
public abstract java.util.List<java.lang.Object> selectUsers(java.lang.String, java.lang.String);
descriptor: (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List;
flags: ACC_PUBLIC, ACC_ABSTRACT
MethodParameters:
Name Flags
adCode
userName
Signature: #12 // (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List<Ljava/lang/Object;>;
}
SourceFile: "IUserDao.java"
对比发现,使用-parameters后,字节码文件中增加了MethodParameters项。java.lang.reflect.Executable.getParameters进行反射获取时,即通过字节码就能拿到参数。
springmvc为什么获取到
现在开始来看看学弟的后半个问题,为什么springmvc可以在没有-parameters编译的情况下,能获取到controller中的参数名。跟踪springmvc源代码发现。它使用的是接口为ParameterNameDiscoverer进行方法参数名的识别。
那么为什么有这个工具了,mybatis不使用呢?答案是spring的这个工具并不能解决mybatis的问题,读者可以自行尝试
public interface IUserDao {
public List<Object> selectUsers(String adCode, String userName);
}
ParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
Method method = IUserDao.class.getMethod("selectUsers", new Class[] {String.class, String.class});
System.out.println(Arrays.toString(discoverer.getParameterNames(method))); // null
可是明明springmvc中controller就能使用,mybatis为什么不能使用,发现springmvc的controller是class类,mybatis是接口类型,读者可以尝试如下代码
public class UserDao {
public List<Object> selectUsers(String adCode, String userName){
return null;
}
}
ParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
Method method = IUserDao.class.getMethod("selectUsers", new Class[] {String.class, String.class});
System.out.println(Arrays.toString(discoverer.getParameterNames(method))); // [adCode, userName]
难道接口(interface)在编译没有-parameters参数时,编译后字节码中没有保留参数名,类(class)在编译没有-parameters参数时,编译后字节码中就保留了参数名,答案是否定的,这里给大家看看不带参数编译后内容,可以发现不带参数编译后的内容中并没有参数名
javac UserDao.java
javap -v UserDao.class
Classfile /src/main/java/UserDao.class
Last modified 2021-7-9; size 396 bytes
MD5 checksum ef8ccc11865474541e3c41451654f4a8
Compiled from "UserDao.java"
public class UserDao
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#14 // java/lang/Object."<init>":()V
#2 = Class #15 // UserDao
#3 = Class #16 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 selectUsers
#9 = Utf8 (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List;
#10 = Utf8 Signature
#11 = Utf8 (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List<Ljava/lang/Object;>;
#12 = Utf8 SourceFile
#13 = Utf8 UserDao.java
#14 = NameAndType #4:#5 // "<init>":()V
#15 = Utf8 UserDao
#16 = Utf8 java/lang/Object
{
public UserDao();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
public java.util.List<java.lang.Object> selectUsers(java.lang.String, java.lang.String);
descriptor: (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List;
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=3
0: aconst_null
1: areturn
LineNumberTable:
line 8: 0
Signature: #11 // (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List<Ljava/lang/Object;>;
}
SourceFile: "UserDao.java"
那么springmvc为什么就能获取到参数名呢?
原因是另外一个编译参数 javac -g
生成所有调试信息,包括局部变量。 默认情况下,只生成行号和源文件信息。
Generates all debugging information, including local variables. By default, only line number and source file information is generated.
我们使用-g进行反编译查看:
javac -g UserDao.java
javap -v UserDao.class
Classfile /src/main/java/UserDao.class
Last modified 2021-7-9; size 533 bytes
MD5 checksum 5d603bb8dc44e78b69c15e32f8197033
Compiled from "UserDao.java"
public class UserDao
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#20 // java/lang/Object."<init>":()V
#2 = Class #21 // UserDao
#3 = Class #22 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 LUserDao;
#11 = Utf8 selectUsers
#12 = Utf8 (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List;
#13 = Utf8 adCode
#14 = Utf8 Ljava/lang/String;
#15 = Utf8 userName
#16 = Utf8 Signature
#17 = Utf8 (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List<Ljava/lang/Object;>;
#18 = Utf8 SourceFile
#19 = Utf8 UserDao.java
#20 = NameAndType #4:#5 // "<init>":()V
#21 = Utf8 UserDao
#22 = Utf8 java/lang/Object
{
public UserDao();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LUserDao;
public java.util.List<java.lang.Object> selectUsers(java.lang.String, java.lang.String);
descriptor: (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List;
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=3
0: aconst_null
1: areturn
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 2 0 this LUserDao;
0 2 1 adCode Ljava/lang/String;
0 2 2 userName Ljava/lang/String;
Signature: #17 // (Ljava/lang/String;Ljava/lang/String;)Ljava/util/List<Ljava/lang/Object;>;
}
SourceFile: "UserDao.java"
发现在字节码中生成了LocalVariableTable(局部变量表),参数名被保存到局部变量表中了,这个时候就能获取到,但是局部变量表在接口定义的抽象方法中是不存在的。感兴趣的读者可以自行尝试。
所以springmvc能读取到变量名是通过 在编译期通过 -g参数生成局部变量表,然后通过局部变量表获取到具体参数。如果没有-g编译的参数,那么springmvc也不能获取到参数名。
我们常用的maven在编译的时候会默认带上 -g参数
源码地址:https://github.com/apache/maven-compiler-plugin 感兴趣的可以自行查看
/**
* Set to <code>true</code> to include debugging information in the compiled class files.
*/
@Parameter( property = "maven.compiler.debug", defaultValue = "true" )
private boolean debug = true;
/**
* Keyword list to be appended to the <code>-g</code> command-line switch. Legal values are none or a
* comma-separated list of the following keywords: <code>lines</code>, <code>vars</code>, and <code>source</code>.
* If debug level is not specified, by default, nothing will be appended to <code>-g</code>.
* If debug is not turned on, this attribute will be ignored.
*
* @since 2.1
*/
@Parameter( property = "maven.compiler.debuglevel" )
private String debuglevel;
那么感兴趣的读者可以进行如下配置之后,进行maven编译,然后去查看编译后的字节码信息,里面是没有局部变量表和方法参数名
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<debug>false</debug>
<parameters>false</parameters>
</configuration>
</plugin>
另外springboot 2.x中也自动带上了-parameters参数,所以springboot 2.x自动解决了获取参数名称的问题,这也可以算做是Springboot2.x需要至少JDK8的原因之一
springboot2.x spring-boot-starter-parent.pom.xml中
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
总结
通篇阅读后读者会发现,mybatis传输多个参数时,为什么需要指定上注解原因就一目了然了。springmvc能获取到参数名也有结论了,所以本质上在Java中通过反射能够获取方法参数名称的方式主要有两种:
1、编译时添加 -g 参数,然后通过解析字节码读取局部变量表获取(接口没有局部变量表)
2、JDK8及以上,编译时添加 -parameters 参数,然后通过反射获取(接口和类都能使用)
后记
通过现象看问题原因,通过原因找到问题本质,有时候看似一个简单的问题,背后却隐藏着很多细节知识点。