SpringMVC原理(9)-带你深入了解SpringMVC的异常处理原理(HandlerExceptionResolver)

206 阅读16分钟

抛出问题

1、 我们在浏览器发起一个请求,如果出现错误,那么默认的错误页会是这样的

image.png

2、 当我们使用postman、axios等工具发送请求时,如果出现错误,那么得到的响应结果却是一个json数据

image.png

解决问题

那么它们是怎么处理的呢,这就要用到我们的异常处理解析器了:HandlerExceptionResolver

当我们执行目标方法的过程中出现异常的时候,mvc会根据不同的客户端返回不同的错误信息。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {          
            processedRequest = checkMultipart(request);

            // 省略代码
            ...
                
            // 执行目标方法
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
            
            // 省略代码
            ...
        }
        catch (Exception ex) {
            // 捕获异常赋值给dispatchException
            dispatchException = ex;
        }
        
        // 省略代码
        ...
            
        // 这里会处理异常,如果处理不了,就继续往上抛
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
        // 拦截器的后置处理,继续把异常往上抛【这时会抛给Tomcat】
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
    catch (Throwable err) {
        // 拦截器的后置处理,继续把异常往上抛【这时会抛给Tomcat】
        triggerAfterCompletion(processedRequest, response, mappedHandler,
                               new NestedServletException("Handler processing failed", err));
    }

    // 省略代码
    ...
}

processDispatchResult()

     private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
                                        @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
                                        @Nullable Exception exception) throws Exception {
     ​
         boolean errorView = false;
     ​
         // 1、处理异常
         if (exception != null) {
             // 1.1、如果是ModelAndViewDefiningException
             if (exception instanceof ModelAndViewDefiningException) {
                 logger.debug("ModelAndViewDefiningException encountered", exception);
                 mv = ((ModelAndViewDefiningException) exception).getModelAndView();
             }
             else {
                 Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
                 // 1.2、遍历异常处理解析器来处理异常,无法处理就继续往上抛
                 mv = processHandlerException(request, response, handler, exception);
                 // ModelAndView有值,此属性才是true!
                 errorView = (mv != null);
             }
         }
     ​
         // 2、渲染页面【这是一个动态策略】
         if (mv != null && !mv.wasCleared()) {
             render(mv, request, response);
             if (errorView) {
                 // 清除请求域中设置过的异常信息的数据
                 WebUtils.clearErrorRequestAttributes(request);
             }
         }
     ​
         ...
     ​
         // 3、拦截器的最终处理
         if (mappedHandler != null) {
             // Exception (if any) is already handled..
             mappedHandler.triggerAfterCompletion(request, response, null);
         }
     }
  1. 处理异常,无法处理就继续往上抛
  2. 渲染页面【这是一个动态策略
    1. 如果方法正常执行完成后有ModelAndView那么就渲染
    2. 如果方法执行出异常那么就会进入异常的处理逻辑,这时会得到一个ModelAndView
      1. 异常处理器执行成功也会得到一个ModelAndView,然后会进行渲染
      2. 执行失败那就没有ModelAndView,也就是说不需要进行渲染
  3. 拦截器的最终处理

异常信息:

image.png

processHandlerException()

