使用@ModelAttribute、Model、Map、@SessionAttributes能便捷地将我们的业务数据封装到模型里并交由视图解析调用。下面开始一一分析
使用@ModelAttribute可以直接将我们的方法入参添加到模型中。我们先看一个实例:
<!-- 扫描com.mvc.controller包下所有的类,使spring注解生效 -->
<context:component-scan base-package="com.mvc.controller" />
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property><!-- 前缀,在springMVC控制层处理好的请求后,转发配置目录下的视图文件 -->
<property name="suffix" value=".jsp"></property><!-- 文件后缀,表示转发到的视图文件后缀为.jsp -->
</bean>
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping("model1")
public String model1(@ModelAttribute User user){//绑定user属性到视图中
user.setId(1);
user.setPassword("myPassword");
user.setUserName("myUserName");
return "model1";
//直接返回视图名,springMVC会帮我们解析成/WEB-INF/views/model1.jsp视图文件
}
@RequestMapping("model2")
public String model2(@ModelAttribute User user){//绑定user属性到视图中
user = new User(2,"myUserName","myPwd");//这里直接新建一个对象
return "model1";
}
}
在目录/WEB-INF/views文件夹下添加:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<% String path = request.getContextPath(); String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>">
<title>hello spring mvc</title>
</head>
<body>
用户id:${user.id }<br>
用户名:${user.userName }<br>
用户密码:${user.password }
</body>
</html>
开启服务器后,假设tomcat监听8080端口,项目名为springMVC,则游览器访问结果如下图所示:
这里我们访问了控制器第一个方法,通过@ModelAttribute注解成功的完成地将user数据添加到模型中。我们再访问第二个,却会得到如下结果:
这是因为我们在model2()中设置成员属性时,新建了一个对象,这个对象尽管使用了引用变量user,但却不是原来注解绑定的那个对象。这就好像:A是被标记的房子,原来小明住在A里,但后面小明跑到B房子去了。这时候标记仍在A房子,不会因为小明跑到了B房子就使标记出现在B房子上。这里A房子就是我们被注解的实例对象,小明就是引用变量user,B房子就是我们使用带三个参数的构造方法新建的实例对象。
在SpringMVC调用任何方法前,被@ModelAttribute注解的方法都会先被依次调用,并且这些注解方法的返回值都会被添加到模型中。对于上例的moedel1方法,我们改写成如下所示:
@ModelAttribute
public User getUser1(){
System.out.println("getUser1方法被调用");
return new User(1,"myUserName1","myPwd1");
}
@ModelAttribute
public User getUser2(){
System.out.println("getUser2方法被调用");
return new User(2,"myUserName2","myPwd2");
}
@RequestMapping("model1")
public String model1(){//绑定user属性到视图中
return "model1";//直接返回视图名,springMVC会帮我们解析成/WEB-INF/views/model1.jsp视图文件
}
这时候访问游览器,得到结果
用户id:2
用户名:myUserName2
用户密码:myPwd2
此时控制台输出:
getUser2方法被调用
getUser1方法被调用
这里说明,getUser1()方法首先被调用了,但模型属性认为id为2的user,说明后面调用的getUser1()的模型数据兵并不能对前面的进行覆盖,即当存在多个同类型的模型数据时,第一个才是有效的。
而如果我们想添加多个相同类型的模型数据,可使用如下方法:
@ModelAttribute
public void getUser3(Model model){//注入model入参
System.out.println("getUser3方法被调用");
model.addAttribute("user1",new User(1,"myUserName1","myPwd1"));
model.addAttribute("user2",new User(2,"myUserName2","myPwd2"));
}
@RequestMapping("model3")
public String model3(Model model){//我们可以在这绑定入参model读取前面的数据
System.out.println(model.asMap().get("user1"));
System.out.println(model.asMap().get("user2"));
return "model3";
}
User [id=1, userName=myUserName1, password=myPwd1]
User [id=2, userName=myUserName2, password=myPwd2]
在实际开发中,我们常常需要在控制器每个方法调用前做些资源准备工作,如获取当次请求的servletAPI,输入输出流等,我们可以直接在方法入参上注明,springMVC会帮我们完成注入,如下所示:
@RequestMapping("doSth")
public void doSth(HttpServletRequest request,HttpServletResponse response,BufferedReader bufferedReader,PrintWriter printWriter){
//可以直接调用每个方法参数,spring已帮我们完成注入
System.out.println("do something....");
}
但现在问题来了,如果我们很多个方法都要使用到这些web资源,是否都要在方法入参上一一写明呢?这未免过于繁琐,事实上,我们可以结合被@ModelAttribute注解的方法会在控制器每个方法调用前执行的特点来完成全局资源统一准备工作,见下面的例子:
package com.mvc.controller;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
@Controller
public abstract class BaseController {
//准备web资源
protected ServletContext servletContext;//声明为protected方便子类继承使用
protected HttpSession session;
protected HttpServletRequest request;
protected HttpServletResponse response;
//可以用来读取上传的IO流
protected BufferedReader bufferedReader;
//可以用来给安卓、IOS或网页ajax调用输出数据
protected PrintWriter printWriter;
@ModelAttribute
protected void setReqAndRes(HttpServletRequest request,
HttpServletResponse response) throws IOException {
this.request = request;
this.response = response;
this.session = request.getSession();
this.servletContext = session.getServletContext();
this.bufferedReader = request.getReader();
this.printWriter = response.getWriter();
}
}
然后我们可能有UserContrller,ArticleController,xxxxController等等,都可以直接继承BaseController,然后在每一个方法都可以自由使用以上web资源,这样就简便高效很多了。
在上面的实例中,我们就尝试使用Model存储和读取数据操作。在springMVC中,Model、ModelMap、Map及其实现类,尽管各自的操作方法不一样,但它们存储的数据都会被spring获取,并绑定到模型数据中。我们来看下面的实例:
@ModelAttribute
public void getUser3(Map map){//我们使用map类型存储的数据一样会被绑定到model中
System.out.println("getUser3方法被调用");
map.put("user1",new User(1,"myUserName1","myPwd1"));
map.put("user2",new User(2,"myUserName2","myPwd2"));
}
@RequestMapping("model3")
public String model3(Model model){//我们可以在这绑定入参model读取前面的数据
System.out.println(model.asMap().get("user1"));
System.out.println(model.asMap().get("user2"));
return "model3";
}
@RequestMapping("model4")
public String model4(HashMap map){//我们可以在这绑定入参map的任意实现类读取前面的数据
System.out.println(map.get("user1"));
System.out.println(map.get("user2"));
return "model3";
}
@RequestMapping("model5")
public String model4(ModelMap map){//我们可以在这绑定入参modelMap读取前面的数据
System.out.println(map.get("user1"));
System.out.println(map.get("user2"));
return "model3";
}
访问三个不同的url触发不同的方法,得到的结果都是一致的:
用户1id:1
用户1名:myUserName1
用户1密码:myPwd1用户2id:2
用户2名:myUserName2
用户2密码:myPwd2
这说明,在springMVC中Model、ModelMap、Map(及其实现类)三者的地位是等价的,都可以将数据绑定到模型中供前端视图获取使用。
在项目中,我们可能需要将登陆用户的信息存储到Session中,
使用@SessionAttribute我们可以将特定的模型属性存储到一个Session域的隐含模型中,然后可以在同一个会话多个请求内共享这些信息。我们先来看一个没使用@sessionAttribute注解的实例:
@Controller
@RequestMapping("/user")
//@SessionAttributes("user")——————————————注意看!!我这里注释了
public class UserController3 {
@RequestMapping("req1")
public String req1(HttpSession session,ModelMap map ){
User user = new User(1,"myUserName1","myPwd1");
map.put("user",user);//将数据存储在模型中
return "redirect:req2";//返回字符串,且使用redirect:表示重定向
//同样我们可以使用forward:完成跳转。
//注意这里redirect:前后都不能有空格
//如果为redirect: req2,则会重定向到" req2"
//如果为redirect :req2,则重定向无效,直接转向视图”redirect :req2.jsp"
}
@RequestMapping("req2")
public String req2(ModelMap modelMap,HttpSession session){
User user = (User) modelMap.get("user");
System.out.println("user from model :" + user);//输出“user from model :null”
User suser = (User) session.getAttribute("user");
System.out.println("user from session :" + suser);//输出"user from session :null"
return "model1";
}
}
我们上面通过重定向跳转到另一个链接,它们处于不同的请求上下文,所以无法通过model和session访问到我们之前存储的user对象。最终都输出null。
接下来,我们把上例类定义头上的@SessionAttributes注释去掉。再访问链接,req2中的两个打印信息分别为:
user from model :User [id=1, userName=myUserName1, password=myPwd1]
user from session :User [id=1, userName=myUserName1, password=myPwd1]
在这里,如果我将map.put("user",user);
中的key:”user”改为“user1”,则输出又变为null
从上面实验,我们可以根据“控制变量法”可以得出结论:@SessionAttribute(“val”)会自动查找模型中名称对应为”val”的数据,并将其存储到一个Session域的隐含模型中(并且可以直接通过HttpSession API获取),在以后的相同会话请求中,SessionAttribute会自动将其存放在调用方法的模型中。
但是,如果我们直接将数据存到Session中,在下次请求中,springMVC不会将改数据放到当次调用方法的模型里。看下面实例:
@RequestMapping("req1")
public String req1(HttpSession session,ModelMap map ){
User newUser = new User(10,"myNewPassword","myNewUserName");//这里我们新建了一个User,并将其存储在Session中
session.setAttribute("newUser", newUser);//存储操作
return "redirect:req2";
}
@RequestMapping("req2")
public String req2(ModelMap modelMap,HttpSession session){
User newUser = (User) modelMap.get("newUser");//这里我们尝试从模型中取出上次请求存储在Session中的数据
System.out.println("newUser from model :" + newUser);
User snewUser = (User) session.getAttribute("newUser");//这里我们尝试直接通过Session读取
System.out.println("newUser from session :" + snewUser);
return "model1";
}
访问上面方法,我们得到的打印结果是:
newUser from model :null
newUser from session :User [id=10, userName=myNewPassword, password=myNewUserName]
说明在第二次请求中,模型中并没有自动放入Session的数据
@SessionAttribute相关的还有一个SessionStatus接口,它有唯一的实现类:
public class SimpleSessionStatus implements SessionStatus {
//判断当前会话是否结束,如果为true,则sprng会清空我们使用@SessionAttributes注册的Session数据
//但它不会清空我们手动设置到Session域隐含模型中的数据。
private boolean complete = false;
//设置会话结束
@Override
public void setComplete() {
this.complete = true;
}
//判断会话是否结束
@Override
public boolean isComplete() {
return this.complete;
}
}
通过SessionStatus,能灵活控制我们在@SessionAttributes注册的会话属性。
这里的理解是容易的,所以我们不再进行实例测试,感兴趣的朋友可到文尾下载本篇本章测试源码。
我们稍微改造一下我们上面的实例,将Model入参改为使用@ModelAttribute User user。看下面实例主要代码:
@Controller
@RequestMapping("/user")
@SessionAttributes("user")
public class UserController2 {
@RequestMapping("req1")
public String req1(HttpSession session,@ModelAttribute User user){
user.setId(1);
user.setPassword("myPassword");
user.setUserName("myUserName");
return null;
}
}
在这里我们尝试使用@ModelAttribute将user存储到模型中,然后再让@SessionAttributes将模型中user存储到Session中,(后面本来是重定向req2再通过模型获取打印出来,这里为了测试实例直接返回null)。
然后我们通过链接请求调用req1方法,结果却报错:
org.springframework.web.HttpSessionRequiredException: Expected session attribute ‘user’
这已错误咋一看非常莫名奇妙,为什么会期望Session中有user呢?首先弄明白:谁会去期望有呢?当然不会是我们的@SessionAttributes,因为它的作用就是将user存储到Session中呀,那么就是我们的@ModelAttribute。
在这里,我们希望通过@ModelAttribute来将user放入隐含模型中,但事实上,它会先到模型中寻找名字对应的属性,并将其赋给入参,如果在当前请求域的隐含模型中没有这个属性,它会到Session域的隐含模型中去找,对于一般属性,如果还是找不到,就会创建一个新的实例。
但是,如果我们的user属性被@SessionAttributes注明为会话属性,如果在Session域隐含模型找不到,就会报错,报什么错呢?就报:
org.springframework.web.HttpSessionRequiredException: Expected session attribute ‘user’
这很好理解,既然user都被标注了Session域属性了,如果在自己老家都找不着人,肯定很着急,自然就要报错了
下面,我们结合流程图来理解@ModelAttribute和@SessionAttributes的运作流程:
我们上面出现的问题就主要体现在三个红底白字框里
本节内容测试源码可到https://github.com/jeanhao/spring下的modelAttribute文件夹下下载