深入理解@ModelAttribute注解的工作过程

  在SpringMVC中,我们可以通过使用@ModelAttribute注解标记方法,实现类似于Struts2中Preparable拦截器的效果,其使用方法我们已经在《SpringMVC中如何处理模型数据》中讲述过了。
  现在我们仍以上篇文章中的更新操作为例,来讨论@ModelAttribute的工作过程。即:有一个User类,有id、userName、email三个属性。现在要完成一个更新操作,但是其中有一项属性不能被修改,例如id,那么只能修改两项属性,userName和email,所以从form表单传递的信息就只能有这两项,我们是通过@ModelAttribute注解标记方法来实现的:(其中数据库相关的操作仅采用模拟的方式)
  
index.jsp:

1
2
3
4
5
6
7
8
9
10
11
12
13
 <!--
模拟修改操作
1. 原始数据为: 1,Jack,Jack@163.com
2. 将username和email修改为Mike、Mike@qq.com,id不能被修改.
3. 表单回显, 模拟操作直接在表单填写对应的属性值
-->
<form action="springmvc/testModelAttribute" method="Post">
username: <input type="text" name="username" value="Jack"/>
<br>
email: <input type="text" name="email" value="Jack@163.com"/>
<br>
<input type="submit" value="Submit"/>
</form>

  
controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ModelAttribute
public void getUser(Map<String, Object> map) {
System.out.println("modelAttribute method");
// 模拟从数据库中获取对象
User user = new User(1,"Jack", "Jack@163.com");
System.out.println("从数据库中获取一个对象: " + user);
map.put("user", user);
}
@RequestMapping("/testModelAttribute")
public String testModelAttribute(User user){
System.out.println("修改: " + user);
return SUCCESS;
}

运行后,在控制台输出:

image_1b0u4nqt51qsb1b4019hq1fme1h5cm.png-21kB

  在这个例子中,SpringMVC通过映射请求调用目标处理方法testModelAttribute方法之前,做了下面三件事情:
  第1步: 执行 @ModelAttribute 注解修饰的方法: 从数据库中取出对象,把对象放入到了 Map 中,键为:user。
  第2步: SpringMVC 从 Map 中取出 User 对象 user, 并把表单的请求参数赋给 user 的对应属性。
  第3步: SpringMVC 把上述对象作为参数传入目标方法testModelAttribute(User user)。
  
  通过阅读SpringMVC的源码,我了解了这三个步骤具体是这样实现的:
  
  第1步: 执行 @ModelAttribute 注解修饰的方法,实际上把 @ModelAttribute 方法中 Map 中的数据user放在了 implicitModel(这是一个BindingAwareModelMap类型的对象,BindingAwareModelMap类型实现了Map接口) 中
  
  第2步:核心的功能都在第2步实现,这一步主要做了三件事,其中核心步骤是前两件事,即 2.1 和 2.2 :
  
   2.1(第一件事):确定查找键值对所需要的键(key):
   
    2.1.1:若目标方法的 POJO 类型的参数没有使用 @ModelAttribute 作为修饰, 则 key 为 POJO 类名第一个字母的小写
    2.1.2:若目标方法的 POJO 类型的参数使用了@ModelAttribute 来修饰, 则 key 为 @ModelAttribute 注解的 value 属性值
  在上面的例子中,目标方法testModelAttribute(User user)中的入参没有被@ModelAttribute修饰,所以key值为POJO 类名第一个字母的小写形式,即“user”。
  
   2.2(第二件事):在 implicitModel(即map) 中查找 key 对应的对象:
   
    2.2.1:若 @ModelAttribute 标记的方法在 Map 中保存过这样一个键值对, 其 key 和 2.1 确定的 key 一致, 则会获取到key对应的键值对的value值;
    2.2.2:若 implicitModel 中不存在 key 对应的对象,则检查当前的控制器类是否被@SessionAttributes注解修饰如果使用了@SessionAttributes注解修饰且@SessionAttributes注解的value值中包含了key,则尝试从HttpSession中获取key所对应的value值,如果value值存在则获取到如果value值不存在则抛出异常
    2.2.3:如果没有使用@SessionAttributes注解修饰该控制器类,或者使用了,但是@SessionAttributes注解中的value值不包含key,则SpringMVC会通过反射来创建一个POJO类型的对象
    
   2.3(第三件事):,用表单中传递过来的参数值去更新2.2确定的value值(或通过反射建立的对象)。
    
  第3步:同上面的第3步描述的一样,即SpringMVC 把上述对象作为参数传入目标方法。
  
  以上就是@ModelAttribute注解的工作流程,我们了解了流程之后,就会发现,在2.2.2中有这样一种情况:如果:
  ①在 implicitModel 中不存在 key 对应的对象;
  ②控制器类有标记了@SessionAttributes注解;
  ③@SessionAttributes注解的value值中包含key;
  ④HttpSession域中不存在该key值对应的value值。
  当同时满足这4种情况时,就会抛出异常,解决方法有两种:
  1、通过@ModelAttribute注解修饰目标方法的入参,确定一个key值,使其不被包含在@SessionAttributes注解的value值中。
  2、使用@ModelAttribute方法,在目标方法调用之前,将key对应的键值对放入implicitModel 中。