我们这里抛出的异常是ArithmeticException,所以会来到 1.2 的逻辑:

     protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
                                                    @Nullable Object handler, Exception ex) throws Exception {
     ​
         // Success and error responses may use different content types
         request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
     ​
         // Check registered HandlerExceptionResolvers...
         ModelAndView exMv = null;
         // 1、遍历所有的异常处理解析器来处理异常
         if (this.handlerExceptionResolvers != null) {
             for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
                 // 处理异常,生成 ModelAndView 对象
                 exMv = resolver.resolveException(request, response, handler, ex);
                 if (exMv != null) {
                     break;
                 }
             }
         }
         
         // 情况1:如果ModelAndView有值,那么就设置一个视图名字,然后把此对象返回
         if (exMv != null) {
             if (exMv.isEmpty()) {
                 request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
                 return null;
             }
             // 设置一个默认的视图名字
             if (!exMv.hasView()) {
                 String defaultViewName = getDefaultViewName(request);
                 if (defaultViewName != null) {
                     exMv.setViewName(defaultViewName);
                 }
             }
             if (logger.isTraceEnabled()) {
                 logger.trace("Using resolved error view: " + exMv, ex);
             }
             else if (logger.isDebugEnabled()) {
                 logger.debug("Using resolved error view: " + exMv);
             }
             WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
             return exMv;
         }
     ​
         // 情况2:如果异常未处理成功,那么就继续往外抛
         throw ex;
     }
  1. 遍历所有的异常解析器调用其resolveException()来解析异常,并得到一个ModelAndView,没拿到会一直遍历,直到拿到或者没有异常解析器了

  2. 情况1:如果ModelAndView有值,那么就设置一个视图名字,然后把此对象返回

  3. 情况2:如果异常未处理成功(ModelAndView为空),那么就继续把异常往外抛

异常处理解析器

image.png

我们在 doDispatch()发现了它的异常处理策略:先把异常交给异常解析器来处理,能处理最好,处理不了就继续往上抛,然后doDispatch()继续捕获后来执行拦截器的最终处理,然后继续往上抛【这时会抛给Tomcat

我们来看一下这几个异常处理解析器分别都干了什么事情:

DefaultErrorAttributes

它只把异常信息存储到request域中,其它啥也不干

     // org.springframework.boot.web.servlet.error.DefaultErrorAttributes.ERROR
     private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR";
     ​
     public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
                                          Exception ex) {
         // 存储异常信息到request域中
         storeErrorAttributes(request, ex);
         // 然后返回null
         return null;
     }
     ​
     private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
         // ERROR_ATTRIBUTE = org.springframework.boot.web.servlet.error.DefaultErrorAttributes.ERROR
         request.setAttribute(ERROR_ATTRIBUTE, ex);
     }

HandlerExceptionResolverComposite

它是一个异常处理解析器的组合类,它里面有3个异常处理解析器,所以它又遍历所有的异常解析器来处理异常

     public ModelAndView resolveException(
         HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
     ​
         if (this.resolvers != null) {
             // 遍历所有的异常处理解析器来解析异常得到一个ModelAndView对象
             for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
                 ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
                 if (mav != null) {
                     return mav;
                 }
             }
         }
         
         // 如果都处理不了,就返回一个null
         return null;
     }

它们都继承于AbstractHandlerExceptionResolver类,所以会来到父类的逻辑:

     public ModelAndView resolveException(
         HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
     ​
         if (shouldApplyTo(request, handler)) {
             prepareResponse(ex, response);
             // 解析异常得到ModelAndView对象
             ModelAndView result = doResolveException(request, response, handler, ex);
             
             // 省略代码
             ...
                 
             // 返回ModelAndView对象
             return result;
         }
         else {
             return null;
         }
     }

doResolveException():解析异常得到ModelAndView对象。每个子类都有不同的实现逻辑

ExceptionHandlerExceptionResolver

它用来处理@ExceptionHandler + @ControllerAdvice注解标注的方法

     protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
                                                            HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
     ​
         // 拿到@ExceptionHandler标注的方法
         ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
         // 我们这里没有,所以直接返回null了
         if (exceptionHandlerMethod == null) {
             return null;
         }
     ​
         if (this.argumentResolvers != null) {
             exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
         }
         if (this.returnValueHandlers != null) {
             exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
         }
     ​
         ServletWebRequest webRequest = new ServletWebRequest(request, response);
         ModelAndViewContainer mavContainer = new ModelAndViewContainer();
     ​
         try {
             if (logger.isDebugEnabled()) {
                 logger.debug("Using @ExceptionHandler " + exceptionHandlerMethod);
             }
             Throwable cause = exception.getCause();
             if (cause != null) {
                 // Expose cause as provided argument as well
                 exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod);
             }
             else {
                 // Otherwise, just the given exception as-is
                 exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
             }
         }
         catch (Throwable invocationEx) {
             // Any other than the original exception (or its cause) is unintended here,
             // probably an accident (e.g. failed assertion or the like).
             if (invocationEx != exception && invocationEx != exception.getCause() && logger.isWarnEnabled()) {
                 logger.warn("Failure in @ExceptionHandler " + exceptionHandlerMethod, invocationEx);
             }
             // Continue with default processing of the original exception...
             return null;
         }
     ​
         if (mavContainer.isRequestHandled()) {
             return new ModelAndView();
         }
         else {
             ModelMap model = mavContainer.getModel();
             HttpStatus status = mavContainer.getStatus();
             ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status);
             mav.setViewName(mavContainer.getViewName());
             if (!mavContainer.isViewReference()) {
                 mav.setView((View) mavContainer.getView());
             }
             if (model instanceof RedirectAttributes) {
                 Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
                 RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
             }
             return mav;
         }
     }

