Spring MVC的内容比较多也比较复杂,上面介绍了常用的内容,但是还有一些比较琐碎且常用的知识需要介绍,故本节命名为"拾遗".
1. @ResponseBody转换为JSON的秘密
一直以来,当想要把控制器的返回转变成JSON数据集时,只需要在方法上标注@ResponseBody即可,那个Spring MVC是如何做到的呢?回到上一节中,在进入控制器方法之前,当遇到标注的@ResponseBody后,处理器就会记录这个方法的响应类型为JSON数据集.当执行完控制器返回后,处理器会启用结果解析器(ResultResolver)去解析这个结果,他会去轮询注册给Spring MVC的HttpMessageConverter接口的实现类.因为MappingJackson2HttpMessageConverter这个实现类已经被Spring MVC注册,加上Spring MVC将控制器的结果类型标注为JSON,所以就匹配上了,于是通过他就可以在处理器内部把结果转换为JSON了.当然有时候轮询不到匹配的HttpMessageConverter,那么他会交由Spring MVC后续流程去处理.如果控制器返回结果被MappingJackson2HttpMessageConverter进行了转换,那么后续的模型和视图(ModelAndView)就返回null,这样视图解析器和视图渲染将不再执行,其流程如图.
2. 重定向
重定向(Redirect)就是通过各种方法将各种网络请求重新定一个方向转到其他位置.这里继续使用代码中的data.html视图,这里需要完成插入一个新的角色信息到数据库,而插入之后需要通过该视图展现给请求者.假设原本就存在一个showRole方法来展示用户,这样我们希望的是插入用户之后,就使用这个showRole方法来展示用户,这样旧的功能就能够重用了.下面来完成这个功能.
/**
* 使用字符串指定跳转
* @param name
* @param remark
* @return
*/
@GetMapping("/redirect1")
public String redirect1(String name, String remark){
try {
SysRole sysRole = new SysRole();
sysRole.setName(name);
sysRole.setRemark(remark);
System.out.println(sysRole.getName());
System.out.println(sysRole.getRemark());
sysRole.setCreateBy("李登印");
sysRole.setCreateTime(new Date());
sysRole.setLastUpdateBy("李登印");
sysRole.setLastUpdateTime(new Date());
sysRole.setDelFlag((byte)0);
sysRoleService.save(sysRole);
System.out.println("id:"+sysRole.getId());
return "redirect:/role/show?id="+sysRole.getId();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
/**
* 使用模型和视图指定跳转
* @param name
* @param remark
* @return
*/
@GetMapping("/redirect2")
public ModelAndView redirect2(String name, String remark){
try {
SysRole sysRole = new SysRole();
sysRole.setName(name);
sysRole.setRemark(remark);
System.out.println(sysRole.getName());
System.out.println(sysRole.getRemark());
sysRole.setCreateBy("李登印");
sysRole.setCreateTime(new Date());
sysRole.setLastUpdateBy("李登印");
sysRole.setLastUpdateTime(new Date());
sysRole.setDelFlag((byte)0);
sysRoleService.save(sysRole);
System.out.println("id:"+sysRole.getId());
ModelAndView mv = new ModelAndView();
mv.setViewName("redirect:/role/show?id="+sysRole.getId());
return mv;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
/**
* 显示用户
* @param id
* @param model
* @return
*/
@RequestMapping("/show")
public String show(Long id, Model model){
//访问模型层得到数据
SysRole sysRole = sysRoleService.findById(id);
//加入数据模型
model.addAttribute("sysRole", sysRole);
//返回模型和数据
return "/role/details";
}
另外给出details.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>权限列表</title>
</head>
<body>
<table border="1">
<tr>
<td>角色编号</td>
<td>角色名称</td>
</tr>
<tr>
<td th:text="${sysRole.id}"></td>
<td th:text="${sysRole.name}"></td>
</tr>
</table>
</body>
</html>
代码中的showRole方法查询用户信息后.绑定到数据模型中,然后返回一个字符串,他只向html视图,这样视图就能够把数据模型的数据渲染出来
redirect1方法是先新增角色信息,而新增角色数据库会返回角色编号(id),然后通过以"redirect:"开头的字符串,然后后续的字符串指向/role/details这个URL,并且将id作为参数传递,这样就可以调用这个请求.在redirect2的方法中,类似redirect1的方法,先插入角色,但是他将视图名称转化为redirect1返回的字符串,这样Spring MVC就可以执行重定向.
这里使用一个参数id传递给showRole方法,redirect1和redirect2方法已经包含了sysRole对象的全部信息.而在showRole方法却要重新查询一次,这个是不合理的.如果将SysRole对象直接传递给showRole方法,这在URL层面是完成不了的,好在Spring MVC也考虑到了这样的场景.它提供了REdirectAttriburees,这是一个扩展了ModelMap的接口,他有一个addFlashAttribute方法,这个方法可以保存需要传递给重定向的数据,改写代码
/**
* 使用字符串指定跳转
* @param name
* @param remark
* @return
*/
@GetMapping("/redirect1")
public String redirect1(String name, String remark,RedirectAttributes ra){
try {
SysRole sysRole = new SysRole();
sysRole.setName(name);
sysRole.setRemark(remark);
System.out.println(sysRole.getName());
System.out.println(sysRole.getRemark());
sysRole.setCreateBy("李登印");
sysRole.setCreateTime(new Date());
sysRole.setLastUpdateBy("李登印");
sysRole.setLastUpdateTime(new Date());
sysRole.setDelFlag((byte)0);
sysRoleService.save(sysRole);
System.out.println("id:"+sysRole.getId());
ra.addFlashAttribute("sysRole", sysRole);
return "redirect:/role/show";
}catch (Exception e){
e.printStackTrace();
}
return null;
}
/**
* 使用模型和视图指定跳转
* @param name
* @param remark
* @return
*/
@GetMapping("/redirect2")
public ModelAndView redirect2(String name, String remark, RedirectAttributes ra){
try {
SysRole sysRole = new SysRole();
sysRole.setName(name);
sysRole.setRemark(remark);
System.out.println(sysRole.getName());
System.out.println(sysRole.getRemark());
sysRole.setCreateBy("李登印");
sysRole.setCreateTime(new Date());
sysRole.setLastUpdateBy("李登印");
sysRole.setLastUpdateTime(new Date());
sysRole.setDelFlag((byte)0);
sysRoleService.save(sysRole);
System.out.println("id:"+sysRole.getId());
ra.addFlashAttribute("sysRole", sysRole);
ModelAndView mv = new ModelAndView();
mv.setViewName("redirect:/role/show");
return mv;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
/**
* 显示用户
* 参数SysRole可以直接子从数据模型RedirectAttributes对象中取出
* @param
* @param model
* @return
*/
@RequestMapping("/show")
public String show(SysRole sysRole, Model model){
//加入数据模型
model.addAttribute("sysRole", sysRole);
//返回模型和数据
return "/role/details";
}
上述代码中加入了RedirectAttributes对象的参数,然后将redirect1和redirect2方法中插入的用户信息通过addFlashAttribute方法保存起来,在执行重定向到showRole方法中,并且将sysRole对象传递,那她是怎么做到的呢?
首先,被addFlashAttribute方法保存的参数,在控制器执行完成之后,会被保存到session对象中.当执行重定向的时候,在进入重定向之前把Session中的参数取出,用以填充重定向方法的参数和数据模型,之后删除Session中的数据,之后删除重定向的数据,然后就可以调用重定向方法,并将对象传递给重定向的方法.其流程如下:
3. 操作会话对象
在Web应用中,操作会话(HttpSession)对象是十分普遍的,对此Spring MVC也提供了支持.主要是两个注解用来操作HttpSession对象,他们是@SessionAttribute和@SessionAttributes.其中,@SessionAttribute应用于参数,他的作用是将HttpSession中的属性读出,赋予控制器的参数;@SessionAttributes则只能用于类的注解,他会把相关数据模型的属性保存到session中.(我暂时没法给出例子)
4. 给控制器增加通知
在Spring AOP中,可以通过通知来增强Bean的功能.同样的Spring MVC也可以给控制器增加通知,于是在控制器方法的前后和异常发生时去执行不同的处理.这里涉及4个注解,他们是@ControllerAdvice,@initBinder,@ExceptionHandler和@ModelAttribute.这里需要注意的是他们的作用和执行的顺序
- @ControllerAdvice:定义一个控制器的通知类,允许定义一些关于增强控制器的各类通知和限定增强那些控制器的功能等.
- @InitBinder:定义控制器参数绑定规则,如转换规则,格式化等.他会在参数转换之前执行.
- @ExceptionHandler:定义控制器发生异常后的操作.一般来说,发生异常之后可以指定到友好页面,以避免用户使用的不友好.
- @ModelAttribute:可以在控制器方法之前,对数据模型进行操作.
下面展示这些注解的使用方法.首先是创建控制器的通知,代码如下:
package cn.hctech2006.boot.bootmvc.controller.advice;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import java.text.SimpleDateFormat;
import java.util.Date;
@ControllerAdvice(
//指定拦截的包
basePackages = {"cn.hctech2006.boot.bootmvc.controller.*"},
//限定标注为@Controller的类才会拦截
annotations = Controller.class
)
public class MyControllerAdvice {
@InitBinder
public void initDataBinder(WebDataBinder webDataBinder){
//自定义时间日期编辑器,限定格式为yyyy-MM-dd,且参数不允许为空
CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"),false);
//注册自定义日期编辑器
webDataBinder.registerCustomEditor(Date.class, dateEditor);
}
//在执行控制器之前执行,可以初始化数据模型
@ModelAttribute
public void projectModel(Model model){
model.addAttribute("prject_name", "chapter10");
}
//异常处理,使得被拦截的控制器方法发生异常时,都嫩狗用相同的视图响应
@ExceptionHandler(value = Exception.class)
public String exception(Model model, Exception ex){
//给数据模型增加异常信息
model.addAttribute("exception_messgae",ex.getMessage());
//返回异常视图
return "exception";
}
}
下面来具体阐述他们的作用
- @ControllerAdvice表明这是一个控制器通知类,这个注解也标注了Controller,所以他会在SpringIoc容器中自动扫描和装配.他的配置项basePackages配置的是包名限制,也就是符合该配置的包的扫描器才会被这个控制器通知所拦截,而annotations的配置则是在原有包名限定的基础上再添加被标注为@Controller的类才会被拦截.
- @initBinder是一个在控制器参数转换之前执行的代码.这里的WebDataBinder参数对象是Spring MVC会自动生成的参数,这里定义了日期(Date)类型的参数,采用了限定格式"yyyy-MM-dd",则不在需要加入@DateTimeFormat对格式在进行指定,直接采用"yyyy-MM-dd"格式传递日期参数即可.
- @ModelAttribute是一个数据模型的注解.他在执行控制器方法之前被执行,代码中增加了一个工程名称(prject_name)的字符串,因此在控制器中可以获取他
- @ExceptionHandler的配置项为Exception,它可以拦截所有控制器发生的异常.这里的Exception参数是SpringMVC执行控制器发生异常时传递的,而在方法中,给数据模型添加了异常信息,然后返回一个字符串exception,这个字符串指向HTML视图.先来完成控制器
package cn.hctech2006.boot.bootmvc.controller.advice.test;
@Controller
@RequestMapping("/test")
public class AdviceController {
public String test(Date date, ModelMap modelMap){
//从数据模型中获取数据
System.out.println(modelMap.get("project_name"));
//打印日期参数
System.out.println(date);
//抛出异常,这样流转到控制器异常通知李.
throw new RuntimeException("异常了,跳转到控制器通知的异常信息里面");
}
}
这个控制器所在的包刚好是控制器通知(MyControllerAdvice)所指定的包,他标注的@Controller也是通知指定的注解,这样控制器通知就可以拦截这个控制器.首先他会先执行其标注了@initBinder和@ModelAttribute的两个方法.因为标注@InitBinder的方法设定了日期格式为"yyyy-MM-dd",所以控制器的日期参数并没有加入格式的限定.而标注@ModelAttribute的方法在数据模型中设置了新的属性,所以这里的控制器也能从数据模型中获取数据.控制器方法最后抛出异常,这样就会让MyControllerAdvice标注@ExceptionHandler的方法触发,并且将异常消息传递给他.为了让异常通知被展示,需要一个视图,代码如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" http-equiv="Content-Type" content="text/html">
<title>异常界面</title>
<script src="http://code.jquery.com/jquery-3.2.0"></script>
</head>
<body>
<table>
<tr>
<td th:text="${exception_messgae}"></td>
</tr>
</table>
</body>
</html>
通过上面的代码可以将控制器异常通知锁绑定的异常消息渲染到html中.这样凡是控制器发生异常,就能够通过对应的异常页面给渲染出来,从而避免系统对使用者的不友好,提高网站的友好度.
通过上面的代码,整个流程就开发完成了.如图所示
控制台输出
chapter10
Sat May 23 00:00:00 CST 2020
显然测试的结果是成功了.
5. 获取请求头参数
在Http请求中,有些网站会利用请求头的数据进行身份验证,所以有时还会在控制器中拿到请求头的数据.在Spring MVC中可以通过注解@RequestHeader进行获取.下面先来写一个HTML页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" http-equiv="Content-Type" content="text/html">
<title>获取请求头参数</title>
<script src="http://code.jquery.com/jquery-3.2.0.js"></script>
<script type="text/javascript">
$.post({
url : "./role",
headers : {id : 1},
success : function (sysRole) {
if(sysRole == null || sysRole.id == null){
alert("获取失败");
return;
}
//弹出请求返回的用户信息
alert("id="+sysRole.id)
}
});
</script>
</head>
<body>
</body>
</html>
代码中用脚本对控制器发出了请求,并且设置了请求头,是一个键为id而值为1的请求头,这样的请求头也会发送到控制器中.那么控制器该怎么获取请求头参数呢?其实也是十分简单的,使用注解@RequestHeader就可以了.下面在SysRoleController中加入代码如下
@GetMapping("/header/page")
public String headerPage(){
return "/role/header";
}
@PostMapping("/header/role")
@ResponseBody
//通过@RequestHeader接受请求头参数
public SysRole headsysRole(@RequestHeader("id") Long id){
SysRole sysRole = sysRoleService.findById(id);
return sysRole;
}
代码中headerPage方法是请求代码清单的HTML页面.headderSysRole方法中的参数id则是使用注解@RequestHeader(“id”)它代表从请求头中获取键为id的参数,这样就可以从请求头中获取参数.在浏览器地址栏中输入
http://localhost:8243/role/header/page就可以看到结果,如图