浅析 SpringMVC 和 MyBatis 方法参数注入

本贴最后更新于 1405 天前,其中的信息可能已经斗转星移

浅析 SpringMVC 和 MyBatis 方法参数注入

后端项目中,我们只需要关注 Controller、Service、DAO 三层,而其他层由于其通用性,框架已经帮我们做好了。

本篇博客浅析框架如何注入方法参数,主要内容如下:

  • SpringMVC 中 Controller 方法参数注入
  • MyBatis 中 Mapper 方法参数注入

一、实例

本篇博客以实现查找一个地区的同名用户接口为例,jdk 版本 1.8

浏览器发送 HTTP GET 请求,URL 为**xxx/api/vi/nameSameArea?name=ccran&area=china**

1.1 Controller 方法参数注入

Controller 控制器代码可能如下所示:

@RestController
@RequestMapping("api/v1/")
public class MyController {
    @Autowired
    MyService myService;
  
	@GetMapping(path = "nameSameArea")
    public Response nameSameArea(String name,String area) {
    	return Response.ok(myService.nameSameArea(name,area));
    }
}

1.2 Mapper 方法参数注入

Service 最终调用的 Mapper 代理,其接口代码可能如下所示:

public interface MyMapper{
    @Select("select count(*) from xxx where name=#{name} and area=#{area}")
    int countNameSameArea(@Param("name")String name,@Param("area")String area);
}

1.3 浅析

显然,参数的成功注入依赖于框架,而框架则依赖于反射完成方法参数的正确注入,我们只需要反射拿到方法的参数名称就行了。

  • 对于 Controller 方法参数注入而言,SpringMVC 会在 HttpServletRequest 中拿到对应参数名称作为 key 的 value 即可正确注入方法参数。
	// 反射获取nameSameArea方法
	Method method = MyController.class.
                getDeclaredMethod("nameSameArea",
                        String.class,
                        String.class);
	// 获取参数名称构造入参
        Parameter[] parameters = method.getParameters();
        Object[] params = new Object[parameters.length];
        for (int i = 0; i < params.length; i++) {
            params[i] = request.getParameters(parameters[i].getName());
        }
	// 正确调用nameSameArea方法
        method.invoke(myController, params);
  • 对于 Mapper 方法参数注入而言,MyBatis 将入参封装成 Map,并替换#{name},#{area}占位符为 Map 中 key 为 name 以及 area 的值即可。
	// 由于创建的是代理对象,可以直接拿到method
	Map<String, Object> argsMap = new HashMap<>();
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            argsMap.put(parameters[i].getName(), args[i]);
        }
	// 根据Select注解的值以及参数Map生成sql
        String sql = generateSql(method.getAnnotation(Select.class).value(), argsMap);
	// 后续执行sql并获取结果封装成pojo返回

根据以上分析,可以通过 Parameter 对象的 getName 方法拿到参数名称。

但是,我们知道,Mapper 接口中,不加入 @Param 注解,我们是无法正确注入参数的。而在 Controller 中,即使不加 @RequestParam 注解,我们也能正确注入参数,这是为什么呢?是不是因为 Controller 中的是非抽象方法,Mapper 中的是抽象方法呢?

我们创建一个实例来看看怎么回事:

创建抽象类 ReflectMethodParamDemo,其中 method 是非抽象方法,abstractMethod 是抽象方法,在 main 方法中打印两个方法的参数名称。

public abstract class ReflectMethodParamDemo {
    public abstract void abstractMethod(String name, String area);

    public void method(String name, String area) {
        System.out.println(name + ":" + area);
    }
  
    public static void main(String[] args){
        printParamName("abstractMethod",String.class,String.class);
        printParamName("method",String.class,String.class);
    }

    // 打印方法的参数名称
    public static void printParamName(String name, Class<?>... parameterTypes){
        Method method = null;
        try {
            method = ReflectMethodParamDemo.class.getDeclaredMethod(name,
                    parameterTypes);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        Parameter[] parameters = method.getParameters();
        for (Parameter parameter : parameters) {
            System.out.print(parameter.getName()+"\t");
        }
        System.out.println();
    }
}

输出如下:

arg0 arg1
arg0 arg1

可见,好像和方法是否抽象没什么关系,反射本质是去访问类的元信息,而类的元信息都在 class 文件里面。

因此,我们用 javap 解析一下 ReflectMethodParamDemo 的字节码文件看看。

 public abstract void abstractMethod(java.lang.String, java.lang.String);
    descriptor: (Ljava/lang/String;Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_ABSTRACT

  public void method(java.lang.String, java.lang.String);
    descriptor: (Ljava/lang/String;Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
	 LineNumberTable:
	 LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      30     0  this   Lcom/ccran/jvm/ReflectMethodParamDemo;
            0      30     1  name   Ljava/lang/String;
            0      30     2  area   Ljava/lang/String;

我们可以看到,在非抽象方法的 Code 属性的子属性 LocalVariableTable 中有 name、area 的常量信息。而在抽象方法中,因为它没有方法体,所以不会有 Code 属性,所以没有 name、area 的常量信息。

因此,我们大胆猜测,SpringMVC 是通过局部变量表获取方法的参数名称。而在 MyBatis 中,因为 Mapper 文件中都是抽象方法,没有任何地方保存方法的参数名称,我们只能通过 @Param 注解来标志方法的参数名称。

那 Parameter 对象的 getName 方法是如何获取方法的参数名称的呢。

查阅资料后,我们可以在用 javac 编译.java 文件的时候加入 -parameters 参数,重新编译 ReflectMethodParamDemo 并执行

输出如下:

name area
name area

成功输出了方法的参数名称 😄,通过 javap 解析 class 文件看看都发生了什么。

 public abstract void abstractMethod(java.lang.String, java.lang.String);
    descriptor: (Ljava/lang/String;Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_ABSTRACT
    MethodParameters:
      Name                           Flags
      name
      area

可以发现方法每个方法后面多了一个 MethodParameters 属性,看来 Parameter 对象的 getName 方法就是访问 MethodParameters 属性中的信息,从而获取方法的参数名称,如果是空则给我们默认的 arg0,arg1 这样的名称。

在 IDEA 中,可以进入 File---Settings---Build,Execution,Deployment---Compiler---Java Compiler

在 Additional command line parameters 中加入 -parameters 参数

最后,我们做个测试,在 Mybatis 的 Mapper 文件中移除方法参数的 @Param 注解,加入 -parameters 编译参数,看看程序是否报错,所幸,一切正常。😃

二、总结

  1. 由于 Controller 中的方法是非抽象方法,SpringMVC 可以通过局部变量表获取方法的参数名称;MyBatis 中 Mapper 接口的方法都是抽象方法没有方法体,所以没有 Code 属性,自然也没有局部变量表,无法获取方法的参数名称,只能通过 @Param 来标志方法的参数名称。
  2. 通过在 javac 编译时加入 -parameters 属性可以将方法参数名称这样的元信息加入到字节码文件的 MethodParameters 属性中,从而保证 Parameter 对象的 getName 方法可以获取到方法的参数名称。
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    941 引用 • 1458 回帖 • 150 关注
  • MyBatis

    MyBatis 本是 Apache 软件基金会 的一个开源项目 iBatis,2010 年这个项目由 Apache 软件基金会迁移到了 google code,并且改名为 MyBatis ,2013 年 11 月再次迁移到了 GitHub。

    170 引用 • 414 回帖 • 429 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...