ResponseStatusExceptionResolver

用来处理@ResponseStatus注解标注的异常

     protected ModelAndView doResolveException(
           HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
     ​
        try {
            // 如果异常是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());
           }
        }
        catch (Exception resolveEx) {
           if (logger.isWarnEnabled()) {
              logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", resolveEx);
           }
        }
        return null;
     }

DefaultHandlerExceptionResolver

用来处理mvc框架内部的异常

     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);
           }
        }
        catch (Exception handlerEx) {
           if (logger.isWarnEnabled()) {
              logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx);
           }
        }
        return null;
     }

我们在 doDispatch()发现了它的异常处理策略:先把异常交给异常解析器来处理,能处理最好,处理不了就继续往上抛,然后doDispatch()继续捕获后来执行拦截器的最终处理,然后继续往上抛【这时会抛给Tomcat

到这里我们分析了每个异常处理器可以处理的异常,但是我们发现谁也处理不了我们的这个异常,所以异常到底是怎么被处理的?

此时当我们放行这个请求之后,我们会发现一个神奇的事情:又来了一个新的请求 /error路径的请求

image.png


注意:这个请求的路径是/error的!

我们底层是有一个专门来处理这个/error路径的controllerBasicErrorController

这时候就会由它来处理这个请求

image.png

BasicErrorController

我们可以看到,它也是一个controller,所以它也能来处理请求!

     @Controller
     @RequestMapping("${server.error.path:${error.path:/error}}") //默认的请求路径就是 /error
     public class BasicErrorController extends AbstractErrorController {
     }

处理浏览器发送的请求(白页)

BasicErrorController里的errorHtml()

     @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
     public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
         // 1、获取http状态码
         HttpStatus status = getStatus(request);
         // 2、获取到请求域中的异常信息 DefaultErrorAttributes
         Map<String, Object> model = Collections
             .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
         response.setStatus(status.value());
         // 3、利用 ErrorViewResolver 得到一个 ModelAndView 对象
         ModelAndView modelAndView = resolveErrorView(request, response, status, model);
         // 4、如果没得到,就new一个名为 error 的 ModelAndView 对象
         return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
     }
     
// 获取要哪些异常属性的值  
protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) {
   ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
   if (this.errorProperties.isIncludeException()) {
      options = options.including(Include.EXCEPTION);
   }
   if (isIncludeStackTrace(request, mediaType)) {
      options = options.including(Include.STACK_TRACE);
   }
   if (isIncludeMessage(request, mediaType)) {
      options = options.including(Include.MESSAGE);
   }
   if (isIncludeBindingErrors(request, mediaType)) {
      options = options.including(Include.BINDING_ERRORS);
   }
   return options;
}

流程:

  1. 获取http状态码

  2. 获取到之前由DefaultErrorAttributes设置到请求域中的异常信息,我们可以通过设置ErrorProperties中属性的值来决定要获取哪些数据

    1. getErrorAttributeOptions():决定要获取哪些异常信息的值
  3. 利用ErrorViewResolver得到一个ModelAndView对象

  4. 如果没得到,就new一个 ModelAndView返回

1、获取http状态码

