SpringMVC深入二
处理模型数据
MVC设计模式是,发一个请求到目标处理器,目标处理器去调业务方法,业务方法可能有返回值,可能是一个对象或者集合。然后转发页面,需要把业务对象返回值(对象或者集合)在页面上显示出来。
在SpringMVC中用以下几种途径输出模型数据:
ModelAndView
:处理方法返回值类型为ModelAndView时,方法体即可通过该对象添加模型数据Map及Model
:入参为org.springframework.ui.Model
,org.springframework.ui.ModelMap
或java.util.Map
时,处理方法返回时,Map中的数据会自动添加到模型中@SessionAttributes
:将模型中的某个属性暂存到HttpSession中,以便多个请求之间可以共享这个属性@ModelAttribute
:方法入参标注该注解后,入参的对象就会放到数据模型中
ModelAndView
SpringMVC会把ModelAndView的Model中数据放入到request域对象中。
如下,返回视图为SUCCESS
,添加一个time
数据
/**
* 目标方法的返回值可以是ModelAndView类型。
* 其中可以包含视图和视图模型信息
* @return
*/
@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView(){
String viweName = SUCCESS;
ModelAndView modelAndView = new ModelAndView(viweName);
//添加模型数据到ModelAndView中
modelAndView.addObject("time", new Date());
return modelAndView;
}
所以在success.jsp
页面中可以取出time
对应的数据。
time:${requestScope.time}
Map及Model
目标方法的入参可以加入一个Map或者Model的参数。
SpringMVC在内部使用了一个org.springframework.ui.Model
接口存储模型数据。
具体步骤:
- SpringMVC在调用方法前会创建一个隐含的模型对象作为模型数据的存储容器
- 如果方法的入参为Map或Model类型,SpringMVC会将隐含模型的引用传递给这些入参。在方法体内,开发者可以通过这个入参对象访问到模型中的所有数据,也可以向模型中添加新的属性数据。
如下:
/**
* 目标方法可以添加Map类型的参数,还可传入ModelMap或Model,通常传入Map类型
* @param map
* @return
*/
@RequestMapping("/testMap")
public String testMap(Map<String, Object> map) {
map.put("names", Arrays.asList("tom", "jerry", "mike"));
return SUCCESS;
}
在success.jsp
页面中可以取出names
对应的数据
names:${requestScope.names}
@SessionAttributes
若希望多个请求共用某个模型属性数据,则可以在控制器上类上标注一个@SessionAttributes
,SpringMVC将在模型中对应的属性暂存到HttpSession
中。
@SessionAttributes
除了可以通过属性名指定需要放到会话中的属性外,还可以通过模型属性的对象类型指定哪个模型属性需要放到会话中
@SessionAttributes(types=User.class)
会将隐含模型中所有的类型为User.class的属性添加到会话中@SessionAttributes(value={"user1","user2"})
@SessionAttributes(value={User.class, Dept.class})
@SessionAttributes(value={"user1","user2"}, types={Dept.class})
如下:
@RequestMapping("/testSessionAttributes")
public String testSessionAttributes(Map<String, Object> map) {
User user = new User("wz", "hello");
map.put("user", user);
map.put("school", "wuhan");
return SUCCESS;
}
map
把user
和school
放入到了request的请求域中。
当在控制器上加上@SessionAttributes(value={"user"},types={String.class})
后,可以把user
和school
放入到session中。
user
使用的是value
属性值,school
使用的是types
属性值。
需要注意的是这个注解只能放在类上。
在jsp页面中可以取出值:
user:${requestScope.user}
<br>
user:${sessionScope.user}
<br>
school:${requestScope.school}
<br>
school:${sessionScope.school}
@ModelAttribute
通过例子来说明,假设有如下的表单,其原始数据为id=1,username=tom,password=123456,email=123@123.com,age=12
,在做修改的时候,不能在表单中显示密码,所以密码是不知道的,现在把age
修改为13
:
<form action="springmvc/testModelAttribute" method="post">
<input type="hidden" name="id" value="1" />
username:<input type="text" name="username" value="tom"/>
<br>
email:<input type="text" name="email" value="123@123.com"/>
<br>
age:<input type="text" name="age" value="12"/>
<br>
<input type="submit" value="submit" />
</form>
映射代码如下:
@RequestMapping("/testModelAttribute")
public String testModelAttribute(User user){
System.out.println("修改:" + user);
return SUCCESS;
}
控制台输出如下:
修改:User [id=1, username=tom, password=null, email=123@123.com, age=13]
可以发现password
为null
,这样在修改时就会有问题,需要先获取到password
的值。
然后,添加如下的代码,观察结果。
这里模拟从数据库中获取数据,把user
放入到map
中,并使用了@ModelAttribute
注解:
@ModelAttribute
public void getUser(@RequestParam(value="id", required=false) Integer id,
Map<String, Object> map){
if (id != null) {
//模拟从数据库中获取对象
User user= new User(1, "tom", "123456", "123@123.com", 12);
System.out.println("从数据库中获取一个对象:" + user);
map.put("user", user);
}
}
提交表单,控制台输出结果如下:
从数据库中获取一个对象:User [id=1, username=tom, password=123456, email=123@123.com, age=12]
修改:User [id=1, username=tom, password=123456, email=123@123.com, age=13]
可以发现表单的user
的password
现在有了值。
在改之前先查数据库,然后把表单的属性值赋给从数据库获取那个对象的对应属性。
有@ModelAttribute
标记的方法,会在每个目标方法执行之前被SpringMVC调用
运行流程:
1.执行@ModelAttribute
修饰的方法:从数据库中取出对象,把对象放入到Map中,键为:user
2.SpringMVC从Map中取出User对象,并把表单的请求参数赋给该User对象的对应属性。
3.SpringMVC把上述对象传入目标方法的参数
注意:在@ModelAttribute
修饰的方法中,放入到Map时的键需要和目标方法入参类型的第一个字母小写的字符串一致。
源码分析的流程:
- 调用
@ModelAttribute
注解修饰的方法,实际上把@ModelAttribute
方法中Map
中的数据放在了implicitModel
中。 解析请求处理器的目标参数,实际上该目标参数来自于
WebDataBinder
对象的target
属性。创建WebDataBinder对象
- 确定
objectName
属性:若传入的attrName
属性值为”“,则objectName
为类名第一个字母小写。注意:attrName
若目标方法的POJO使用了@ModelAttribute
来修饰,则attrName
值即为@ModelAttribute
的value属性值。 - 确定
target
属性:在implicitModel
中查找对应的属性,若存在,则返回。若不存在,则验证当前Handler是否使用了@SessionAttributes
进行修饰,若使用了,则尝试从session中获取attrName所对应的属性值。若session中没有对应的属性值,则抛出异常。若Handler没有使用@SessionAttributes
进行修饰,或@SessionAttributes
中没有value值指定的key和attrName相匹配,则通过反射创建了POJO对象。
- 确定
SpringMVC把表单的请求参数赋给了
WebDataBinder
的target
对应的属性。- SpringMVC会把
WebDataBinder
的attrName
和target
给到implicitModel
,进而传到request域 - 把
WebDataBinder
的target作为参数传递给目标方法的入参。
SpringMVC确定目标方法POJO类型入参的过程?
- 确定一个key:若目标方法的POJO类型的参数没有使用
@ModelAttribute
作为修饰,则key为POJO类名第一个字母的小写。若使用@ModelAttribute
,则key为@ModelAttribute
注解的value属性值。 在
implicitModel
中查找key对应的对象,若存在,则作为入参传入- 若在
@ModelAttribute
标记的方法中在Map中保存过,且key和1
确定的key一致。
- 若在
。若
implicitModel
不存在key对应的对象,则检查当前的Handler是否使用@SessionAttributes
修饰,若使用了该注解,且@SessionAttributes
注解的value属性值包含了key,则会从HttpSession中获取key所对应的value值,若存在则直接传入到目标方法的入参中,若不存在则抛出异常。- 若Handler没有标识
@SessionAttributes
注解或@SessionAttributes
注解value的值中不包含key,会通过反射来创建POJO类型的参数,作为目标方法的参数 - SpringMVC会把key和POJO类型的对象保存到
implicitModel
中,进而会保存到request中。
@ModelAttribute
注解也可以修饰目标方法POJO类型的入参,其value属性值有如下的作用:
- SpringMVC会使用value属性值在
implicitModel
中查找对应的对象,若存在则会直接传入到目标方法的入参中 - SpringMVC会以value作为key,POJO类型的对象为value,存到request中。
对与上面的例子,把原来map中key由user
改为abc
,在对应的返回页面中查看request域中的值:
@ModelAttribute
public void getUser(@RequestParam(value="id", required=false) Integer id,
Map<String, Object> map){
if (id != null) {
//模拟从数据库中获取对象
User user= new User(1, "tom", "123456", "123@123.com", 12);
System.out.println("从数据库中获取一个对象:" + user);
map.put("abc", user);
}
}
@RequestMapping("/testModelAttribute")
public String testModelAttribute(User user){
System.out.println("修改:" + user);
return SUCCESS;
}
在success.jsp
页面中取出值:
<br>
abc:${requestScope.abc}
<br>
user:${requestScope.user}
在页面上显示结果如下,abc
对应的是数据库中的值,user
对应的是表单中的值:
abc:User [id=1, username=tom, password=123456, email=123@123.com, age=12]
user:User [id=1, username=tom, password=null, email=123@123.com, age=15]
如果,我们将handler的入参POJO对象,使用@ModelAttribute
修饰:
@RequestMapping("/testModelAttribute")
public String testModelAttribute(@ModelAttribute("abc") User user){
System.out.println("修改:" + user);
return SUCCESS;
}
结果如下,abc为更新后的对象,user为空:
abc:User [id=1, username=tom, password=123456, email=123@123.com, age=15]
user:
@SessionAttributes
注解引发的异常
移除如下使用@ModelAttribute
注解的代码:
@ModelAttribute
public void getUser(@RequestParam(value="id", required=false) Integer id,
Map<String, Object> map){
if (id != null) {
//模拟从数据库中获取对象
User user= new User(1, "tom", "123456", "123@123.com", 12);
System.out.println("从数据库中获取一个对象:" + user);
map.put("user", user);
}
}
只保留
@RequestMapping("/testModelAttribute")
public String testModelAttribute(@ModelAttribute("user") User user){
System.out.println("修改:" + user);
return SUCCESS;
}
则抛出异常:org.springframework.web.HttpSessionRequiredException: Session attribute 'user' required - not found in session
原因是,会先在implicitModel
中查找user
对应的属性,若存在,则返回。若不存在,则验证当前Handler是否使用了@SessionAttributes
进行修饰,若使用了,则尝试从session中获取attrName所对应的属性值。若session中没有对应的属性值,则抛出异常。