在上一篇文章关于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 " +