就是通过原生的request从请求域中拿,没拿到默认返回500

     public static final String ERROR_STATUS_CODE = "javax.servlet.error.status_code";
     ​
     protected HttpStatus getStatus(HttpServletRequest request) {
         // 从请求域中拿
         Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
         
         // 没拿到默认返回500
         if (statusCode == null) {
             return HttpStatus.INTERNAL_SERVER_ERROR;
         }
         try {
             return HttpStatus.valueOf(statusCode);
         }
         catch (Exception ex) {
             return HttpStatus.INTERNAL_SERVER_ERROR;
         }
     }

2、获取请求域中的错误信息

获取到之前由DefaultErrorAttributes设置到请求域中的异常信息

     protected Map<String, Object> getErrorAttributes(HttpServletRequest request, ErrorAttributeOptions options) {
         WebRequest webRequest = new ServletWebRequest(request);
         // 获取错误信息
         return this.errorAttributes.getErrorAttributes(webRequest, options);
     }
     ​
     public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
         Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
         // 【A】移除或放入一些信息
         if (Boolean.TRUE.equals(this.includeException)) {
             options = options.including(Include.EXCEPTION);
         }
         if (!options.isIncluded(Include.EXCEPTION)) {
             errorAttributes.remove("exception");
         }
         if (!options.isIncluded(Include.STACK_TRACE)) {
             errorAttributes.remove("trace");
         }
         if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
             errorAttributes.put("message", "");
         }
         if (!options.isIncluded(Include.BINDING_ERRORS)) {
             errorAttributes.remove("errors");
         }
         return errorAttributes;
     }
     ​
     // 【B】
     public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
         Map<String, Object> errorAttributes = new LinkedHashMap<>();
         // 时间
         errorAttributes.put("timestamp", new Date());
         // 状态码
         addStatus(errorAttributes, webRequest);
         // 错误详情
         addErrorDetails(errorAttributes, webRequest, includeStackTrace);
         // 路径
         addPath(errorAttributes, webRequest);
         return errorAttributes;
     }

可以通过配置ErrorProperties类来决定要获取哪些异常信息的值

完整的:【B】

image.png

经过移除后的:【A】

image.png

3、解析错误页面

遍历所有的ErrorViewResolver来解析错误页面得到ModelAndView对象

     protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
                                             Map<String, Object> model) {
         for (ErrorViewResolver resolver : this.errorViewResolvers) {
             // 利用 ErrorViewResolve r来得到 ModelAndView 对象
             ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
             if (modelAndView != null) {
                 return modelAndView;
             }
         }
         return null;
     }
     ​
     // 这里的逻辑就是看我们静态资源目录下是否有对应状态码开头的错误页面
     // 如:4xx.html、5xx.html、400.html、500.html、...
     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;
     }

这里的逻辑就是看我们静态资源目录下是否有对应状态码的错误页面,我们是没有的,所以得到的ModelAndView对象还是一个null

默认的 DefaultErrorViewResolver 是把响应状态码作为错误页的地址/error/statusCode.html

:4xx.html、5xx.html、400.html、500.html、...

错误页面解析器

image.png

4、创建一个名为error的ModelAndView

就是创建一个ModelAndView对象

     public ModelAndView(String viewName, @Nullable Map<String, ?> model) {
         this.view = viewName;
         if (model != null) {
             getModelMap().addAllAttributes(model);
         }
     }
     ​
     public ModelMap getModelMap() {
         if (this.model == null) {
             this.model = new ModelMap();
         }
         return this.model;
     }
     ​
     public ModelMap addAllAttributes(@Nullable Map<String, ?> attributes) {
         if (attributes != null) {
             putAll(attributes);
         }
         return this;
     }

刚创建的ModelAndView对象:

image.png

最终的ModelAndView对象(也就是返回值)

image.png

到这一步我们终于得到了ModelAndView对象。

5、视图解析器渲染页面

BasicErrorController也是一个controller,我们的/error请求就是被它处理的。所以我们得到的ModelAndView对象也会被视图解析器渲染。

它也是一个controller,也要经过处理器映射器、处理器适配器、返回值处理器、视图解析器、...这一系列流程

