1. 数据模型处理
Spring MVC 提供了以下几种途径输出模型数据:
- ModelAndView: 处理方法返回值类型为 ModelAndView时, 方法体即可通过该对象添加模型数据;
- Map 及 Model: 入参为org.springframework.ui.Model、org.springframework.ui.ModelMap 或 java.uti.Map 时,处理方法返回时,Map中的数据会自动添加到模型中;
- @SessionAttributes: 将模型中的某个属性暂存到HttpSession 中,以便多个请求之间可以共享这个属性;
- @ModelAttribute: 方法入参标注该注解后, 入参的对象就会放到数据模型中。
1.1 ModelAndView
控制器处理方法的返回值如果为 ModelAndView, 则其既包含视图信息,也包含模型数据信息。
下面演示一下ModelAndView的简单使用。
首先需要在控制器类SpringMVCTest定义如下方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SpringMVCTest {
public final static String SUCCESS = "success";
/**
* 返回值ModelAndView包含视图信息,也包含模型数据信息。
* SpringMVC会把ModelAndView的数据存放在Request请求域对象中。
*/
"/testModelAndView") (
public ModelAndView testModelAndView() {
String viewName = SUCCESS;
ModelAndView modelAndView = new ModelAndView(viewName);
//添加模型数据到ModelAndView中
modelAndView.addObject("time", new Date());
return modelAndView;
}
}然后在结果视图页面success.jsp中进行如下代码编写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Success</title>
</head>
<body>
<h2>Success page</h2>
<!-- ${requestScope.time }相当于request.getAttribute("time"); -->
time: ${requestScope.time }<br/>
</body>
</html>在index.jsp页面点击超链接进行请求响应,index.jsp的具体代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<a href="testModelAndView">Test ModelAndView</a><br/>
</body>
</html>点击超链接请求成功的响应结果如下所示:
1.2 Map
控制器方法的入参可以是Map类型,也可以是Model类型或ModelMap类型的参数。
我们在控制器类SpringMVCTest中添加如下方法:
1
2
3
4
5
6
7
8
9
10
11
12/**
* 目标方法可以添加Map类型(实际也可以是Model类型或ModelMap类型)的参数。
* 处理方法返回时,Map中的数据会自动添加到模型中。
* 我们可以在Request请求域对象中取到Map的值。
*/
"/testMap") (
public String testMap(Map<String, Object> map) {
System.out.println(map.getClass().getName());
//输出org.springframework.validation.support.BindingAwareModelMap
map.put("name", "shoto");
return SUCCESS;
}可以在success.jsp中添加如下语句以用来获取存储在请求域中的Map模型中的name的值。
1
name: ${requestScope.name }
注意:在方法testMap体内,开发者可以通过这个入参对象map访问到模型中的所有数据,也可以向模型中添加新的属性数据。另外这里的Map集合实际是BindingAwareModelMap类,其继承体系如下所示:
1.3 SessionAttributes
若希望在多个请求之间共用某个模型属性数据,则可以在控制器类上标注一个@SessionAttributes, Spring MVC将在模型中对应的属性暂存到 HttpSession 中。
SessionAttributes 除了可以通过属性名指定需要放到会话中的属性外(使用注解的value属性),还可以通过模型属性的对象类型指定哪些模型属性需要放到会话中(使用注解的types属性),下面演示一下其具体的用法:
在控制器类中定义如下方法:
1
2
3
4
5
6
7
8
9"/testSessionAttributes") (
public String testSessionAttributes(Map<String, Object> map) {
// new User(uid, username, password, email, age)
User user = new User(1, "shoto", "abc123", "shoto@gmail.com", 22);
//向map集合中添加user对象以及String类型的值shoto
map.put("user", user);
map.put("name", "shoto");
return SUCCESS;
}在控制器类上添加如下注解:
1
"user"}, types= {String.class}) (value= {
该注解的value的值”user”需要与map.put(“user”, user);语句中的键”user”相同,它表明会将该User对象存放在Session域中(Request域也有)。而tyeps中的值String.class表明会将Map模型中的String类型的值存放在会话域中。
我们可以在结果视图编写如下代码进行模型数据的获取:
1
2
3
4Request user: ${requestScope.user }<br/>
Session user: ${sessionScope.user }<br/>
Request name: ${requestScope.name }<br/>
Session name: ${sessionScope.name }<br/>请求响应成功的结果如下所示:
1.4 ModelAttribute
假设存在这个场景:在页面的表单上,我需要修改当前的用户密码。我们之前的做法可能是先new一个User对象,然后将表单的数据如密码存储在User对象中,然后再使用该User对象去进行数据库对应数据的更新操作,但是这样在进行数据库更新操作时又需要将对应的数据取出来,过程较为麻烦。
现在我们可以使用另一种方式来解决,即先从数据库获取对应要更新的User对象,然后将表单的数据对应更新到该对象的对应属性中(注意:该步骤SpringMVC自动帮我们完成),然后我们可以使用该对象进行对应的数据库更新操作。
首先在index.jsp页面上添加如下代码:
1
2
3
4
5
6<!-- 模拟提交数据后进行相应的数据库数据的修改操作 -->
<form action="testModelAttribute" method="POST">
<input type="hidden" name="uid" value="1"/>
password:<input type="text" name="password" value="abc123"/><br/>
<input type="submit" name="提交"/>
</form>然后需要在控制器方法中添加如下方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void getUser(@RequestParam(value="uid", required=false) Integer uid, Map<String, Object> map) {
//模拟从数据库获取对象
if (uid != null) {
//根据uid中从数据库中获取对象
User user = new User(uid, "Tom", "123456", "shoto@gmail.com", 22);
System.out.println("从数据中获取一个对象:" + user);
//将从数据库获取的user对象存储到数据模型中
map.put("user", user);//这里的键为User类的首字母小写
}
}
"/testModelAttribute") (
public String testModelAttribute(User user) {
System.out.println("修改User对象:" + user);
return SUCCESS;
}运行结果如下所示:
运行流程:
- 首先执行@ModelAttribute 的注解修饰的方法getUser,即从数据库中取出User对象,然后把对象放入Map中,其中的键为”user”;
- SpringMVC 的内部会从Map中取出User对象,并把表单的请求参数赋给该User对象的对应属性。(SpringMVC内部执行过程,可参见源码)
- SpringMVC把上述对象传入目标方法的参数,也就是将User对象传入到testModelAttribute的参数中。
注意: 在@ModelAttribute 修饰的方法中,放入到Map时的键需要和目标方法入参类型的第一个字母小写的字符串一致。上述的Map的键为”user”,即为testModelAttribute方法的入参User的的首字母小写对应的字符串。
当然,我们可以在目标方法testModelAttribute的入参处使用 @ModelAttribute 注解,也就是实现了Map键的自定义,而不是非要与目标方法入参类型的第一个字母小写的字符串一致。具体的代码如下所示:
另外补充一种情况,假如现在控制器类SpringMVCTest的定义如下:
1 | "user"}) (value= { |
即当前有@SessionAttributes注解修饰类,且没有@ModelAttribute 修饰的getUser方法,那么此时会发生Session attribute ‘user’ required - not found in session的异常,SpringMVC底层在处理类定义处标注了@SessionAttributes(value= {“user”})时,则会尝试从会话中获取该属性,也就是值”user”所对应的User对象,并将其赋给该入参user,然后再用请求消息填充该入参对象。如果在会话中找不到对应的属性,则抛出 HttpSessionRequiredException 异常。
ModelAttribute运行处理的具体细节可以参考SpringMVC的源代码!以便加深理解。
2. 视图和视图解析器
之前我们在配置文件中配置过视图解析器InternalResourceViewResolver,其具体的解析过程如下所示:
请求处理方法执行完成后,最终返回一个 ModelAndView对象。对于那些返回 String(如”success”),View 或 ModeMap 等类型的处理方法,Spring MVC 也会在内部将它们装配成一个ModelAndView 对象,它包含了逻辑名和模型对象的视图。
Spring MVC 借助视图解析器(ViewResolver)得到最终的视图对象(View),最终的视图可以是 JSP ,也可能是Excel、JFreeChart 等各种表现形式的视图。
2.1 视图
视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户。为了实现视图模型和具体实现技术的解耦,Spring 在org.springframework.web.servlet 包中定义了一个高度抽象的 View接口。视图对象由视图解析器负责实例化。由于视图是无状态的,也就是每次请求都会创建一个新的视图,所以他们不会有线程安全的问题
下面显示的是一些常用的视图实现类:
2.2 视图解析器
SpringMVC 为逻辑视图名的解析提供了不同的策略,可以在 Spring WEB 上下文中配置一种或多种解析策略,并指定他们之间的先后顺序。每一种映射策略对应一个具体的视图解析器实现类。
视图解析器的作用比较单一:将逻辑视图解析为一个具体的视图对象。所有的视图解析器都必须实现 ViewResolver 接口。
下面显示的一些常用的视图解析器:
- 程序员可以选择一种视图解析器或混用多种视图解析器
- 每个视图解析器都实现了 Ordered 接口并开放出一个 order 属性,可以通过 order 属性指定解析器的优先顺序,order 越小优先级越高。
- SpringMVC 会按视图解析器顺序的优先顺序对逻辑视图名进行解析,直到解析成功并返回视图对象,否则将抛出 ServletException 异常。
2.2.1 示例一
下面我们在项目中使用了 JSTL,那么 SpringMVC 会自动把视图由InternalResourceView 转为 JstlView。并且使用 JSTL 的 fmt 标签实现国际化资源文件的使用。
首先需要添加如下两个Jar包:
然后需要在MVC配置文件applicationContext-mvc.xml中配置国际化资源文件。
1
2
3
4<!-- 配置国际化资源文件 -->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="i18n"/>
</bean>在src路径在创建如下三个文件
其中i18.properties和i18n_en_US.properties的内容如下所示:1
2i18n.username=Username
i18n.password=Passwordi18n_zh_CN.properties的内容如下,分别指用户名和密码的中文:
1
2i18n.username=\u7528\u6237\u540D
i18n.password=\u5BC6\u7801- 在控制器类SpringMVCTest添加如下方法:
1
2
3
4
5"/testJstlView") (
public String testJstlView() {
System.out.println("testJstlView");
return SUCCESS;
}
- 在控制器类SpringMVCTest添加如下方法:
最后在index.jsp编写请求响应,在浏览器上会根据浏览器设置的本地语言对应显示英文或者中文的字样。
若希望直接响应通过 SpringMVC 渲染的页面,可以使用 mvc:viewcontroller 标签实现,在配置文件配置如下:
1 | <!-- 配置直接转发的页面,而无须在经过控制器的方法 --> |
注意:需要配置 <mvc:annotation-driven>
2.2.2 示例二
下面我们演示一下自定义视图类的使用:
首先需要在MVC配置文件进行如下配置:
1
2
3
4
5
6<!-- 配置视图解析器 BeanNameViewResolver:使用视图的名字来解析-->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
<!-- 通过order属性来定义视图解析器的优先级,order值越小优先级越高,
视图解析器的order默认值为Integer.MAX_VALUE -->
<property name="order" value="100"></property>
</bean>编写自定义视图类,具体代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14"abc")//使用该注解将自定义视图定义为组件,记得在扫描注解的配置时要包含该包下的该类。 (value=
public class HelloView implements View {
public String getContentType() {
return "text/html";
}
public void render(Map<String, ?> map, HttpServletRequest req, HttpServletResponse resp) throws Exception {
resp.getWriter().write("Hello View");
}
}在控制器类中定义如下方法并在index.jsp进行请求响应即可
1
2
3
4
5"/testView") (
public String testView() {
System.out.println("testView");
return "abc";
}
注意:testView方法的返回值”abc”与HelloView的Component注解的值相同。abc代表该视图名称,若HelloView的Component注解未指明值,则默认为”helloView”,testView方法的返回值也应该为”helloView”。
3. 重定向
一般情况下,控制器方法返回字符串类型的值会被当成逻辑视图名处理,如果返回的字符串中带 forward: 或 redirect: 前缀时,SpringMVC 会对他们进行特殊处理:将 forward: 和redirect: 当成指示符,其后的字符串作为 URL 来处理。
- redirect:success.jsp:会完成一个到 success.jsp 的重定向的操作
- forward:success.jsp:会完成一个到 success.jsp 的转发操作
下面演示一下重定向操作:
在控制器类中定义如下方法:
1 | "/testRedirect") ( |
此时在index.jsp页面进行请求响应时会进行重定向操作,虽然还是当前页面。