本文设计 SpringMVC 异常处理体系源码分析, SpringMVC 异常处理相关类的设计模式, 实际工作中异常处理的实践.
问题场景
假设我们的 SpringMVC 应用中有如下控制器:
代码示例 - 1
- @RestController("/order")
- public class OrderController{
- @RequestMapping("/detail")
- public Object orderDetail(int orderId){
- // ...
- }
- }
这个控制器中接收了一个参数: int 类型的 orderId. 假设我在请求的使传递的参数为 orderId=99999999999 或者 orderId=53844181132132asdf. 很显然, 我们的第一个参数超出了 int 的范围, 第二个参数类型不符合. 这时肯定会报 400 错误, 假设我们的应用是部署在 tomcat 里边的, 我们会得到的错误页面是这样的:
代码示例 - 2
- <html>
- <head><title>Apache Tomcat/7.0.42 - Error report</title>
- <style>
- <!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}A.name {color : black;}HR {color : #525D76;}-->
- </style>
- </head>
- <body>
- <h1>HTTP Status 400 - </h1>
- <HR size="1" noshade="noshade">
- <p><b>type</b> Status report</p>
- <p><b>message</b> <u></u></p>
- <p>
- <b>description</b>
<u>The request sent by the client was syntactically incorrect.</u>
- </p><HR size="1" noshade="noshade">
- <h3>Apache Tomcat/7.0.42</h3>
- </body>
- </html>
当我们碰到这个错的时候, 实际上都没有进入目标方法, 控制台也看不到 controller 方法执行的日志相关信息. 根据经验, 我们知道这是请求错误, 是请求参数不匹配导致的(实际抛出的异常是: org.springframework.beans.TypeMismatchException: Failed to convert value of type). 也许你会说解决这个问题, 只需要传递正确的参数就可以了, 但是 spring 是怎么处理这个错误的, 流程是怎样? 如果了解这些, 对于我们解决问题更有帮助.
源码调试分析
为了追踪处理过程, 我会使用断点调试的方式. 我们知道, SpringMVC 的核心是 DispatchServlet. 所有的请求会被 DispatchServlet 接收, 并在其 doDispatch(...)方法中处理. doDispatch()方法会找到对应的 handler, 然后 invoke. 所以我们在 doDispatch 方法中打个断点. 我们使用 postman 发起一个请求, 并传递一个错误的参数. 先贴一点 doDispatch()方法的代码:
代码示例 - 3
- protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
- // 删除一些代码
- try {
- ModelAndView mv = null;
- Exception dispatchException = null;
- try {
- // 删除一些代码方便阅读
- try {
- // Actually invoke the handler.
- mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
- }
- finally {
- if (asyncManager.isConcurrentHandlingStarted()) {
- return;
- }
- }
- applyDefaultViewName(request, mv);
- mappedHandler.applyPostHandle(processedRequest, response, mv);
- }
- catch (Exception ex) {
- dispatchException = ex; // 这里捕获了异常 TypeMismatchException
- }
- processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
- }
- catch (Exception ex) {
- }
- finally {
- // 删除一些代码
- }
- }
当请求进入 doDispatch()方法之后, 单步执行发现, 发生了一个异常, 然后, 这个异常被 catch 住了, catch 块里边进行了如下操作:
代码示例 - 4
dispatchException = ex;
异常的详细信息是:
代码示例 - 5
org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'; nested exception is java.lang.NumberFormatException: For input string: "53844181132132asdf"
继续执行, 走出了 catch 块之后, 便进入了 processDispatchResult 方法:
代码示例 - 5
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
HandlerExecutionChain mappedHandler, ModelAndView mv, 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);
- }
- }
- // 方便阅读, 删除了其他代码
- }
这个方法中对异常进行判断, 发现不是 "ModelAndViewDefiningException" 就交给 processHandlerException()方法继续处理. processHandlerException 方法代码如下:
代码示例 - 6
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
- Object handler, Exception ex) throws Exception {
- // Check registered HandlerExceptionResolvers...
- ModelAndView exMv = null;
- for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
- exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
- if (exMv != null) {
- break;
- }
- }
- // 去掉了一些代码
- throw ex;
- }
这里的 for 循环是为了找一个 handler 来处理这个异常. 这里的 handler 列表有:
- ExceptionHandlerExceptionResolver
- ResponseStatusExceptionResolver
- DefaultHandlerExceptionResolver
自定义的 ExceptionResolver 1
...
自定义的 ExceptionResolver N
异常体系的设计模式
在上面的代码中, 通过 for 循环需要在众多的 handler 中找一个 HandlerExceptionResolver 的实现类来处理异常. 这里的 handler 列表是在应用初始化的时候就创建了, 前三个是 spring 内部自带的, 后面是我们自定义的(如果有的话). 处理异常的方法是 resolveException(), 它其实是在 HandlerExceptionResolver 接口中定义的, 该接口只有一个方法 resolveException(), 代码如下:
代码示例 - 7
- public interface HandlerExceptionResolver {
- ModelAndView resolveException(
- HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
- }
Spring 自带的 ExceptionHandlerExceptionResolver,ResponseStatusExceptionResolver,DefaultHandlerExceptionResolver 都是继承自 AbstractHandlerExceptionResolver 类, 这个类是一个抽象类, 它实现了 HandlerExceptionResolver 接口, 它对 HandlerExceptionResolver 接口约定的方法的所实现代码是这样的:
代码示例 - 8
- public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
- Object handler, Exception ex) {
- if (shouldApplyTo(request, handler)) {
- logException(ex, request);
- prepareResponse(ex, response);
- return doResolveException(request, response, handler, ex);
- }
- else {
- return null;
- }
- }
这个方法其实是一个模板, 这里使用的是模板方法设计模式. 这个模板定义了处理异常的逻辑, return null 或者进入 if 执行 "三步走", 看上面代码, 这三步分别是:
- logException(ex, request);
- prepareResponse(ex, response);
- doResolveException(request, response, handler, ex);
这里的第三部 doResolveException(request, response, handler, ex)是一个抽象方法, 它也是我们的模板方法. 它的声明是这样的:
代码示例 - 9
protected abstract ModelAndView doResolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex);
这个抽象方法就是留个子类来实现的. 模板我定好了, 子类想咋处理就怎么实现. 无论你咋实现, 反正我这 "三步走" 是已经定好的了. 所以, 模板方法设计模式就是这样:"定义一个操作中的算法的骨架, 而将一些步骤延迟到子类中. TemplateMethod 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤."
继续回到代码逻辑, 刚才讲到, 我们的 for 循环遍历当前的 handler, 并调用当前 handler 的 resolveException 方法. 正如 [代码示例 - 8 ] 所示这个 resolveException 方法是个模板方法, 它的第一步就是一个 if 判断, 这个判断的方法代码如下:
代码示例 - 10
- protected boolean shouldApplyTo(HttpServletRequest request, Object handler) {
- if (handler != null) {
- if (this.mappedHandlers != null && this.mappedHandlers.contains(handler)) {
- return true;
- }
- if (this.mappedHandlerClasses != null) {
- for (Class handlerClass : this.mappedHandlerClasses) {
- if (handlerClass.isInstance(handler)) {
- return true;
- }
- }
- }
- }
- return (this.mappedHandlers == null && this.mappedHandlerClasses == null);
- }
this.mappedHandlers 是一个 Set , 它存储了当前异常处理器有哪些 handler. 如果这个 set 不为空, 并且包含了当前的目标 handler, 那就说明这个异常处理器可以处理当前的目标 handler.(这里所说的 handler 其实就是 controller 的目标方法, 以开篇的例子来说, 这个 handler 类包含的信息, 目标方法, 总之 handler 指明我们要调用的是 OrderController 类的 orderDetail 方法).
于是, 我们的 for 循环, 依次发现了 ExceptionHandlerExceptionResolver 不能处理, ResponseStatusExceptionResolver 也不能处理, 下一个轮到 DefaultHandlerExceptionResolver 的时候, 可以了, 进入了 if 里边的 "三步走". 最终执行了该类对模板方法 doResolveException 的实现代码, 这个代码是这样的:
代码示例 - 11
@Override
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response,
- Object handler, Exception ex) {
- try {
- if (ex instanceof NoSuchRequestHandlingMethodException) {
- return handleNoSuchRequestHandlingMethod(...);
- }
- // 删除部分 else if instanceof 判断
- else if (ex instanceof TypeMismatchException) {
- // 执行到了这里
- return handleTypeMismatch((TypeMismatchException) ex, request, response, handler);
- }
- // 删除部分 else if instanceof 判断
- else if (ex instanceof BindException) {
- return handleBindException((BindException) ex, request, response, handler);
- }
- }
- catch (Exception handlerException) {
- }
- return null;
- }
这个方法, 对异常类型进行判断, 上面提到, 由于我们传递的错误参数导致了 TypeMismatchException 异常, 所以, 根据上面的代码, 我们本次的错误被 handleTypeMismatch()方法处理了. handleTypeMismatch 方法的代码非常的简单, 全部代码如下:
protected ModelAndView handleTypeMismatch(TypeMismatchException ex,
- HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
- response.sendError(HttpServletResponse.SC_BAD_REQUEST);
- return new ModelAndView();
- }
执行到这里, 最终返回了一个 new ModelAndView()对象. 根据 [ 代码示例 - 6 ] 中的代码所示, 程序终于可以跳出这个 for 循环了. 进入下面的 if 语句之后, 由于得到的是一个空的 ModelAndView 对象, 所以执行了 exMv.isEmpty()的代码, return 了 null.
接下来程序便回到了 processDispatchResult 方法, 调用了 mappedHandler.triggerAfterCompletion(request, response, null); 之后, 一切便结束了. 这里的方法调用是责任链设计模式, 本篇不在过多的解释, 意思就是异常处理之后, 继续交给后续的 intercepter 处理. 最终, 我们便看到了开篇所给出的 400 页面.
如何解决参数异常导致的 400 错误
经过上面的分析, 我们已经知道了这个 400 错误是如何发生的. 那么改如何解决呢? 通常情况下, 我们的应用都会有很多 controller 和方法, 这么多的 controller 和方法我们不可能一个个的去处理. 所以, 通常来说, 定义一个全局的处理器会是一个比较好的选择. spring 给了我们很多的选择.(感兴趣的可以看: https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc )
本例中, 我为了处理这个 400 错误, 使用了如下的方式. 新建一个类 GlobalDefaultExceptionHandler, 并保证该类可以被 spring 容器初始化, 其代码如下:
- @ControllerAdvice
- public class GlobalDefaultExceptionHandler {
- @ExceptionHandler(value = TypeMismatchException.class)
- @ResponseBody
- public Object defaultErrorHandler1(HttpServletRequest req, Exception e) throws Exception {
- if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
- throw e;
- }
- ResaultBean res = new ResaultBean("请求的参数中有格式错误");
- return res;
- }
- @ExceptionHandler(value = HttpRequestMethodNotSupportedException.class)
- public Object defaultErrorHandler2(HttpServletRequest req, Exception e) throws Exception {
- if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
- throw e;
- }
- ModelAndView mav = new ModelAndView();
- mav.addObject("exception", e);
- mav.addObject("url", req.getRequestURL());
- mav.setViewName("error");
- return mav;
- }
- }
本例中将 @ ControllerAdvice 和 @ ExceptionHandler 搭配使用, 实现了对 TypeMismatchException 和 HttpRequestMethodNotSupportedException 的处理. 当有这两个异常发生时, 分别会执行这里的逻辑, 并返回我们自定义的结果.
注意, 在 defaultErrorHandler1()方法中, 我们还搭配了 @ ResponseBody 注解, 使用过 springmvc 的同学都知道, 到我们在 controller 的某个方法上注解 @ ResponseBody 的时候, 表示这个方法返回的是 json, 而不是某个视图页面. 同理, 这里的异常处理加上 @ ResponseBody 注解, 表示对这个异常的处理结果返回的也是. 开发 api 的同学需要正是这个配置, 而不是在 "正常情况下返回 json, 错误的情况下 400 页面 html" 那就很糟糕了. 另外 @ ExceptionHandler 搭配 ResponseBody 使用好像是在 spring 3.1 之后才支持的, 之前是只能返回 ModelAndView 和 String ( 也是一个页面配置). 但是这个可以忽略, 因为现在大家用的都是高版本的了.
defaultErrorHandler2()中, 返回的是 ModelAndView. 即, 我们可以也可以指定返回某个页面. 在这个例子中, 我使用了两个 @ExceptionHandler 注解分别处理了两个异常情况 mailto:我使用了两个@ExceptionHandler注解分别处理了两个异常情况 . 你当然可以使用 @ExceptionHandler mailto:你当然可以使用@ExceptionHandler (value = Exception.class)来处理所有的异常了.
@ExceptionHandler 的原理其实就是, 就是将其所注解的处理类, 配置到了 ExceptionHandlerExceptionResolver 类的 exceptionHandlerCache 中, 上面说的 for 循环在挑选处理器的时候, 会找到 ExceptionHandlerExceptionResolver 来处理. 后面就映射到了我们自定义处理类 GlobalDefaultExceptionHandler 中的相应方法. 然后我们看到的结果就是:
- {
- "code": 10001,
- "message": "请求的参数中有格式错误"
- }
至此, 400 错误的发生和解决算是粗略的讲完了. 这里我虽然是调试了代码, 并分析了相关的执行流程, 以及设计模式. 但是还是感觉略知一二. 要想完全弄清楚, 还是需要继续深入的. Spring 真的强大的, 设计的好, 功能全, 代码写的也漂亮. 值得学习啊.
附加一点:
如何处理请求处理过程中发送的异常
本文主要是想通过源码来分析 400 错误发生的过程, 顺带的了解一下 SpringMVC 异常处理方面的设计. 这里补充一点, 如果我们想处理请求过程中发生的异常. 那么我们只需要实现 HandlerExceptionResolver 接口即可. 实现的方法如下:
- public class ApiHandlerExceptionResolver implements HandlerExceptionResolver {
- @Override
- public ModelAndView resolveException(HttpServletRequest request,
- HttpServletResponse response, Object handler, Exception exception) {
- ModelAndView model = new ModelAndView();
- // do something ...
- return model;
- }
- }
通过这个 ApiHandlerExceptionResolver, 当我们的 controller 方法在执行过程中, 抛出了异常 (自己并未 try,catch 捕获的) 比如说空指针异常, 数组越界异常等. 就可以走这里了, 而不是返回一个 tomcat 500 错误页面. 这个配置算是比较常用的, 所以不再解释. 反而是上面所说的 400 处理, 即请求处理之前的错误一些应用中并未配置.
来源: http://www.bubuko.com/infodetail-2656478.html