还记得我们上面processDispatchResult()的逻辑吗,第2步就是渲染页面:

     protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
         // 1、国际化
         Locale locale =
             (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
         response.setLocale(locale);
     ​
         View view;
         // 2、得到视图名,我们的视图名就是 error
         String viewName = mv.getViewName();
         // 【情况1】:有视图名字,利用视图解析器来解析视图名字得到视图对象
         if (viewName != null) {
             
             view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
             if (view == null) {
                 throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
                                            "' in servlet with name '" + getServletName() + "'");
             }
         }
         else {
             // 【情况2】:如果没有视图名字,意味着可以直接得到视图对象!
             view = mv.getView();
             if (view == null) {
                 throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
                                            "View object in servlet with name '" + getServletName() + "'");
             }
         }
     ​
         // Delegate to the View object for rendering.
         if (logger.isTraceEnabled()) {
             logger.trace("Rendering view [" + view + "] ");
         }
         try {
             // ModelAndView中status的值不为null,就设置状态码
             if (mv.getStatus() != null) {
                 response.setStatus(mv.getStatus().value());
             }
             // 3、渲染页面
             view.render(mv.getModelInternal(), request, response);
         }
         catch (Exception ex) {
             if (logger.isDebugEnabled()) {
                 logger.debug("Error rendering view [" + view + "]", ex);
             }
             throw ex;
         }
     }

流程:

  1. 国际化相关

  2. mv.getViewName():得到视图名字 error

    1. 情况1:有视图名字,就利用视图解析器来解析视图名字得到视图对象:mv = resolveViewName()
    2. 情况2:如果没有视图名字,意味着ModelAndView中有视图对象,可以直接得到视图对象
  3. view.render():渲染页面

1、国际化

利用国际化解析器来解析地域信息

Locale locale =
    (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);
2、利用视图解析器得到视图对象
     protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
           Locale locale, HttpServletRequest request) throws Exception {
     ​
        if (this.viewResolvers != null) {
           // 遍历所有的视图解析器来解析
           for (ViewResolver viewResolver : this.viewResolvers) {
              View view = viewResolver.resolveViewName(viewName, locale);
              if (view != null) {
                 return view;
              }
           }
        }
        return null;
     }

默认有4个,而ContentNegotiatingViewResolver又是一个组合类

image.png

来到ContentNegotiatingViewResolver

     public View resolveViewName(String viewName, Locale locale) throws Exception {
         RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
         Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
         // 1、获取客户端可以接受的媒体类型 Accept属性
         List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
         if (requestedMediaTypes != null) {
             // 2、获取候选的视图对象
             List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
             // 3、得到最佳匹配
             View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
             if (bestView != null) {
                 return bestView;
             }
         }
     ​
         String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ?
             " given " + requestedMediaTypes.toString() : "";
     ​
         if (this.useNotAcceptableStatusCode) {
             if (logger.isDebugEnabled()) {
                 logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
             }
             return NOT_ACCEPTABLE_VIEW;
         }
         else {
             logger.debug("View remains unresolved" + mediaTypeInfo);
             return null;
         }
     }
     ​
     ​
     // 2、获取候选的视图对象
     private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
         throws Exception {
     ​
         List<View> candidateViews = new ArrayList<>();
         if (this.viewResolvers != null) {
             Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
             // 遍历视图解析器来根据视图名得到视图对象
             for (ViewResolver viewResolver : this.viewResolvers) {
                 View view = viewResolver.resolveViewName(viewName, locale);
                 if (view != null) {
                     candidateViews.add(view);
                 }
                 for (MediaType requestedMediaType : requestedMediaTypes) {
                     List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
                     for (String extension : extensions) {
                         String viewNameWithExtension = viewName + '.' + extension;
                         view = viewResolver.resolveViewName(viewNameWithExtension, locale);
                         if (view != null) {
                             candidateViews.add(view);
                         }
                     }
                 }
             }
         }
         if (!CollectionUtils.isEmpty(this.defaultViews)) {
             candidateViews.addAll(this.defaultViews);
         }
         return candidateViews;
     }

