Spring MVC实战系列教程(3)--响应方式
上一节讲述了spring mvc接收客户端参数传递的方式,解决了“入”的问题,本节将要讲述服务端如何生成响应到客户端,即“出”的问题。
(一)直接返回一个字符串,指定视图名。
这是之前在例子中已经遇到的方式,比如这个方法返回"index", 即代表有一个包含该字符串的视图文件存在,再通过spring mvc配置文件中的视图解析器,为该视图名加上前缀和后缀,就获得了该视图在服务器中的路径和名称。即控制器执行完毕就转向执行该web应用下的/WEB-INF/jsp/index.jsp并将执行后生成的HTML作为响应发送给客户端接收
@RequestMapping("/queryString")
public String test1(String userName,String userPass,Integer age){
System.out.println("用户名为:"+userName+",密码为:"+userPass+"年龄为:"+age);
return "index";
}
<!--视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
id="internalResourceViewResolver">
<!-- 前缀 -->
<property name="prefix" value="/WEB-INF/jsp/" />
<!-- 后缀 -->
<property name="suffix" value=".jsp" />
</bean>
在实际应用中, 直接跳转到一个视图通常是没有意义的,既然是MVC,那通常都要携带数据(即model)转向视图,那么怎么将所需数据对象放到响应中呢?可以在方法中添加一个Map类型的参数来保存我们model的部分,其key为String类型,value为Object类型。回想一下我们在servlet中跳转视图时是使用四个作用域对象来传递数据到视图,这四个作用域对象本质上就是一个以String为key,以Object为value的Map对象,所以spring mvc设计成Map完全合理,并且默认作用域是request,示例代码如下:
@RequestMapping("/test1")
public String test1(Map<String, Object> resultMap){
User user = new User();
user.setUserName("张三");
user.setAge(12);
// 将模型数据保存在结果集中
resultMap.put("user", user);
return "index";
}
注意,这里我们手工造了一个User对象,并为其赋值,实际应用中该数据对象应该来自于业务逻辑组件。那么在index.jsp中就可以使用EL来显示user对象的内容${user},当然也可以使用内嵌的java表达式<%=request.getAttribute("user")%>来打印,当然要重写下User类的toString方法,否则打印出来的就是地址。如果观察下浏览器地址栏,发现URL并没有变化,说明spring mvc默认使用服务器端跳转方式而不是响应重定向。
(二)返回ModelAndView对象
spring mvc提供了ModelAndView对象用于将视图逻辑名同模型数据部分封装在一起,这样方法可以直接返回一个封装好的ModelAndView,而不需要提供Map参数。示例如下:
@RequestMapping("/test2")
public ModelAndView test2(){
ModelAndView mav = new ModelAndView();
// 设置视图逻辑名
mav.setViewName("index");
// 构建模型数据
User user = new User();
user.setUserName("李四");
user.setAge(21);
// 将模型加入ModelAndView实例
mav.getModel().put("user", user);
return mav;
}
可以看到,其model部分依然是一个<String,Object>的Map。当然,除了单独调用其成员方法设置视图和模型以外,也可以使用ModelAndView构造器在创建对象时一次性将视图和模型绑定好,比如上面这段代码可以改成这样:
@RequestMapping("/test2")
public ModelAndView test2() {
// 构建模型数据
User user = new User();
user.setUserName("wang5");
user.setAge(24);
ModelAndView mav = new ModelAndView("index", "user", user);
return mav;
}
其效果是一样的。
(三)针对Ajax请求,返回普通字符串
如果是Ajax请求,就不能返回一个视图组件了,而是单独返回一部分数据,比如最简单的就是一个字符串,请看示例代码: @RequestMapping("/test3")
@ResponseBody
public String test3(String name) {
System.out.println("参数:"+name);
return "你的名字是:" + name;
}
可以看出,这种方式跟之前的直接跳转视图的方法结构几乎一模一样,所不同的是为方法增加了一个叫@ResponseBody的注解,该注解表示方法的返回值(字符串)不再是一个视图名,而是直接作为响应体返回给客户端,并且不需要使用HttpServletResponse,PrintWriter这些API仅仅需要增加一个注解就非常简单得完成了一个Ajax的响应。客户端的代码如下:
<script src="http://code.jquery.com/jquery-1.8.3.min.js"></script>
<script type="text/javascript">
$(function(){
//按钮单击时执行
$("#testBtn").click(function(){
$.post("${pageContext.request.contextPath}/jumpToView/test3", {name:"张三" },
function (data){
$("#result").html(data);
});
});
});
</script>
</head>
<body>
<input type="button" id="testBtn" value="ajax请求" />
<span id="result"></span>
</body>
(四)返回JSON数据
对于简单的ajax请求,直接返回一个简单String尚可,但多数情况下需要返回对象或对象集合等复杂数据,这种情况往往使用Json串作为响应。另外,在移动互联网大行其道的今天,服务端同各种APP交互通常也使用Json。这一小节我们就来简单看看如何在spring mvc中将对象或对象集合转成json串,这里我们使用jackson框架来辅助转换过程。
1. 在pom中添加jackson库的依赖:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.1</version>
</dependency>
2. 将单个对象转成json,示例代码如下:
/**
* 使用jackson将单个对象转换为json 串返回
* @param name
* @return
*/
@RequestMapping("/test4")
@ResponseBody
public String test4(String name,Integer age) {
System.out.println("参数1:"+name+",参数2:"+age);
ObjectMapper om = new ObjectMapper();
User user = new User();
user.setUserName(name);
user.setAge(age);
// object 转出 json串
String jsonResult = "";
try {
jsonResult = om.writeValueAsString(user);
System.out.println("转换成的Json串为:"+jsonResult);
} catch (JsonProcessingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return jsonResult;
}
可以看到,使用jackson实现对象与json串直接的转换非常简单,只需使用一个ObjectMapper的writeValueAsString方法即可,请大家自行观察结果。
注意:依然不要忘了@ResponseBody注解,否则spring mvc会去寻找以你的json串命名的jsp
3. 将对象集合转换为json串,没有什么区别,同样使用ObjectMapper的writeValueAsString方法:
/**
* 使用jackson将对象集合转换为json 串返回
* @param name
* @return
* @throws JsonProcessingException
*/
@RequestMapping("/test5")
@ResponseBody
public String test5() {
String jsonResult = "";
List<User> userList = new ArrayList<User>();
User u1 = new User();
User u2 = new User();
User u3 = new User();
u1.setUserName("张三");
u2.setUserName("李四");
u3.setUserName("王五");
userList.add(u1);
userList.add(u2);
userList.add(u3);
ObjectMapper om = new ObjectMapper();
try {
jsonResult = om.writeValueAsString(userList);
} catch (JsonProcessingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return jsonResult ;
}
观察结果,可以看到是以json数值的形式封装的:
[{"userName":"张三","userPass":null,"age":null},{"userName":"李四","userPass":null,"age":null},{"userName":"王五","userPass":null,"age":null}]
(五)controller之间跳转
跟servlet类似,除了可以跳转到一个页面,也可以从一个controller方法跳到另一个controller方法,当然要分服务器端跳转(foward)和客户端跳转(redirect)
1. forward跳转,即在同一个应用内部跳转,客户端只请求一次,在return语句中使用forward:跳转组件的URI关键字即可,示例代码:
/**
* 以foward方式从一个controller调到另一个controller
* 并传递客户端参数和服务端参数
* @param name
* @return
*/
@RequestMapping("/test6")
public String test6(String name,HttpServletRequest req){
req.setAttribute("key1", new String("来自于上一个controller的数据"));
return "forward:/jumpToView/test7";
}
/**
* 跳转到的组件
* @param name
* @param req
* @return
*/
@RequestMapping("/test7")
@ResponseBody
public String test7(String name,HttpServletRequest req){
System.out.println(name+","+req.getAttribute("key1"));
return "测试controller之间跳转";
}
第一个controller方法(test6)不负责响应,把控制器交给跳转到的controller方法,跳转后第二个controller方法(test7)不仅可以接收到客户端传来的请求参数,也可以接受到在第一个controller方法中定义在request作用域中的属性值,说明两个组件都在同一个请求作用域中。观察网络报文,也确实只有一次请求和一次响应。
2. redirect跳转,请求的第一个controller方法会以302状态码响应客户端请求,通知客户端重新请求另一个组件,并另一个组件的URL放入响应头的location字段中发回给客户端。在return语句中使用redirect:跳转组件的URI关键字即可,示例代码如下:
/**
* 以redirect方式跳转到另一个controller方法
* @param name
* @param req
* @return
*/
@RequestMapping("/test8")
public String test8(String name,HttpServletRequest req){
req.setAttribute("key2", new String("来自于上一个controller的数据"));
return "redirect:/jumpToView/test7";
}
观察结果可以发现,跳转成功了,但是第二个组件(test7)接收不到客户端传递给第一个组件(test8)的参数,也接收不到test8自己封装在request中的内部属性,观察HTTP报文,发现确实是生成了两次请求两次响应,所以两个组件实际上不在同一个请求作用域中(request)。
(六)两个额外的问题:
1. 静态资源映射的问题:
按照之前的配置使用spring mvc后,发现动态资源(如jsp,controller等)可以正常请求到,但是像html这种静态资源请求不到了。原因是spring mvc的核心控制器覆盖了web服务器默认对静态资源的映射,需要在springmvc相应配置文件里加入一个Handler配置即可:
<mvc:default-servlet-handler />
这个配置表示将对静态资源的处理交还给web服务器,我们这里是tomcat。
2. 返回字符串响应时的中文乱码问题
不管是普通String,还是Json,只要响应中包含了中文,都不能正常呈现。这是由于spring mvc的转换器(converter)机制对于响应中的文本类型默认采用ISO-8859-1编码,要解决此问题也很简单,在spring mvc配置文件中加入一个converter,指定响应的编码方式为UTF-8即可:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<!-- <value>application/json;charset=UTF-8</value> -->
<value>text/html;charset=UTF-8</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
关于converter的机制我们后面会介绍。
(七)总结
本节介绍了spring mvc生成响应的几种常见方式,下一节我们将介绍使用servlet3.x的特性实现0配置加载spring mvc容器的方式。