springMVC4(8)模型数据绑定全面分析
使用@ModelAttribute、Model、Map、@SessionAttributes能便捷地将我们的业务数据封装到模型里并交由视图解析调用。以下開始一一分析
在方法入參上使用@ModelAttribute
使用@ModelAttribute能够直接将我们的方法入參加入到模型中。我们先看一个实例:
1. springMVC核心文件配置:
<!-- 扫描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>
2. 编写控制器:
@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";
}
}
3. 编写视图层文件
在文件夹/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>
username:${user.userName }<br>
用户密码:${user.password }
</body>
</html>
4. 启动server測试
开启server后,假设tomcat监听8080端口,项目名为springMVC,则游览器訪问结果例如以下图所看到的:
这里我们訪问了控制器第一个方法。通过@ModelAttribute注解成功的完毕地将user数据加入到模型中。
我们再訪问第二个。却会得到例如以下结果:
这是由于我们在model2()中设置成员属性时。新建了一个对象,这个对象虽然使用了引用变量user,但却不是原来注解绑定的那个对象。这就好像:A是被标记的房子。原来小明住在A里,但后面小明跑到B房子去了。这时候标记仍在A房子,不会由于小明跑到了B房子就使标记出如今B房子上。
这里A房子就是我们被注解的实例对象,小明就是引用变量user,B房子就是我们使用带三个參数的构造方法新建的实例对象。
在方法定义上使用@ModelAttribute
在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
username: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]
使用@ModelAttribute完毕数据准备工作
在实际开发中。我们经常须要在控制器每一个方法调用前做些资源准备工作,如获取当次请求的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和Map操作模型数据
在上面的实例中。我们就尝试使用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(及其实现类)三者的地位是等价的,都能够将数据绑定到模型中供前端视图获取使用。
@SessionAttribute
在项目中。我们可能须要将登陆用户的信息存储到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的数据
SessionStatus
@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注冊的会话属性。
这里的理解是easy的,所以我们不再进行实例測试,感兴趣的朋友可到文尾下载本篇本章測试源代码。
理解@ModelAttribute和@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文件夹下下载