我们这里最终会用到的是这个视图解析器:BeanNameViewResolver。根据视图名字和类型(View.class)去容器找对应的bean

     public View resolveViewName(String viewName, Locale locale) throws BeansException {
         ApplicationContext context = obtainApplicationContext();
         // 容器中没有名为error的bean
         if (!context.containsBean(viewName)) {
             // Allow for ViewResolver chaining...
             return null;
         }
         if (!context.isTypeMatch(viewName, View.class)) {
             if (logger.isDebugEnabled()) {
                 logger.debug("Found bean named '" + viewName + "' but it does not implement View");
             }
             // Since we're looking into the general ApplicationContext here,
             // let's accept this as a non-match and allow for chaining as well...
             return null;
         }
         // 从容器中拿View对象
         return context.getBean(viewName, View.class);
     }

最终得到到视图对象是一个内部类:StaticView

image.png

3、渲染页面

StaticView类我们可以看到它的渲染方法,就是拼接字符串得到一个html页面

     private static class StaticView implements View {
     ​
         private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);
     ​
         private static final Log logger = LogFactory.getLog(StaticView.class);
     ​
         // 渲染页面
         @Override
         public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
             throws Exception {
             if (response.isCommitted()) {
                 String message = getMessage(model);
                 logger.error(message);
                 return;
             }
             response.setContentType(TEXT_HTML_UTF8.toString());
             StringBuilder builder = new StringBuilder();
             Object timestamp = model.get("timestamp");
             Object message = model.get("message");
             Object trace = model.get("trace");
             if (response.getContentType() == null) {
                 response.setContentType(getContentType());
             }
             builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
                 "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
                 .append("<div id='created'>").append(timestamp).append("</div>")
                 .append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
                 .append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
             if (message != null) {
                 builder.append("<div>").append(htmlEscape(message)).append("</div>");
             }
             if (trace != null) {
                 builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
             }
             builder.append("</body></html>");
             response.getWriter().append(builder.toString());
         }
     ​
         private String htmlEscape(Object input) {
             return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;
         }
     ​
         private String getMessage(Map<String, ?> model) {
             Object path = model.get("path");
             String message = "Cannot render error page for request [" + path + "]";
             if (model.get("message") != null) {
                 message += " and exception [" + model.get("message") + "]";
             }
             message += " as the response has already been committed.";
             message += " As a result, the response may have the wrong status code.";
             return message;
         }
     ​
         @Override
         public String getContentType() {
             return "text/html";
         }
     ​
     }

到这一步我们的页面也就渲染完成了。这就是浏览器上是怎么显示的这个白页的源码!

再来回顾一下我们之前抛出的问题:

我们在浏览器发起一个请求,如果出现错误,那么默认的错误页会是这样的

image.png

json格式

image.png

和之前一样的套路,最终会来到处理错误请求的BasicErrorControllererror()

     @RequestMapping
     public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
         // 1、获取http状态码
         HttpStatus status = getStatus(request);
         if (status == HttpStatus.NO_CONTENT) {
             return new ResponseEntity<>(status);
         }
         // 2、得到响应体数据:获取请求域中的错误信息
         Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
         // 3、构造一个 ResponseEntity 对象返回
         return new ResponseEntity<>(body, status);
     }

1、获取http状态码

就是通过原生的request从请求域中拿,没拿到默认返回500

2、获取请求域中的错误信息

得到响应体数据,获取到之前由DefaultErrorAttributes设置到请求域中的异常信息

前面这两步和前面白页的逻辑一样,不再分析

3、构造一个ResponseEntity对象返回

ResponseEntity也就是返回值

image.png

到这一步我们的错误信息就拿到了,最后再由返回值处理器来处理一下就完成了

4、返回值处理器处理返回值

返回值处理的源码可以看本系列之前的文章:SpringMVC原理(7)-返回值的处理

再来回顾一下我们之前抛出的问题:

当我们使用postman、axios等工具发送请求时,如果出现错误,那么得到的响应结果却是一个json数据

image.png

看到这里的话,那么我想大家现在对于Spring MVC的异常处理机制应该有一个认识了。

