浅析 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 编译参数,看看程序是否报错,所幸,一切正常。😃
二、总结
- 由于 Controller 中的方法是非抽象方法,SpringMVC 可以通过局部变量表获取方法的参数名称;MyBatis 中 Mapper 接口的方法都是抽象方法没有方法体,所以没有 Code 属性,自然也没有局部变量表,无法获取方法的参数名称,只能通过 @Param 来标志方法的参数名称。
- 通过在 javac 编译时加入 -parameters 属性可以将方法参数名称这样的元信息加入到字节码文件的 MethodParameters 属性中,从而保证 Parameter 对象的 getName 方法可以获取到方法的参数名称。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于