Spring MVC学习—项目统一异常处理机制详解与使用案例
Posted L-Java
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring MVC学习—项目统一异常处理机制详解与使用案例相关的知识,希望对你有一定的参考价值。
基于最新Spring 5.x,详细介绍了Spring MVC项目的统一异常处理机制,比如@ExceptionHandler和容器错误页面。
本次我们来学习Spring MVC项目的统一异常处理机制的几种实现方式,统一异常处理能够为我们节省大量处理异常的时间,让我们编写出优美的代码。
Spring MVC学习 系列文章
Spring MVC学习(1)—MVC的介绍以及Spring MVC的入门案例
Spring MVC学习(2)—Spring MVC中容器的层次结构以及父子容器的概念
Spring MVC学习(3)—Spring MVC中的核心组件以及请求的执行流程
Spring MVC学习(4)—ViewSolvsolver视图解析器的详细介绍与使用案例
Spring MVC学习(5)—基于注解的Controller控制器的配置全解【一万字】
Spring MVC学习(6)—Spring数据类型转换机制全解【一万字】
Spring MVC学习(7)—Validation基于注解的声明式数据校验机制全解【一万字】
Spring MVC学习(8)—HandlerInterceptor处理器拦截器机制全解
Spring MVC学习(9)—项目统一异常处理机制详解与使用案例
Spring MVC学习(10)—文件上传配置、DispatcherServlet的路径配置、请求和响应内容编码
Spring MVC学习(11)—跨域的介绍以及使用CORS解决跨域问题
1 HandlerExceptionResolver异常解析器
1.1 HandlerExceptionResolver概述
如果在请求映射至Handler到执行拦截器链的postHandle后处理之间,包括解析HandlerAdapter,执行Handler(执行业务逻辑)等过程中抛出了异常,那么DispatcherServlet将委托HandlerExceptionResolver链来解决异常并提供替代的处理方法,比如返回统一的异常视图,或者响应指定的错误信息。
通过自定义异常以及自定义异常解析器,可以为应用实现统一的异常处理逻辑,在开发中非常有用!当然,这并不一定是最高效的和最常见的方式,因为我们后面还会见识到如何采用@ExceptionHandler注解快速实现统一的异常处理逻辑!
HandlerExceptionResolver接口仅有一个resolveException方法用于处理异常:
public interface HandlerExceptionResolver {
/**
* 尝试解决在处理程序执行期间抛出的给定异常,返回ModelAndView,表示特定错误页面(如果适用)。
* <p>
* 返回的ModelAndView可能是空的(ModelAndView.isEmpty返回true),这表示表示异常已成功解决,并且不会返回任何异常视图
*
* @param request 当前 HTTP 请求
* @param response 当前 HTTP 响应
* @param handler 需要被执行的handler,可能为null
* @param ex 在handler执行期间抛出的异常
* @return 一个用于转发到异常视图的ModelAndView,如果异常一杯处理并且不需要返回异常视图,那么可以为null
*/
@Nullable
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
resolveException方法将返回ModelAndView类型的结果,返回值有如下约定:
- ModelAndView对象中可以包含包含响应的错误的数据和一个要转发到的错误视图。
- 如果异常在解析器中已被处理,并且不需要返回异常视图,则可以返回一个空的ModelAndView。
如果异常仍未解决,则返回null,以便后面的异常解析器继续尝试解决,如果解析器链执行完毕异常仍未解决,则该异常将直接向上抛出到 Servlet 容器。
1.2 默认实现
Spring MVC为我们提供了一些默认的HandlerExceptionResolver实现,它们都有不同的处理异常的方式!
HandlerExceptionResolver | Description |
---|---|
SimpleMappingExceptionResolver | 维护了异常类型名到错误视图名之间的映射关系,可用于在浏览器应用程序中呈现错误页面。 |
DefaultHandlerExceptionResolver | 解析Spring MVC 引发的异常,将它们映射到HTTP状态代码并返回给客户端。 |
ResponseStatusExceptionResolver | 解析使用@ResponseStatus注解表示的异常,将注解中的值映射到HTTP状态代码并返回给客户端。 |
ExceptionHandlerExceptionResolver | 通过调用在@Controller类或者@ControllerAdvice类中的具有@ExceptionHandler注解的方法来解决异常。 |
Spring 5.2.8.RELEASE版本中,默认情况下,将会注册ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver、DefaultHandlerExceptionResolver这三个异常处理器(在DispatcherServlet.properties配置文件中)!
也就是说默认支持Spring MVC 异常、@ResponseStatus注解异常、@ExceptionHandler注解的异常处理方法。
1.3 自定义异常解析器
如果具有自定义的异常解析器,那么就不会加载默认的异常解析器!
1.3.1 自定义异常
我们自定义一个异常!
/**
* 自定义异常类
*
* @author lx
*/
public class SysException extends Exception {
private String message;
@Override
public String getMessage() {
return message;
}
public SysException(String message) {
this.message = message;
}
}
1.3.2 异常视图页面
当抛出异常之后,我们将信息转发到对应的异常视图:
<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head>
<title>异常页面</title>
</head>
<body>
<h1>出问题啦!</h1>
<br/>
<span style="color: #dc143c; ">${errorMsg}</span>
</body>
</html>
1.3.3 自定义异常解析器
我们自定义一个异常解析器,用于将异常信息转发到error.jsp异常视图!
我们直接将异常解析器通过注解或者XML交接Spring管理即可,DispatcherServlet中会自动查找所有HandlerExceptionResolver的bean。
/**
* 自定义异常处理器
*
* @author lx
*/
@Component
public class SysExceptionResolver implements HandlerExceptionResolver {
/**
* 处理异常业务逻辑
*/
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 获取到异常对象
SysException e;
//异常类型区分
if (ex instanceof SysException) {
e = (SysException) ex;
} else {
e = new SysException("系统正在维护....");
}
/*
* 创建ModelAndView对象
* 设置模型信息,即异常提示,以及需要跳转的异常视图名
*
*/
ModelAndView mv = new ModelAndView();
mv.addObject("errorMsg", e.getMessage());
mv.setViewName("error.jsp");
return mv;
}
}
1.3.4 测试
我们准备一个Controller,分别模拟抛出SysException和其他异常!
@Controller
public class HandlerExceptionResolverController {
/**
* 模拟抛出SysException
*/
@RequestMapping("/err1")
public void handlerExceptionResolver() {
throw new SysException("你的网络不好,请稍等…………");
}
/**
* 模拟抛出其他异常
*/
@RequestMapping("/err2")
public void handlerExceptionResolver2() {
throw new RuntimeException();
}
}
启动项目,尝试访问/err1,得到如下结果:
尝试访问/err2,得到如下结果:
1.4 异常解析器链
我们可以通过在 Spring 配置中声明多个HandlerExceptionResolver的 bean,并根据需要设置其顺序属性来形成异常解析器链。并且支持order排序,order值越大,异常解析器的在链中的位置越靠后(解析器链实际上是一个List集合)。我们通过实现Ordered接口或者使用@Order注解来确定order值,如果不配置order值,那么默认为最大值,即Integer.MAX_VALUE,也就是说在链尾部!
当抛出异常时,DispatcherServlet将会依次调用异常解析器链的每一个析器的resolveException方法,如果当前异常解析器的resolveException方法返回null,那么表示未能成功处理该异常,那么继续调用下一个异常解析器,否则,表示异常处理器成功,不会继续调用后续的解析器!
我们为SysExceptionResolver添加@Order注解:
新建另一个异常解析器SysExceptionResolver2,它的order值为0,小于2,因此它将会被先于SysExceptionResolver调用!
/**
* 自定义第二个异常处理器
*
* @author lx
*/
@Component
@Order(0)
public class SysExceptionResolver2 implements HandlerExceptionResolver {
/**
* 处理异常业务逻辑
*/
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
//直接设置ModelAndView
ModelAndView mv = new ModelAndView();
mv.addObject("errorMsg", "第二个异常处理器");
mv.setViewName("error.jsp");
return mv;
}
}
启动项目,访问/err1和/err2,均得到如下结果:
如果将SysExceptionResolver2的@Order注解值改为大于2,或者去掉该注解,那么SysExceptionResolver将会先被调用:
2 @ExceptionHandler统一异常处理
在上面的HandlerExceptionResolver
的学习中,我们通过自定义HandlerExceptionResolver来实现项目统一的异常处理,这是完全没问题的,但是,却并不一定是最好用的!另外,如果有自定义的HandlerExceptionResolver,那么默认的HandlerExceptionResolver实现就不会再配置!
Spring提供了HandlerExceptionResolver
的几个默认实现,它们具有自己的可扩展的解决异常的方式,我们完全可以直接利用这些默认实现,只需要配置对应的异常处理的方案,而无需再自定义HandlerExceptionResolver,无需自己编写完整的异常处理逻辑!
此前我们说过,ExceptionHandlerExceptionResolver
通过调用在@Controller类或者@ControllerAdvice类中的具有@ExceptionHandler注解的方法来解决来自Controller方法的异常,这实际上就是一种非常常用并且简单的处理异常的方式,在目前的项目中,大多使用该方式!
2.1 @ExceptionHandler测试
我们定义一个ExceptionHandlerController,其内部有一个@ExceptionHandler方法,该方法返回一个异常视图:
/**
* @author lx
*/
@Controller
public class ExceptionHandlerController {
@RequestMapping("/eh1")
public void eh1() {
throw new RuntimeException("eh测试");
}
/**
* 异常处理方法
*
* @param e 抛出的异常
*/
@ExceptionHandler
public ModelAndView exceptionHandler1(Exception e) {
System.out.println("----exceptionHandler1-----");
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("errorMsg", e.getMessage());
modelAndView.setViewName("/eh/error.jsp");
return modelAndView;
}
}
建立一个异常处理视图:
<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head>
<title>ExceptionHandler</title>
</head>
<body>
<h1>出问题啦!</h1>
<br/>
<span style="color: #dc143c; ">${errorMsg}</span>
<br/>
</body>
</html>
我们关闭前面的自定义的异常处理器,启动项目,访问/eh1,得到如下结果:
成功的解决了异常,是不是很简单!
2.2 @Controller和@ControllerAdvice
在@Controller或者@ControllerAdvice类中定义的@ExceptionHandler注解的方法有什么区别呢?
简单的说,@Controller类中的@ExceptionHandler方法仅用于解决当前类中的Controller方法抛出的异常,而@ControllerAdvice类中的@ExceptionHandler方法则可用于全局Controller方法抛出的异常!其中@Controller类中@ExceptionHandler方法优先级更高!
我们新建一个ExceptionHandlerAdvice,标注@ControllerAdvice注解,其内部的exceptionHandler方法用于处理全局异常:
/**
* @author lx
*/
@ControllerAdvice
public class ExceptionHandlerAdvice {
/**
* 异常处理方法
*/
@ExceptionHandler
public ModelAndView exceptionHandler(Exception e) {
System.out.println("----ExceptionHandlerAdvice-----");
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("errorMsg", "ExceptionHandlerAdvice");
modelAndView.setViewName("/eh/error.jsp");
return modelAndView;
}
}
新建另一个Controlller,但是不配置@ExceptionHandler方法:
/**
* @author lx
*/
@Controller
public class ExceptionHandlerController2 {
@RequestMapping("/eh2")
public void eh2() {
throw new RuntimeException("eh2测试");
}![在这里插入图片描述](https://img-blog.csdnimg.cn/20210603103023603.png#pic_center)
}
启动项目,访问/eh1,得到如下结果:
启动项目,访问/eh2,得到如下结果:
我们如果将ExceptionHandlerController中的@ExceptionHandler方法注释掉,启动项目,再次访问/eh1,得到如下结果:
2.3 @ExceptionHandler方法参数
@ExceptionHandler方法中必须有一个Throwable类型及其子类型的异常参数,用于接收抛出的异常,同时还支持以下参数或者注解:
类型 | 说明 |
---|---|
HandlerMethod | 用于访问引发异常的控制器方法。 |
WebRequest, NativeWebRequest | 用于对请求参数、请求(request)和会话(session)属性的通用访问,而无需直接使用 Servlet API。 |
javax.servlet.ServletRequest, javax.servlet.ServletResponse | 请求和响应,可以指定特性的类型,比如HttpServletRequest、MultipartHttpServletRequest |
javax.servlet.http.HttpSession | session,如果使用了该参数,那么永远不会为null。请注意,session访问不是线程安全的。如果允许多个请求同时访问session,请考虑将RequestMappingHandlerAdapter实例的synchronizeOnSession标志设置为 true。 |
java.security.Principal | 当前经过身份验证的用户 – 如果已知,可能是特定的Principal实现类。 |
HttpMethod | 当前请求的 HTTP 方法。 |
java.util.Locale | 当前请求的区域设置,由可用的LocaleResolver区域解析器确定。 |
java.util.TimeZone, java.time.ZoneId | 与当前请求关联的时区,由LocaleContextResolver确定。 |
java.io.OutputStream, java.io.Writer | 原始响应体,通过 Servlet API 获取的。 |
java.util.Map, org.springframework.ui.Model, org.springframework.ui.ModelMap | 进行错误页面的响应需要访问的模型。总是空的,需要自己配置响应数据! |
RedirectAttributes | 指定用于重定向时的属性。一个专门用于重定向之后还能带参数跳转的的工具类。 |
@SessionAttribute | 用于获取已经存储的session域中的属性 |
@RequestAttribute | 用于访问已创建的、预先存在的request域中的属性 |
2.4 @ExceptionHandler方法返回值
@ExceptionHandler 方法支持以下返回值或者注解:
类型 | 说明 |
---|---|
@ResponseBody | 返回值将会通过HttpMessageConverter实例转换为JSON并直接写入响应。 |
HttpEntity, ResponseEntity | 返回值用于指定完整的响应(包括HTTP状态码、头部信息以及响应体),响应体将通过HttpMessageConverter实例转换并写入响应。 |
String | 一个视图名称,将通过ViewResolver来解析,并与模型数据一起使用(可以与@ModelAttribute方法结合,或者通过设置Model参数来配置模型数据)。 |
View | 一个视图实例,与模型数据一起使用(可以与@ModelAttribute方法结合,或者通过设置Model参数来配置模型数据)。 |
java.util.Map, org.springframework.ui.Model | 配置要添加到模型中的数据,视图名称则通过请求RequestToViewNameTranslator隐式的确定。 |
@ModelAttribute | 配置要添加到模型中的数据,视图名称则通过请求RequestToViewNameTranslator隐式的确定。 |
ModelAndView | 要使用的视图和模型属性,以及(可选)响应状态。 |
void | 如果具有void返回类型(或null返回值)的方法也具有ServletResponse、OutputStream参数或@ResponseStatus注解,则认为该方法已经完全处理了响应。如果没有上面的条件,那么指示没有响应正文,或者选择默认的视图名! |
任何其他返回值 | 如果返回值与上述任何条件不匹配,并且不是简单类型(由 BeanUtils#isSimpleProperty 确定),默认情况下,它被视为要添加到模型中的模型属性。如果是简单类型,则表示该异常仍未解决。 |
2.5 REST异常处理
REST风格服务或者前后端分离的项目的一个常见要求是将错误详细信息包括在响应正文中,通过JSON信息返回,而不是由后端直接返回错误视图!
如果采用自定义异常解析器的方式,那么需要我们自己来转换JSON数据以及设置响应头,但是采用@ExceptionHandler的方式就很轻松的实现REST风格的异常处理!
我们只需要在@ExceptionHandler方法上加上@ResponseBody注解然后开启MVC配置支持即可!
开启MVC配置可以使用JavaConfig的形式:
@EnableWebMvc
@Configuration
public class MvcConfig { }
也可以使用XML的形式:
<mvn:annotation-driven/>
同时需要添加jackson的maven依赖,用于JSON数据的转换:
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson
-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
然后我们在ExceptionHandlerAdvice中添加一个方法,该方法用于返回JSON类型的响应:
/**
* 返回json
*/
@ExceptionHandler
@ResponseBody
public ResponseResult<String> exceptionHandler2(RuntimeException e) {
return new ResponseResult<>("系统内部异常", 500, e.getMessage());
}
class ResponseResult<T> {
private String msg;
private int code;
private T data;
public ResponseResult(String msg, int code, T data) {
this.msg = msg;
this.code = code;
this.data = data;
}
public String getMsg() {
return msg;
}
public int getCode() {
return code;
}
public T getData() {
return data;
}
}
访问/eh1,得到如下结果:
访问/eh2,得到如下结果:
成功的进行了JSON的响应,在前后端分离项目的实际开发中,返回JSON数据是最常见的!
如果所有的方法都是REST风格的响应,那么我们可直接在类上使用@RestController以及@RestControllerAdvice
注解来代替@Controller和@ControllerAdvic。这Controller方法的配置都是一样的,因此非常容易上手!
2.6 异常匹配
@ExceptionHandler方法在默认情况下,将会匹配参数异常类型及其子类型,并且具有与实际抛出的异常类型越接近的参数异常类型的方法将会有越高的匹配优先级,但是在同一个类中,匹配的异常类型不应该一致,否则将抛出异常!
如果当前@Controller中,没有@ExceptionHandler方法或者无法匹配抛出的异常,那么会在@ControllerAdvice类中继续匹配,对于多个@ControllerAdvice或@RestControllerAdvice类,它们的整体匹配优先级支持order排序,可实现Ordered、PriorityOrdered接口,或者采用@Order、@Priority注解。比较优先级为PriorityOrdered>Ordered>@Order>@Priority。order值越小优先级越高,如果没有order值,那么将返回Integer.MAX_VALUE,即最低优先级。
如果在当前@ControllerAdvice类中无法匹配任何@ExceptionHandler方法,那么继续到下一个@ControllerAdvice类中匹配。
如下两个@ControllerAdvice类,我们配置ExceptionHandlerAdvice的Order值为1,ExceptionHandlerAdvice1的Order值为0:
/**
* @author lx
*/
@RestControllerAdvice
@Order(1)
public class ExceptionHandlerAdvice {
@ExceptionHandler
public ResponseResult<String> handle1(Exception e) {
return new ResponseResult<>("Exception", 500, e.getMessage());
}
@ExceptionHandler
public ResponseResult<String> handle2(RuntimeException e) {
return new ResponseResult<>("RuntimeException", 500, e.getMessage());
}
}
/**
* @author lx
*/
@RestControllerAdvice
@Order(0)
public class ExceptionHandlerAdvice1 {
@ExceptionHandler
public ResponseResult<String> handle(Exception e) {
return new ResponseResult<>("Exception异常", 500, e.getMessage());
}
@ExceptionHandler
public ResponseResult<String> handle(IllegalStateException e) {
return new ResponseResult<>("IllegalStateException异常", 500, e.getMessage());
}
@ExceptionHandler
public ResponseResult<String> handle(RuntimeException e) {
return new ResponseResult<>("RuntimeException异常", 500, e.getMessage());
}
}
public class ResponseResult<T> {
private String msg;
private int code;
private T data;
public ResponseResult(String msg, int code, T data) {
this.msg = msg;
this.code = code;
this.data = data;
}
public String getMsg() {
return msg;
}
public int getCode() {
return code;
}
public T getData() {
return data;
}
}
一个Controller:
/**
* @author lx
*/
@Controller
public class OrderController {
@RequestMapping(Spring MVC统一异常处理