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编辑  收藏  举报

导航