总结

简洁版流程

  1. 遍历所有的HandlerExceptionResolver调用其resolveException()来解析异常,并得到一个View,没拿到View会一直遍历,直到拿到View或没有异常解析器了

  2. 默认哪个异常处理解析器都处理不了我们的异常,此时如果放行这个请求会发现又来一个新的请求/error

  3. 我们底层专门有一个BasicErrorController来处理/error路径的请求

  4. 需要html格式的数据的会由errorHtml()这个方法来处理:浏览器默认显示的白页

  5. 需要json格式的数据的会由error()这个方法来处理:json格式数据

自动配置原理:ErrorMvcAutoConfiguration

StaticView组件是如何注册到容器中的?

     @Configuration(proxyBeanMethods = false)
     @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
     @Conditional(ErrorTemplateMissingCondition.class)
     protected static class WhitelabelErrorViewConfiguration {
     ​
         // 白页
         private final StaticView defaultErrorView = new StaticView();
     ​
         // 注册 StaticView 组件到容器中
         @Bean(name = "error")
         @ConditionalOnMissingBean(name = "error")
         public View defaultErrorView() {
             return this.defaultErrorView;
         }
     ​
         // BeanNameViewResolver 视图解析器
         @Bean
         @ConditionalOnMissingBean
         public BeanNameViewResolver beanNameViewResolver() {
             BeanNameViewResolver resolver = new BeanNameViewResolver();
             resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
             return resolver;
         }
     ​
     }

DefaultErrorAttributes、BasicErrorController、DefaultErrorViewResolver组件是如何注册到容器中的?

     @Configuration(proxyBeanMethods = false)
     @ConditionalOnWebApplication(type = Type.SERVLET)
     @ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
     // Load before the main WebMvcAutoConfiguration so that the error View is available
     @AutoConfigureBefore(WebMvcAutoConfiguration.class)
     @EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class })
     public class ErrorMvcAutoConfiguration {
     ​
         private final ServerProperties serverProperties;
     ​
         public ErrorMvcAutoConfiguration(ServerProperties serverProperties) {
             this.serverProperties = serverProperties;
         }
     ​
         // 注册DefaultErrorAttributes
         @Bean
         @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
         public DefaultErrorAttributes errorAttributes() {
             return new DefaultErrorAttributes();
         }
     ​
         // 注册BasicController
         @Bean
         @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
         public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
                                                          ObjectProvider<ErrorViewResolver> errorViewResolvers) {
             return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
                                             errorViewResolvers.orderedStream().collect(Collectors.toList()));
         }
     ​
         @Configuration(proxyBeanMethods = false)
         static class DefaultErrorViewResolverConfiguration {
     ​
             private final ApplicationContext applicationContext;
     ​
             private final ResourceProperties resourceProperties;
     ​
             DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
                                                   ResourceProperties resourceProperties) {
                 this.applicationContext = applicationContext;
                 this.resourceProperties = resourceProperties;
             }
     ​
             // 默认的错误视图解析器
             @Bean
             @ConditionalOnBean(DispatcherServlet.class)
             @ConditionalOnMissingBean(ErrorViewResolver.class)
             DefaultErrorViewResolver conventionErrorViewResolver() {
                 return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
             }
     ​
         }
     ​
     }

HandlerExceptionResolver

用来解析异常然后生成ModelAndView对象

public interface HandlerExceptionResolver {
   // request、response、目标方法、出现的异常
   @Nullable
   ModelAndView resolveException(
         HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);

}

类图:

image.png

  • DefaultErrorAttributes只把异常信息存储到request域中,其它啥也不干

  • ExceptionHandlerExceptionResolver:用来处理@ExceptionHandler + @ControllerAdvice注解标注的方法

  • ResponseStatusExceptionResolver:用来处理@ResponseStatus注解标注的异常

  • DefaultHandlerExceptionResolver:用来处理mvc框架内部的异常

  • HandlerExceptionResolverComposite:它是一个异常处理解析器的组合类,它里面有3个异常处理解析器,所以它又遍历所有的异常解析器来处理异常