关于springboot的异常处理以及源码分析(二)
在上一篇文章关于springboot的异常处理以及源码分析(一)我们通过官方文档起手,看到了springboot对于异常的处理大纲。以及我们分析了一些细节,这里我们将再次回顾官方文档,把其余的内容进行剖析,以及其源码逻辑都会做一个梳理。
一、自定义异常
我们上一篇文章通过自己去配置4xx 5xx的页面来实现了自定义异常页面的跳转。但是我们看到文档中还提供了一些其他的自定义异常处理的逻辑,我们这里就来一一解析。
1、@ControllerAdvice注解+@ExceptionHandler注解
1.1、案例演示
在官方文档中有这么一段描述。
翻译过来就是您还可以定义一个带注释的类来@ControllerAdvice定制 JSON 文档以返回特定的控制器和/或异常类型。如果YourException在与 相同的包中定义的控制器抛出,则使用 POJOAcmeController的 JSON 表示形式。
换句话就是说,如果我们用@ControllerAdvice注解标注了一个类,并且指定了扫描包的范围(不指定就是全部的),那么这个路径下的所有异常都会抛到这里,并且根据你@ExceptionHandler标注的异常类型进行匹配,能匹配上的,就会来到这里来处理异常,返回异常视图。进而我们就可以自己定义异常视图了。那么我们来测试一下。
- 1、我们先定义一个我们自己异常,以后我们业务中都抛出这个异常然后统一处理
public class MyException extends RuntimeException {
// 省略构造
}
- 2、然后我们再定义一个异常页面,controllerAdviceHtml.html我们待会让他统一跳到我们这个页面。这个页面异常简单,就一个一级标题。放在静态资源目录下面就行。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSE Chat</title>
</head>
<body>
<h1>AcmeControllerAdviceHtml</h1>
</body>
</html>
- 3、接着我们就来定义一个全局异常处理器。
// 异常处理我们自己的controller,实际你不配就是整个包
@ControllerAdvice(basePackageClasses = MyController.class)
public class AcmeControllerAdvice extends ResponseEntityExceptionHandler {
// 我们这里处理的异常就是我们自己定义的异常MyException,实际开发,这里可以配置多个,然后根据异常类型来处理,你自己灵活处理就行
@ExceptionHandler(MyException.class)
String handleControllerException(HttpServletRequest request, Throwable ex) {
// 返回我们的视图名称,如果正常,他就会跳转去这个页面,模型就是视图,直接返回字符串
return "controllerAdviceHtml";
}
}
- 4、最后我们再来定义一个controller,然后接口抛出我们自己的异常
看看能不能被这个全局异常处理器处理到。
@RestController
@RequestMapping("/test")
public class MyController {
@GetMapping("testControllerAdvice")
public String testControllerAdvice() {
// 测试 ControllerAdvice,抛出我们自己的异常
throw new MyException("testControllerAdvice error");
}
}
好了,我们来在页面发起这个get请求。
于是我们验证得到了这个玩意他是好使的,那么为什么好使呢,我们就再来看一下源码。
1.2、源码分析,ExceptionHandlerExceptionResolver登场
在分析之前,我先来梳理一下我们上一篇文章的源码逻辑。后面的几种异常我们就不梳理了,但是最后我会给出完整的流程图。
# 异常源码梳理
1、org.springframework.web.servlet.DispatcherServlet#doDispatch
首先我们在DispatcherServlet的doDispatch方法中找到了真正执行我们目标方法(也就是你的接口)的地方。
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
2、在目标方法抛出异常,之后他捕获了异常
// 把异常保存在了dispatchException 中
dispatchException = new NestedServletException("Handler dispatch failed", err);
3、然后在processDispatchResult开始处理异常
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
4、最后来到org.springframework.web.servlet.DispatcherServlet#processHandlerException
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
在这里他遍历所有的异常处理器,然后逐个处理该异常,一旦有一个处理器能处理,就直接跳出了。OK
我们先就过到这里。
我们说他是遍历异常处理器的然后挨个处理的,那么实际上我们上一篇文章在debug的时候看到了这些异常处理器如下图,我们看到其实一共是四个处理器,其中第一个是存放一些页面能返回啥的,并不决定是哪个处理器。
但是我们在上文debug的时候发现,下面三个啥也没干,就直接过去了。最后走了个白页。算了我不卖关子了,直接来说,下面三个处理器就是处理你自定义异常处理的逻辑。我们这里就来debug看一下。我们直接把断点打在这里,然后发起请求。
然后我们来到这里的时候发现依然是那四个异常处理器,其中上面那个依然啥也没干,下面三个是一组。我们来到下面三个的处理。我们debug进来来到这里org.springframework.web.servlet.handler.HandlerExceptionResolverComposite#resolveException()
下面就来遍历这三个处理器,看看能不能处理,能就直接返回视图跳出了。
其中第一个处理器就是ExceptionHandlerExceptionResolver,然后就进入了他的resolveException方法,进行异常的处理。
public ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
// 这里是判断,这个异常处理器,能不能处理这次请求的异常,这里判断可以处理
if (shouldApplyTo(request, handler)) {
prepareResponse(ex, response);
/**
来到这里处理这个异常,处理逻辑很长,但是主要逻辑就是通过你这个异常
去判断哪个处理器可以处理,通过注解标记的异常类型,发现是不是能处理
如果能处理,底层会通过反射调用我们那个方法
smoketest.simple.config.AcmeControllerAdvice#handleControllerException
然后返回一个视图,如图1.2.1
*/
ModelAndView result = doResolveException(request, response, handler, ex);
if (result != null) {
// Print debug message when warn logger is not enabled.
if (logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
logger.debug("Resolved [" + ex + "]" + (result.isEmpty() ? "" : " to " + result));
}
// Explicitly configured warn logger in logException method.
logException(ex, request);
}
return result;
}
else {
return null;
}
}
于是我们这里就返回了这个视图,跳出我们的循环回到org.springframework.web.servlet.DispatcherServlet#processDispatchResult,然后往下开始渲染我们的这个视图
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
// 这里经过遍历解析器,返回了视图
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
// 渲染视图,就是把我们那个页面渲染出来,然后下面就直接返回了,这样就
// 找到了这个页面,并且渲染出来返回了。后面就tomcat会把这个页面通过流返回客户端
render(mv, request, response);
}
// ...... 省略没用的
}
2、@ResponseStatus注解,自定义异常返回
2.1、案例演示
- 定义一个异常
// 通过在异常上标注注解,来定义异常的状态码和返回信息
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "Status Exception")
public class StatusException extends RuntimeException{
// 省略构造
}
- 定义接口,抛出这个异常信息
@RestController
@RequestMapping("/test")
public class MyController {
@GetMapping("testResponseStatus")
public String testResponseStatus() {
// 测试 StatusException,抛出我们自己的异常
throw new StatusException();
}
}
我们看到返回的异常页面为:
2.2、源码分析,ResponseStatusExceptionResolver的作用
下面我们进行源码分析,其实经过上面你也知道了,我们有一个组合的异常解析器的集合,其中有一个是ResponseStatusExceptionResolver,其实看名字也知道,这个解析器就是给我们这个注解的异常准备的。
我们直接进去源码看解析逻辑,我debug都不想打了。
@Override
@Nullable
protected ModelAndView doResolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
try {
// 判断你的异常是不是ResponseStatusException,所以其实我们没必要继承RuntimeException
// 继承了ResponseStatusException也是可以被处理的。
if (ex instanceof ResponseStatusException) {
return resolveResponseStatusException((ResponseStatusException) ex, request, response, handler);
}
// 找到打@ResponseStatus注解的类,其实就是我们那个异常类
ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
if (status != null) {
return resolveResponseStatus(status, request, response, handler, ex);
}
// 如果存在异常,那就开始解析,所以逻辑落到了解析这里
if (ex.getCause() instanceof Exception) {
return doResolveException(request, response, handler, (Exception) ex.getCause());
}
}
// 省略没用的......
return null;
}
我们接着来看解析逻辑
@Override
@Nullable
protected ModelAndView doResolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
try {
if (ex instanceof ResponseStatusException) {
return resolveResponseStatusException((ResponseStatusException) ex, request, response, handler);
}
ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
// 如果你注解打了状态码,我们是BAD_REQUEST,他就来这里处理,因为这是非必填的,
// 所以他要兼容
// 那我们就看这里
if (status != null) {
return resolveResponseStatus(status, request, response, handler, ex);
}
// 如果你没打状态码,那就来这里
if (ex.getCause() instanceof Exception) {
return doResolveException(request, response, handler, (Exception) ex.getCause());
}
}
// 省略没用的......
return null;
}
我们再来看一下打了状态码的异常处理逻辑。
protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request,
HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception {
// 取出状态码BAD_REQUEST
int statusCode = responseStatus.code().value();
// 取出异常信息Status Exception
String reason = responseStatus.reason();
// 来这里进行视图的处理
return applyStatusAndReason(statusCode, reason, response);
}
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver#applyStatusAndReason
protected ModelAndView applyStatusAndReason(int statusCode, @Nullable String reason, HttpServletResponse response)
throws IOException {
// 因为我们是标注了异常信息的,所以这里不会走,也是一个兜底,其实和你标了一样,只是
// 你不标,他给你生成了一个
if (!StringUtils.hasLength(reason)) {
response.sendError(statusCode);
}
else {
// 取出异常信息
String resolvedReason = (this.messageSource != null ?
this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) :
reason);
/**
这里是重点,在上面构建好了异常的相关内容之后,code和reason
这里调用HttpServletResponse 发了一个Error的方法。下面我们单独解释
*/
response.sendError(statusCode, resolvedReason);
}
return new ModelAndView();
}
我们看到上面最后构造好了异常相关的信息,然后触发了response.sendError(statusCode, resolvedReason);我们不妨来看看这行代码是在哪里的,
org.apache.catalina.connector.ResponseFacade#sendError(int, java.lang.String)我们看到这个方法是tomcat的,不是mvc的。所以其实这里他会直接发一个异常信息,然后就走到了我们上一篇文章说的org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml这个方法开始渲染视图,注意我们第一篇说的白页在没有异常处理器能处理的时候,会调用到/error其实也是这个逻辑。这是tomcat发送的,也就是说,在调用到这里之后其实mvc已经结束了,他后面的逻辑还会走,会触发spring的一些逻辑,对数据做一些处理,但是因为我们前端请求是和tomcat交互的,此时tomcat在发送了error之后就已经和前端返回了异常页面了通过http请求。
至于doDispatch之后的那些逻辑都已经没用了,后面就被tomcat接管了,tomcat会调用org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml然后返回渲染的视图,tomcat就会和前端交互了,你渲染的有5xx的就走你的,没有就走空白页,这些我们都在第一篇说过了。mvc后面的还会执行,但是不起作用了,因为tomcat已经和前端交互了,你这就是正常的代码运行了没业务能力。而这个error错误tomcat用/error能处理就返回,不能处理,tomcat就会给你返回那个最原始的蓝白色的那个有个猫的异常页面。
至于tomcat和mvc之间的关系,我后面会单独写文章处理。
所以你知道,我们这个注解就是他被ResponseStatusExceptionResolver解析器处理,在最后被tomcat发了/error请求,,然后tomcat会开始一个新的接口请求,其实就是/error,然后再次走org.springframework.web.servlet.DispatcherServlet#doDispatch
到达org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml,然后渲染视图,返回前端。
3、内部异常
这种异常不是我们自己的异常,他是框架的异常,比如你请求了一个不存在的参数,本来人家参数叫a,你请求了一个c。或者是参数类型不对,本来人家是字符串,你请求了个数字。或者本来人家是对象,你请求了数字,这种都是内部异常,他的处理就是在我们的第三个解析器里面。
DefaultHandlerExceptionResolver。
3.1、案例演示
就不演示了,非常简单。
3.2、源码分析,DefaultHandlerExceptionResolver的实力
先来看下他内部处理的这个解析器的注释。你能看到他会处理这些异常,然后都会给你返回去,所以以后你遇到了这种异常,可以来这个解析器直接定位。
我们来这里看他的异常解析逻辑。写的那是非常直白,就是N个if else判断
protected ModelAndView doResolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
// 可以看到各种异常的分支都有判断,我们随便挑一个
try {
if (ex instanceof HttpRequestMethodNotSupportedException) {
return handleHttpRequestMethodNotSupported(
(HttpRequestMethodNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof HttpMediaTypeNotSupportedException) {
return handleHttpMediaTypeNotSupported(
(HttpMediaTypeNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof HttpMediaTypeNotAcceptableException) {
return handleHttpMediaTypeNotAcceptable(
(HttpMediaTypeNotAcceptableException) ex, request, response, handler);
}
else if (ex instanceof MissingPathVariableException) {
return handleMissingPathVariable(
(MissingPathVariableException) ex, request, response, handler);
}
else if (ex instanceof MissingServletRequestParameterException) {
return handleMissingServletRequestParameter(
(MissingServletRequestParameterException) ex, request, response, handler);
}
else if (ex instanceof ServletRequestBindingException) {
return handleServletRequestBindingException(
(ServletRequestBindingException) ex, request, response, handler);
}
else if (ex instanceof ConversionNotSupportedException) {
return handleConversionNotSupported(
(ConversionNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof TypeMismatchException) {
return handleTypeMismatch(
(TypeMismatchException) ex, request, response, handler);
}
else if (ex instanceof HttpMessageNotReadableException) {
return handleHttpMessageNotReadable(
(HttpMessageNotReadableException) ex, request, response, handler);
}
else if (ex instanceof HttpMessageNotWritableException) {
return handleHttpMessageNotWritable(
(HttpMessageNotWritableException) ex, request, response, handler);
}
else if (ex instanceof MethodArgumentNotValidException) {
return handleMethodArgumentNotValidException(
(MethodArgumentNotValidException) ex, request, response, handler);
}
else if (ex instanceof MissingServletRequestPartException) {
return handleMissingServletRequestPartException(
(MissingServletRequestPartException) ex, request, response, handler);
}
else if (ex instanceof BindException) {
return handleBindException((BindException) ex, request, response, handler);
}
else if (ex instanceof NoHandlerFoundException) {
return handleNoHandlerFoundException(
(NoHandlerFoundException) ex, request, response, handler);
}
else if (ex instanceof AsyncRequestTimeoutException) {
return handleAsyncRequestTimeoutException(
(AsyncRequestTimeoutException) ex, request, response, handler);
}
}
// 省略没用的
}
我们以第一个case为例看他的解析逻辑:handleHttpRequestMethodNotSupported
protected ModelAndView handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex,
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
String[] supportedMethods = ex.getSupportedMethods();
if (supportedMethods != null) {
response.setHeader("Allow", StringUtils.arrayToDelimitedString(supportedMethods, ", "));
}
// 还是发送/error走tomcat交给BasicErrorController来处理,后面就都一样了。
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, ex.getMessage());
return new ModelAndView();
}
4、自定义异常处理器
4.1、案例演示
你去看看上面那几个异常处理器,他们都有一个特点就是实现了HandlerExceptionResolver接口,其实我们也可以自己写一个,把他放到容器里面就完了。他也能被读到那个集合里面,遍历处理就行。
- 定义解析器
// 放入容器
@Component
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
try {
/**
* 这里直接发tomcat,和前端交互,其实后面的就都没用了
*/
response.sendError(540,"levi define error message");
} catch (IOException e) {
throw new RuntimeException(e);
}
// 返回个空,这里也没啥用,后面都是没业务能力的了
return new ModelAndView();
}
}
- 定义一个接口:
@RequestMapping("/test")
public class MyController {
@GetMapping("testControllerAdvice")
public String testControllerAdvice() {
throw new MyException("testControllerAdvice error");
}
醉了,没有我们的540异常码,还是404,所以这里有问题。下面我们来看源码。
4.2、源码分析
其实我们也能猜到,我们的自己的那个处理器加进去之后排在后面,前面的处理器一旦符合逻辑能处理,就直接返回视图了。轮不到我们的。
代码位于org.springframework.web.servlet.DispatcherServlet#processHandlerException
所以我们要改变一下顺序,我们说异常处理器,你怎么弄他也还是一个spring的bean,我们通过spring的能力改变一下他的加载顺序不就行了吗。其实不就是@order注解吗。
// 优先级最高,数字越小,优先级越高
@Order(Integer.MIN_VALUE)
// 放入容器
@Component
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
try {
/**
* 这里直接发tomcat,和前端交互,其实后面的就都没用了
*/
response.sendError(540,"levi define error message");
} catch (IOException e) {
throw new RuntimeException(e);
}
// 返回个空,这里也没啥用,后面都是没业务能力的了
return new ModelAndView();
}
}
再次来测试。
我们发现我们的解析器排第一了,而且因为我们的解析器没有什么判定逻辑,根本不判断是不是能解析这类异常,所以我们这个啥异常都能进来处理。这其实不对哈,实际开发可能你会覆盖别人的,所以最好是做一下判断,因为源码就是不能解析就跳过,走下一个判断。因为我这里就是演示,所以不区分了。
我们看到已经是我们的540码了,这就没问题了。
5、定义真正的底层 ErrorViewResolver
我们两篇文章下来看到几个点我在这里说一下。
1、当没有一个解析器能处理的时候,就会调用tomcat的response.sendError()方法。
2、当我们在异常解析器中被处理之后,会封装异常信息,然后调用response.sendError()方法。
在response.sendError()调用之后tomcat会发起一个请求,就是/error,然后被BasicErrorController接受请求,比如你是html页面发起的请求就会被转发到,org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml来处理,里面会拿到你封装的异常信息,然后通过ErrorViewResolver的接口实现类DefaultErrorViewResolver,来解析这个异常,看你的异常码是什么比如你是500,那就给你读取静态资源下面的error目录下面的5xx或者500.html(优先读取500.html,要是没有就读取系列码对应的,也就是5xx.html),这样就能转发过去了。要是你error下面都没有,那就给你默认那个白页,他自己内部拼的。
所以ErrorViewResolver 他是决定你到底转去哪个视图的。如果你有什么毛病,不想再error下面读取,那就可以自定义,然后改变这个路径,不过一般好像没人这么干。这里同样存在优先级,你可以指定优先级去覆盖。
// 优先级最高,数字越小,优先级越高
@Order(Integer.MIN_VALUE)
// 放入容器
@Component
public class MyErrorViewResolver implements ErrorViewResolver {
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) throws FileNotFoundException {
return null;// 实现你可以抄一下DefaultErrorViewResolver
}
}