SpringMVC-04-结果跳转及数据处理
1、结果跳转
SpringMVC中有两种实现 Handler 的方式:接口实现 和 注解实现,
两种方式对请求结果的处理各有不同。
1.1、接口Handler处理结果
public class ControllerTest implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView mv = new ModelAndView();
mv.addObject("msg", "ControllerTest");
mv.setViewName("/test");
return mv;
}
}
接口Handler使用ModelAndView
对象处理结果
ModelAndView
是SpringMVC中的一种 中间数据对象,
封装了 Model层处理后的结果数据 和 将要跳转视图的逻辑视图名,
走视图解析器,逻辑视图名拼接前后缀。
1.2、注解Handler处理结果
@Controller
@RequestMapping("/h1")
public class HandlerMethodTest {
// 返回值 void 参数有 resp , 结果:不走视图解析器,响应自然返回,由 resp 控制
@RequestMapping("/t1")
public void test1(HttpServletResponse resp) {
System.out.println("方法参数为:resp");
}
// 返回值 void 参数无 resp , 结果:走视图解析器,逻辑视图名 默认为 对应的RequestMappingInfo的路径
@RequestMapping("/t2")
public void test2(HttpServletRequest req, Model model) throws IOException {
model.addAttribute("msg", "test2");
System.out.println("方法参数为:req + model");
}
// 返回值 String 无论参数 ,
// 结果1(没被 @ResponseBody 标注):统一走视图解析器,逻辑视图名 为返回的String值
// 结果2(被 @ResponseBody 标注):不走视图解析器,返回的String值被当作响应体返回
@RequestMapping("/t3")
@ResponseBody
public String test3(HttpServletRequest req, HttpServletResponse resp, Model model) throws Exception {
model.addAttribute("msg", "test3");
return "test";
}
}
注解Handler,即HandlerMethod,
其相应的Handler适配器 根据其返回值和方法参数的不同,有不同的执行策略:
-
返回值 void
-
方法参数有 HttpServletResponse
不走视图解析器,响应自然返回,由 resp 控制
-
方法参数无 HttpServletResponse
走视图解析器,逻辑视图名 默认为 对应的RequestMappingInfo的路径
-
-
返回值 String,无论参数
-
没被 @ResponseBody 标注
统一走视图解析器,逻辑视图名 为返回的String值
-
被 @ResponseBody 标注
不走视图解析器,返回的String值被当作响应体直接返回给客户端浏览器
-
@ResponseBody 作用:
改变 HandlerMethod 的返回值意义,把其当作响应体而不是逻辑视图名直接返回给客户端浏览器,一般标注在返回值为String的 HandlerMethod 上,因为返回的响应体为空值没有意义。
1.3、forward与redirect
@Controller
@RequestMapping("/r1")
public class ResultController {
@RequestMapping("/t1")
public String test(Model model) {
model.addAttribute("msg", "Result1");
return "test";
}
@RequestMapping("/t2")
public String test2(Model model) {
// 转发:forward 视图解析器的特殊前缀,对后面的路径执行转发操作,不做逻辑视图名那样的前后缀拼接
model.addAttribute("msg", "Result2");
return "forward:/WEB-INF/jsp/test.jsp";
}
@RequestMapping("/t3")
public String test3(Model model) {
// 重定向:redirect 视图解析器的特殊前缀,对后面的路径执行重定向操作,不做逻辑视图名那样的前后缀拼接
model.addAttribute("msg", "Result3");
return "redirect:/r1/t2";
}
}
forward:
和redirect:
为 视图解析器的特殊前缀,
对后面的路径执行转发或者重定向操作,不做逻辑视图名那样的前后缀拼接。
官方文档翻译:
指定转发或重定向URL的特殊视图名称的前缀(通常在表单提交和处理后发送给控制器)。
此类视图名称将不会以配置的默认方式解析,而是被视为特殊的快捷方式。
2、参数数据绑定
在SpringMVC 使用注解处理请求的方式中,框架会对 HandlerMethod 的方法参数 进行数据绑定,以便于更简洁的处理请求。
数据绑定的方式分为三种:传统数据绑定、路径变量绑定 和 特定参数绑定
三种绑定方式对于不同的方法参数皆有不同的处理。
注意:后端获取前端数据,都是把它当成String类型获取,再将其解析转换成相应的类型
2.1、传统数据绑定
获取 请求参数 为绑定值:request.getParameter("参数名")
-
普通参数
默认绑定的请求参数 与 方法参数 同名,非必要(即,可以不传相应参数,值为 null)
若要绑定其他请求参数,可用 @RequestParam 注解 指定参数名称 和 是否必要@GetMapping("/t1") public String t1(@RequestParam("username") String name, Model model) { // 1.接收前端参数 System.out.println("接收到前端的参数为:" + name); // 2.将返回的值传递给前端 model.addAttribute("msg", name); // 3.跳转视图 return "test"; }
-
实体类
框架会先创建这个实体类的对象,然后通过类的 属性名 去和 请求参数 进行数据绑定,属性绑定请求参数,非必要
@Data @AllArgsConstructor @NoArgsConstructor class User { private Integer id; private String name; private String pwd; }
@GetMapping("/t2") public String t2(User user, Model model) { // 1.接收前端参数 System.out.println("接收到前端的参数为:" + user.toString()); // 2.将返回的值传递给前端 model.addAttribute("msg", user.toString()); // 3.跳转视图 return "test"; }
-
万能的Map
Map类型的参数 可以收集绑定 所有的请求参数,必须和 @RequestParam 注解一起使用,不然框架只会传一个空Map进来
使用Map参数,会让程序更加灵活多变,具备扩展性@GetMapping("/t3") public String t3(@RequestParam Map<String, Object> map, Model model) { // 1.接收前端参数 System.out.println("接收到前端的参数为:" + map.toString()); // 2.将返回的值传递给前端 model.addAttribute("msg", map.toString()); // 3.跳转视图 return "test"; }
2.2、路径变量绑定
路径变量,即 URI模板变量,是一种简化URL配置的方式,它允许你使用占位符来表示路径动态变化的部分。
编写方式:/{占位符1}/{占位符2}……
例子:/users/{userId}/posts/{postId},userId
和postId
即是路径变量,变量值由前端具体的URL格式化得来。
路径变量 可以被绑定到 HandlerMethod 的方法参数上,这是一种将前端参数写入URL路径的方式,
其只用URL来表示具体的资源位置,符合Rest风格。
获取 路径变量 为绑定值:格式化具体的URL得来
-
普通参数
必须为方法参数标注 @PathVariable 注解
默认绑定的路径变量 与 方法参数 同名,必要
若要绑定其他的路径变量,可用 @PathVariable 注解 指定变量名称 和 是否必要@GetMapping("/t4/{username}/{age}") public String t4(@PathVariable("username") String name, @PathVariable Integer age, Model model) { // 1.接收前端参数 System.out.println("接收到前端的参数为:" + name); System.out.println("接收到前端的参数为:" + age); // 2.将返回的值传递给前端 model.addAttribute("msg", name + "-" + age); // 3.跳转视图 return "test"; }
-
实体类
框架会先创建这个实体类的对象,然后通过类的 属性名 去和 路径变量 进行数据绑定,属性绑定路径变量,非必要
@GetMapping("/t5/{id}/{name}/{pwd}") public String t5(User user, Model model) { // 1.接收前端参数 System.out.println("接收到前端的参数为:" + user.toString()); // 2.将返回的值传递给前端 model.addAttribute("msg", user.toString()); // 3.跳转视图 return "test"; }
2.3、特定参数绑定
HandlerMethod 的方法参数中有一些特定的类型,比如:HttpServletRequest、HttpServletResponse、Model、ModelMap……
它们做数据绑定时,由框架传入特定的对象
这些类型基本上都是框架内部组织的一部分:
HttpServletRequest 代表 请求、
HttpServletResponse 代表 响应、
Model、ModelMap 代表 中间数据容器
获取 框架内部对象 为绑定值:由框架传入
-
特定参数
@GetMapping("/t6") public String t6(HttpServletRequest req, HttpServletResponse resp, Model model) { // 1.接收前端参数 System.out.println("接收到前端的参数为:" + req); System.out.println("接收到前端的参数为:" + resp); // 2.将返回的值传递给前端 model.addAttribute("msg", req); model.addAttribute("msg2", resp); // 3.跳转视图 return "test"; }
3、中间数据对象
-
ModelAndView
模型层结果数据 和 视图信息 的结合封装,内部其实组合了
ModelMap
对象,一般用于 接口Handler 处public class ControllerTest1 implements Controller { public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception { //返回一个模型视图对象 ModelAndView mv = new ModelAndView(); mv.addObject("msg","ControllerTest1"); mv.setViewName("test"); return mv; } }
-
ModelMap
实现了
Map
接口的实现类,用于存储 模型层结果数据 ,常用于 注解Handler 处@RequestMapping("/hello") public String hello(@RequestParam("username") String name, ModelMap model){ //封装要显示到视图中的数据 //相当于req.setAttribute("name",name); model.addAttribute("name",name); System.out.println(name); return "hello"; }
-
Model
模型数据存储对象 的标准接口,代表了规范的定义,常用于 注解Handler 处
值得一提的是,Model
的实现类大都继承了ModelMap
@RequestMapping("/hello2") public String hello(@RequestParam("username") String name, Model model){ //封装要显示到视图中的数据 //相当于req.setAttribute("name",name); model.addAttribute("msg",name); System.out.println(name); return "test"; }
三者对比
就对于新手而言简单来说,使用区别就是:
Model 只是标准接口,只有寥寥几个方法用于储存数据,简化了新手对于Model对象的操作和理解;
ModelMap 继承了 LinkedMap ,除了实现了自身的一些方法,同样的继承 LinkedMap 的方法和特性;
ModelAndView 可以在储存数据的同时,可以进行设置返回的逻辑视图名,控制视图层的跳转。
当然,以后开发考虑的更多的是性能和优化,就不能单单仅限于此的了解。
三者关系
4、乱码问题
测试步骤:
-
编写一个提交的表单 /WEB-INF/jsp/EncodeTest.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>EncodeTest</title> </head> <body> <p>Get表单: </p> <form action="/spring04/e/t" method="get"> <input type="text" name="name"> <input type="submit"> </form> <p>Post表单: </p> <form action="/spring04/e/t" method="post"> <input type="text" name="name"> <input type="submit"> </form> ${msg} </body> </html>
-
后台编写对应的处理类
@Controller public class Encoding { @RequestMapping("/e/t") public String test(String name, Model model) throws UnsupportedEncodingException { System.out.println(name); model.addAttribute("msg", name); //获取表单提交的前端参数 return "EncodeTest"; //跳转到视图显示输入的值 } }
-
Get 中文参数测试,结果:显示正常,无乱码
-
Post 中文参数测试,结果:出现乱码
Post请求中文参数乱码
原因分析:
总所周知,Post参数数据存储在请求体里面,以页面编码解码,变成字节流形式存储,以二进制流的形式发送到的服务器。
服务器收到数据后,以默认编码进行编码。
这里,我的服务器用的是Tomcat9,默认编码为 ISO-8859-1 ,页面编码用的是 UTF-8
前后端编码不一致,导致乱码。
解决:
给服务器设置解析请求的字符集即可,request.setCharacterEncoding("UTF-8");
在项目中,可以写一个过滤器来操作,这里我们可以使用SpringMVC提供的字符集过滤器,在web.xml中配置
<filter>
<filter-name>Encoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Encoding</filter-name>
<servlet-name>DispatcherServlet</servlet-name>
</filter-mapping>
当然,我们也可以自定义过滤器,这里提供网上一位大神的写的
package com.kuang.filter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Map;
/**
* 解决get和post请求 全部乱码的过滤器
*/
public class GenericEncodingFilter implements Filter {
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//处理response的字符编码
HttpServletResponse myResponse=(HttpServletResponse) response;
myResponse.setContentType("text/html;charset=UTF-8");
// 转型为与协议相关对象
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// 对request包装增强
HttpServletRequest myrequest = new MyRequest(httpServletRequest);
chain.doFilter(myrequest, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
}
//自定义request对象,HttpServletRequest的包装类
class MyRequest extends HttpServletRequestWrapper {
private HttpServletRequest request;
//是否编码的标记
private boolean hasEncode;
//定义一个可以传入HttpServletRequest对象的构造函数,以便对其进行装饰
public MyRequest(HttpServletRequest request) {
super(request);// super必须写
this.request = request;
}
// 对需要增强方法 进行覆盖
@Override
public Map getParameterMap() {
// 先获得请求方式
String method = request.getMethod();
if (method.equalsIgnoreCase("post")) {
// post请求
try {
// 处理post乱码
request.setCharacterEncoding("utf-8");
return request.getParameterMap();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
} else if (method.equalsIgnoreCase("get")) {
// get请求
Map<String, String[]> parameterMap = request.getParameterMap();
if (!hasEncode) { // 确保get手动编码逻辑只运行一次
for (String parameterName : parameterMap.keySet()) {
String[] values = parameterMap.get(parameterName);
if (values != null) {
for (int i = 0; i < values.length; i++) {
try {
// 处理get乱码
values[i] = new String(values[i]
.getBytes("ISO-8859-1"), "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
}
hasEncode = true;
}
return parameterMap;
}
return super.getParameterMap();
}
//取一个值
@Override
public String getParameter(String name) {
Map<String, String[]> parameterMap = getParameterMap();
String[] values = parameterMap.get(name);
if (values == null) {
return null;
}
return values[0]; // 取回参数的第一个值
}
//取所有值
@Override
public String[] getParameterValues(String name) {
Map<String, String[]> parameterMap = getParameterMap();
String[] values = parameterMap.get(name);
return values;
}
}
Get请求无乱码分析
在上面的测试中可以看到,在Post请求乱码的情况下,Get请求能够正常显示中文字符,这是什么原因呢?
Get请求的参数写在URL中,一般来说,URL只能使用英文字母、阿拉伯数字和某些标点符号,不能使用其他文字和符号。
这是因为网络标准RFC 1738做了硬性规定:
"...Only alphanumerics [0-9a-zA-Z], the special characters "$-_.+!*'()," [not including the quotes - ed], and reserved characters used for their reserved purposes may be used unencoded within a URL."
"只有字母和数字[0-9a-zA-Z]、一些特殊符号"$-_.+!*'(),"[不包括双引号]、以及某些保留字,才可以不经过编码直接用于URL。"
这意味着,如果URL中有汉字,就必须编码后使用。但是麻烦的是,RFC 1738没有规定具体的编码方法,而是交给应用程序(浏览器)自己决定。这导致"URL编码"成为了一个混乱的领域。
想深入的可以去看看 关于URL编码 这篇文章,初学者只需要知道现在大部分情况下,浏览器都使用 UTF-8 作为URL编码。
于是,就上面的测试而言,
http://localhost:8080/spring04/e/t?name=斗破苍穹
经过 UTF-8 编码转换,得到
http://localhost:8080/spring04/e/t?name=%E6%96%97%E7%A0%B4%E8%8B%8D%E7%A9%B9
然后,服务器接收到请求,用指定解析URL的编码对其进行解码,得到参数值
Tomcat9 解析URL的默认编码为 UTF-8,具体的值可以在Tomcat的 server.xml 中进行配置
即,Connector
标签中的 URIEncoding
属性
<Connector URIEncoding="utf-8" port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
乱码问题,需要平时多注意,在尽可能能设置编码的地方,都设置为统一编码 UTF-8!