Spring MVC 框架学习三:Spring MVC 处理模型数据
SpringMVC 中模型数据的产生
Spring MVC 通过 @RequestMapping 将请求引导到处理方法上,使用合适的方法签名将请求消息绑定到入参中。方法入参绑定请求消息只是处理方法的第一步,还有更重要的任务等待完成,即根据入参执行相应的逻辑,产生模型数据,导向到特定视图中。
如何将模型数据暴露给视图是 SpringMVC 框架的一项重要工作,SpringMVC 提供了多种途径输出模型数据:
1. ModelAndView:处理方法返回值类型为 ModelAndView时,方法体即可通过该对象添加模型数据。
2. @ModelAttribute:方法入参标注该注解后,入参的对象就会放到数据模型中。
3. Map 及 Model:入参为 org.springframework.ui.Model、org.soringframework.ui.ModelMap 或 java.util.Map 时,处理方法返回时,Map中的数据会自动添加到模型中。
4. @SessonAttribute:将模型中的某个属性暂时存到 HttpSession 中,以便多个请求之间可以共享这个属性。
ModelAndView
控制器处理方法的返回值如果为 ModelAndView,则其既包含视图信息,也包含模型数据信息,这样 SpringMVC 就可以使用视图对模型数据进行渲染了。可以简单地将模型数据看成一个 Map<String, Object>对象。
在处理方法的方法体中,可以使用如下方法添加模型对象:
1. ModelAndView addObject(String attributeName, Object attributeValue)
2. ModelAndView addAllObject(Map<String, ?> modelMap)
可以通过如下方法设置视图
1. void setView(View view):指定一个具体的视图对象
2. void setViewName(String viewName):指定一个逻辑视图名
配置web.xml
<servlet> <servlet-name>springDispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springDispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
配置 springDispatcherServlet-servlet.xml
<!-- 配置自动扫描的包 --> <context:component-scan base-package="com.bupt.springmvc.handler"/> <!-- 配置视图解析器:将视图逻辑名解析为/WEB-INF/viewss/<viewsName>.jsp (需在/WEB-INF下新建一个名为views的文件夹) --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>
新建测试类 SpringTest
1 package com.bupt.springmvc.handler; 2 3 import org.springframework.stereotype.Controller; 4 import org.springframework.web.servlet.ModelAndView; 5 6 @RequestMapping("/springmvc") 7 @Controller 8 public class SpringTest 9 { 10 //SpringMVC 会把 ModelAndView 的模型数据放到请求域中去 11 @RequestMapping("/testModelAndView") 12 public ModelAndView testModelAndView() 13 { 14 String viewName = "success"; 15 //指定跳转的逻辑视图名为 success 16 ModelAndView modelAndView = new ModelAndView(viewName); 17 //添加模型数据到ModelAndView 18 modelAndView.addObject("time", new Date()); 19 return modelAndView; 20 } 21 }
新建请求页面 index.jsp
<head> <title>Insert title here</title> </head> <body> <a href="springmvc/testModelAndView">Test ModelAndView</a> </body> </html>
结果页面 success.jsp
<html>
<head>
<title>Insert title here</title>
</head>
<body>
<h4>Success Page</h4>
time: ${requestScope.time }
</body>
</html>
Map 及 Model
SpringMVC 在内部使用一个 org.springframework.ui.Model 接口存储模型数据,它的功能类似于 java.util.Map,但它比Map易用。org.springframework.ui.ModelMap实现了 Map 接口,而 org.springframework.ui.ExtendedModelMap 继承于 ModelMap 同时实现了 Model 接口。
SpringMVC 在调用方法前会创建一个隐含的模型对象,作为模型数据的存储容器,我们称之为 “隐含模型”。如果处理方法的入参为 Map 或 Model 类型,SpringMVC 会将隐含模型的引用传递给这些入参。在方法体内,开发者可以通过这个入参对象访问到模型中的所有数据,也可以向模型数据中添加新的属性数据。
SpringTest类中新增方法
1 //目标方法可以添加 Map 类型的参数,也可以是 Model 类型或者 ModelMap 类型的参数 2 @RequestMapping("/testMap") 3 public String testMap(Map<String, Object> map) 4 { 5 map.put("names", Arrays.asList("tom", "jack", "mike")); 6 return "success"; 7 }
index.jsp新增超链接
<a href="springmvc/testMap">Test Map</a>
success.jsp新增如下代码
map: ${requestScope.names }
SpringMVC 一旦发现处理方法有 Map 或 Model 类型的入参,就会请求内在的隐含模型对象传递给这些参数,因此在方法体中可以通过这个入参对模型对象的数据进行读写操作。
@SessionAttributes
如果希望在多个请求之间共用某个模型属性数据,则可以在控制器类标注一个 @SessionAttributes,SpringMVC 会将模型中对应的属性暂存到 HTTPSession 中。
@SessionAttributes 除了可以通过属性名指定需要放到会话中的属性外,还可以通过模型属性的对象类型指定哪些模型属性需要放到会话中。
1. @SessionAttributes(types=User.class)会将隐含模型中所有类型为 User 的属性添加到会话中
2. @SessionAttributes(value={"user1", "user2"})将名为 user1 和 user2 的模型属性添加到会话中
3. @SessionAttributes(types={"User.class", "Dept.class"})将模型中所有类型为 User 及 Dept 的属性添加到会话中
4. @SessionAtributes(value={"user1", "user2"}, types={Dept.class})将名为 user1 和 user2 的模型属性添加到会话中,同时将所有类型为 Dept 的模型属性添加到会话中
新建一个 POJO
package com.bupt.springmvc.entity; public class User { private int id; private String username; private String password; private String address;
//生成 get 和 setter 方法,生成带参和不带参的构造方法,重写 toString 方法
}
1 @SessionAttributes(value={"user"}, types={String.class}) 2 @RequestMapping("/springmvc") 3 @Controller 4 public class SpringTest 5 { 6 private static final String SUCCESS = "success"; 7 8 @RequestMapping("/testSessionAttributes") 9 public String testSessionAttributes(Map<String, Object> map) 10 { 11 User user = new User("Jack", "123"); 12 map.put("user", user); 13 map.put("msg", "hello"); 14 return SUCCESS; 15 } 16 }
<a href="springmvc/testSessionAttributes">Test SessionAttributes</a>
1 request user: ${requestScope.user } 2 <br><br> 3 request msg: ${requestScope.msg } 4 <br><br> 5 session user: ${sessionScope.user } 6 <br><br> 7 session msg: ${sessionScope.msg }
由结果可以看出,被 @SessionAttributes 注解修饰后,模型属性不仅存在于请求域还存在于会话域。除了可以通过value属性值指定需要放到会话中的属性外,还可以根据types属性值指定哪些类型的模型属性需要放到会话中。需要注意的是 @SessionAttributes 注解只能用于修饰类而不能用于方法上。
@ModelAttribute
在方法定义上使用 @ModelAttribute 注解:SpringMVC 在调用目标处理方法前,会逐个调用在方法上标注了 @ModelAttribute 的方法
在方法的入参前使用 @ModelAttribute 注解:
1. 可以从隐含对象中获取隐含的模型数据对象,再将请求参数绑定到对象中,再传入入参
2. 将方法入参对象添加到模型中
通过例子来说明
1 @Controller 2 public class HelloModelController { 3 4 @ModelAttribute 5 public void populateModel(@RequestParam String abc, Model model) { 6 model.addAttribute("attributeName", abc); 7 } 8 9 @RequestMapping(value = "/helloWorld") 10 public String helloWorld() { 11 return "helloWorld"; 12 } 13 }
在这个代码中,访问控制器方法helloWorld时,会首先调用populateModel方法,将页面参数abc放到model的attributeName属性中,在视图中可以直接访问
1 @Controller 2 public class Hello2ModelController { 3 4 @ModelAttribute 5 public User populateModel() { 6 User user=new User(); 7 user.setAccount("ray"); 8 return user; 9 } 10 @RequestMapping(value = "/helloWorld2") 11 public String helloWorld() { 12 return "helloWorld"; 13 } 14 }
当用户请求/helloWorld2时,首先访问populateModel方法,返回User对象,model属性的名称没有指定,它由返回类型隐含表示,如这个方法返回User类型,那么这个model属性的名称是user。这个例子中model属性名称由返回对象类型隐含表示,model属性对象就是方法的返回值。
也可以指定属性名称
1 @Controller 2 public class Hello2ModelController { 3 4 @ModelAttribute(value="myUser") 5 public User populateModel() { 6 User user=new User(); 7 user.setAccount("ray"); 8 return user; 9 } 10 @RequestMapping(value = "/helloWorld2") 11 public String helloWorld(Model map) { 12 return "helloWorld"; 13 } 14 }
也可以将传递进来的对象与存在于Model中的对象进行合并,这时要将传递进来的对象名称改为与Model中进行合并的那个对象名一致
1 @Controller 2 public class Hello2ModelController { 3 4 @ModelAttribute("myUser") 5 public User populateModel() { 6 User user=new User(); 7 user.setAccount("ray"); 8 return user; 9 } 10 11 @RequestMapping(value = "/helloWorld2") 12 public String helloWorld(@ModelAttribute("myUser") User user) { 13 user.setName("老王"); 14 return "helloWorld"; 15 } 16 }
这两种情况我们在jsp中访问要使用指定的属性名${myUser.name};
需要注意的是:
如果Model中没有key为user的属性,并且没写@ModelAttribute("user"),由于参数列表中有User user对象入参,则Spring会将该对象放入model,并且key值为首字母小写的类名,也就是说对于方法:
1 @RequestMapping(value="/helloworld") 2 public String helloWorld(User user) 3 { 4 return "helloworld"; 5 }
框架提前帮你写了一句model.addAttribute("user",user)。
而且特别需要注意注意,无论model中是否有key为user的属性,都要求User类有无参构造方法
将SpringTest上的 @SessionAttributes 注解注释掉,添加如下方法
//有 @ModelAttribute 标记的方法,会在每个目标方法执行之前被 SpringMVC 调用
@ModelAttribute public void getUser(@RequestParam(value="id", required=false) Integer id, Map<String, Object> map) { System.out.println("modelAttribute method"); if(id != null) { User user = new User(1, "Tom", "1234", "beijing"); System.out.println("模拟从数据库删去一个对象:" + user); map.put("user", user); } } @RequestMapping("/testModelAttribute") public String testModelAttributes(User user) { System.out.println("修改:" + user); return SUCCESS; }
<form action="springmvc/testModelAttribute" method="post"> <input type="hidden" name="id" value="1"/> usernname: <input type="text" name="username" value="Tom"><br> address: <input type="text" name="address" value="beijing"><br> <input type="submit" value="submit"> </form>
在index.jsp页面address栏改为shanghai,运行程序控制台结果为
modelAttribute method 模拟从数据库删去一个对象:User [id=1, username=Tom, password=1234, address=beijing] 修改:User [id=1, username=Tom, password=1234, address=shanghai]
如果保留@SessionAttributes,并注释掉 getUser()方法,运行程序,此时程序会出现异常:
org.springframework.web.HttpSessionRequiredException:Session attribute 'user' required - not found in session
我们通过源码分析程序流程来找出异常产生的原因:
1. 调用 @ModelAttribute 注解修饰的方法,实际上把方法中的 Map 中的数据放在了 implicitModel 中
2. 解析请求处理器的目标参数,实际上该目标参数来自于 WebDataBinder 对象的 target 属性
1) 创建 WebDataBinder 对象,它包括两个属性 objectName 和 target
① 确定 objectName 属性:若传入的 attrName 属性为 ""(空串),且目标方法的 POJO 参数未使用 @ModelAttribute 修饰,则objectName 为此 POJO 类名第一个字母小写
注:若目标方法的 POJO 参数使用了 @ModelAttribute 来修饰,则 attrName 值即为 @ModelAttribute 的 value 属性值,如:
//此时的 attrName 为 abc @RequestMapping("/testModelAttribute") public String testModelAttribute(@ModelAttribute("abc")User user) { System.out.println("修改:" + user); return "success"; }
② 确定 target 属性:在 implicitModel 中查找 attrName 对应的属性值。
若存在,获取到此属性值;(如在被 @ModelAttribute 修饰的方法中的 Map 保存过,且其 key 与 attrName 一致,则 target 值即为其 value 值)
若不存在,则验证当前 Handler 是否使用了 @SessionAttributes 进行修饰。若使用了,则尝试从 Session 中获取 attrName 所对应的属性值,如果 session 中有与 attrName 对应的 key 但没有对应的属性,则抛出异常;若 Handler 没有使用 @SessionAttributes 进行修饰,或 @SessionAttributes 中没有使用 value 值指定的 key 与 attrName 匹配,则通过反射创建 POJO 对象,即创建了一个 target。
所以前面程序之所以抛异常就是,@SessionAttributes 指定了 value="user",且 attrName 也为 user,二者匹配,但在 session 域内没有对应的属性值,所以抛出异常
2) SpringMVC 把表单的请求参数赋给了 WebDataBinder 的 target 对应的属性
3) SpringMVC 会把 WebDataBinder 的 attrName 和 target 给到 implicitModel,进而传递到请求域中
4) 把 WebDataBinder 的 target 作为参数传递给目标方法的入参
以上过程即可以看作为 SpringMVC 确定目标方法 POJO 类型参数入参的过程。
posted on 2016-07-06 11:35 Traveling_Light_CC 阅读(644) 评论(0) 编辑 收藏 举报