现象看本质 | 由mybatis接口传递多参数引发的思考

背景

最近一个小学弟问我,他使用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 参数,然后通过反射获取(接口和类都能使用)

后记

通过现象看问题原因,通过原因找到问题本质,有时候看似一个简单的问题,背后却隐藏着很多细节知识点。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值