一、异常处理方式一:@ControllerAdvice+@ExceptionHandler
(1)使用方法
通过@ControllerAdvice+@Exception的方式便可以指定在请求处理的整个流程中如果出现了@ExceptionHandler注解中指定的这些异常,便可以通过该@ExceptionHandler所标注的方法来处理该类异常,该类中的方法同常规的Controller中的方法一致,如果返回String便会找到相对应的页面进行渲染,如果类上标注了@RestControllerAdvice返回类型为String则会返回JSON字符串。
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})
public String handleException() {
return "error";
}
}
(2)原理分析
SpringMVC的核心组件是DispatcherServlet,而DispatcherServlet的核心处理流程全部在doDispatch方法中,而在这个方法的所有核心步骤中只要抛出了异常都会封装到Exception类型的dispatchException这个变量中,并将该对象传入到processDispatchResult方法中。该方法中会判断是否有异常传入,有异常传入则会调用processHandlerException方法来获取视图,processHandlerException会遍历所有实现了HandlerExceptionResolver接口类的resolveException方法看有没有一个类可以处理该异常。
总结一下:doDispatch方法的核心流程中抛出异常→catch住异常存入dispatchException变量→调用processDispatchResult→调用processHandlerException→遍历所有实现了HandlerExceptionResolver接口类看能否处理当前异常→能处理返回视图进行渲染
关键组件:HandlerExceptionResolver接口
上图是在controller中抛出除0异常后遍历handlerExceptionResolvers集合时debug观察到的集合中的元素,其中DefaultErrorAttributes虽然实现了HandlerExceptionResolver接口但并不处理任何异常,只负责把异常对象存入request域中。而后三个resolver是在DispatcherServlet初始化时存入的,这三个resovler写在了DispatcherServlet.properties中。
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
其中的ExceptionHandlerExceptionResolver负责在标注了@ControllerAdvice的类中找标注了@ExceptionHandler并且标注了当前异常类的方法进行处理,处理后返回视图进行渲染。
顺带说一下ResponseStatusExceptionResolver这个resovler个人认为实际生产中用的较少就不分析了,而DefaultHandlerExceptionResolver负责处理Spring自己抛出的异常,比如下面这种异常,如果客户端请求/demo/hello时没有带参数a,则会抛出org.springframework.web.bind.MissingServletRequestParameterException异常,该异常是Spring内部异常所以由DefaultHandlerExceptionResolver进行处理。
@RestController
@RequestMapping("/demo")
public class DemoController {
@RequestMapping("/hello")
public String hello(@RequestParam("a") int a) {
return "hello";
}
}
二、异常处理方式二:BasicErrorController+ErrorViewResolver
(1)使用方法
我们使用这种处理方式处理异常其实只需要在/template/error下或者静态资源目录下( “classpath:/META-INF/resources/”, “classpath:/resources/”, “classpath:/static/”, “classpath:/public/”)定义对应错误码的页面(如404.html)或者4xx.html、5xx.html页面。
(2)原理分析
经过方式一不能处理的异常会通过发送/error请求再次来到DispatcherServlet的核心流程,Springboot为我们注册了BasicErrorController来专门处理/error请求,BasicErrorController内部的处理过程则是通过ErrorViewResolver解析对应的错误码并返回ModelAndView,同样的Springboot也为我们注册了DefaultErrorViewResolver来解析错误码返回ModelAndView。
BasicErrorController处理流程:
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
//略...
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
//略...
}
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
DefaultErrorViewResolver解析错误视图主要的流程是:
- resolveErrorView方法首先得到错误的http状态码,并根据状态码名称调用resolve方法:
- resolve方法去/templates/error下面寻找名称与状态码对应的模板,如404.html,如果有就会将其渲染成视图,返回ModelAndView对象;
- 如果没有,再调用resolveResource去静态资源目录下寻找: “classpath:/META-INF/resources/”, “classpath:/resources/”, “classpath:/static/”, “classpath:/public/”
- 若仍然没有,resolve方法最终返回ModelAndView为null;
- 当ModelAndView为null,再去验证错误代码是否是4xx或5xx,然后再按照上面resolve方法的逻辑去找是否有4xx.html或5xx.html;
- 如果仍然没找到,返回null;
代码如下:
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
//略...
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
//略...
}
如果我们对DefaultErrorViewResolver处理对应错误码的方式不满意,则可以通过自定义ErrorViewResolver接口的实现类加入到容器中来替换DefaultErrorViewResolver的默认行为。如果我们对BasicErrorController的处理方式不满意,比如我们想直接返回Json字符串,则可以通过自定义ErrorController接口的实现类加入到容器中来替换BasicErrorController。
如果通过DefaultErrorViewResovler仍不能处理当前错误码的/error请求,则会返回视图名为error的ModelAndView,该视图error最终会被BeanNameViewResolver这个视图解析器解析得到一个bean名称为error的View对象,调用该对象的渲染方法,最终为我们呈现出来的就是Springboot默认的错误白页。
总结:异常请求处理流程
其实方式一和方式二都属于异常请求处理流程的一部分,方式一无法处理的异常就会由方式二进行处理。异常请求处理的流程主要分成五步:
- 解析错误请求抛出异常
- 尝试处理错误请求(方式一处理)
- 如果处理失败,会给底层response发送错误信息;然后再重新发送一个/error请求
- /error请求被ErrorController处理(方式二处理)
- 若ErrorController中的ErrorViewResolver仍不能处理返回视图,则new一个视图名为error的ModelAndView对象,最终该视图会由Springboot自动装配的BeanNameViewResolver解析得到一个bean名称为error的View对象,该View对象进行渲染后即得到Springboot的默认错误白页
附:Springboot自动装配的异常处理组件
上述的许多组件都不是我们自己装入容器的,这些组件是由Springboot为我们自动装入的。Springboot在其自动配置包spring-boot-autoconfigure中的ErrorMvcAutoConfiguration类中主要为我们自动装配了包括DefaultErrorAttributes、BasicErrorController、DefaultErrorViewResolver、View(bean名称为error)、BeanNameViewResolver、
这里声明下,本文的异常处理流程是在Springboot搭建的SSM项目环境下,如果是原生的Web.xml搭建的SSM项目则没有Springboot装配的这些组件,如果需要这些组件提供的功能需要自动手动进行导入到容器中。
参考博客
常请求处理流程 发送/error请求以及得到error视图的详细原理_bejsoiv的博客-CSDN博客