1.提供一个错误的地址  http://localhost:8087/aaaaaaaaa

1)浏览器访问

springboot错误码 springboot错误处理机制_springboot错误码

 

 

 2)postman调用

{
    "timestamp": "2021-07-14T02:41:21.571+00:00",
    "status": 404,
    "error": "Not Found",
    "message": "",
    "path": "/aaaaaaaaa"
}

2.提供一个异常的接口

@GetMapping("/compare")
  public String compare(HttpServletRequest request){
    int i=10/0;
    return "index";
  }

1)浏览器访问

springboot错误码 springboot错误处理机制_springboot错误码_02

 

 

 2) postman调用

{
    "timestamp": "2021-07-14T02:42:54.914+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "message": "",
    "path": "/compare"
}

3.自己提供错误页面

1)在resource下面的static目录下创建目录error,里面放置3个html文件

springboot错误码 springboot错误处理机制_springboot错误码_03

 

 

 2)继续浏览器访问2个接口

springboot错误码 springboot错误处理机制_html_04

 

 

 

springboot错误码 springboot错误处理机制_springboot错误码_05

 

 

 3)解释下5xx.html:如果没有500.html的话,系统就使用5xx.html

 

4.源码解读

1)DispatchServlet

org.springframework.web.servlet.DispatcherServlet#doDispatch

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

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

        try {
          processedRequest = this.checkMultipart(request);
          multipartRequestParsed = processedRequest != request;
//查询哪个处理器(controller)能处理我们的请求
          mappedHandler = this.getHandler(processedRequest);
          if (mappedHandler == null) {
            this.noHandlerFound(processedRequest, response);
            return;
          }
          //参数处理适配器
          HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
          String method = request.getMethod();
          boolean isGet = "GET".equals(method);
          if (isGet || "HEAD".equals(method)) {
            long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
            if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
              return;
            }
          }
          //拦截器的前置方法
          if (!mappedHandler.applyPreHandle(processedRequest, response)) {
            return;
          }
          //真正调用目标方法
          mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
          if (asyncManager.isConcurrentHandlingStarted()) {
            return;
          }

          this.applyDefaultViewName(processedRequest, mv);
          mappedHandler.applyPostHandle(processedRequest, response, mv);
        } catch (Exception var20) {
//目标方法执行成异常后,并没有退出,而是捕获异常继续执行
          dispatchException = var20;
        } catch (Throwable var21) {
          dispatchException = new NestedServletException("Handler dispatch failed", var21);
        }
        //最终结果处理器
        this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
      } catch (Exception var22) {
//拦截器的complete方法
        this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
      } catch (Throwable var23) {
//拦截器的complete方法
        this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
      }

    } finally {
      if (asyncManager.isConcurrentHandlingStarted()) {
        if (mappedHandler != null) {
          mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
        }
      } else if (multipartRequestParsed) {
        this.cleanupMultipart(processedRequest);
      }

    }
  }

我们看最终处理器

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) {
        this.logger.debug("ModelAndViewDefiningException encountered", exception);
        mv = ((ModelAndViewDefiningException)exception).getModelAndView();
      } else {
        Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
//自定义异常处理
        mv = this.processHandlerException(request, response, handler, exception);
        errorView = mv != null;
      }
    }

    if (mv != null && !mv.wasCleared()) {
      this.render(mv, request, response);
      if (errorView) {
        WebUtils.clearErrorRequestAttributes(request);
      }
    } else if (this.logger.isTraceEnabled()) {
      this.logger.trace("No view rendering, null ModelAndView returned.");
    }

    if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
      if (mappedHandler != null) {
        mappedHandler.triggerAfterCompletion(request, response, (Exception)null);
      }

    }
  }

我们debug发现我们没有自定义异常处理器,整个流程下来返回空modelAndView,这时候底层发起一个/error请求,这个请求会被BasicErrorController它拦截处理

springboot错误码 springboot错误处理机制_ide_06

 

 这里说明下:为什么浏览器请求返回html页面,而postman请求返回json.原因是contentType导致,浏览器使用默认的,而postman使用application/json.

所有上面2个方法,第一个给html用的,第二个给postman请求返回的。

 

我们继续分析html的

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);
    }

1)先说下默认错误视图在哪

org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration.StaticView

这个是是我们自动装配进来的,包括BasicErrorController也是因为自动装配才生效的。

private static class StaticView implements View {
    private static final MediaType TEXT_HTML_UTF8;
    private static final Log logger;

    private StaticView() {
    }

    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
      if (response.isCommitted()) {
        String message = this.getMessage(model);
        logger.error(message);
      } else {
        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(this.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(this.htmlEscape(model.get("error"))).append(", status=").append(this.htmlEscape(model.get("status"))).append(").</div>");
        if (message != null) {
          builder.append("<div>").append(this.htmlEscape(message)).append("</div>");
        }

        if (trace != null) {
          builder.append("<div style='white-space:pre-wrap;'>").append(this.htmlEscape(trace)).append("</div>");
        }

        builder.append("</body></html>");
        response.getWriter().append(builder.toString());
      }
    }

我们的默认html标签都是在这里组装好的。

2)继续分析原来的获取错误视图org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController#resolveErrorView

protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
            Map<String, Object> model) {
//错误视图解析器,系统默认只装配了一个,ErrorMvcAutoConfiguration在它里面装配的
        for (ErrorViewResolver resolver : this.errorViewResolvers) {
            ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
//如果解析到了用解析到的,如果解析不到返回null,最终使用使用默认error视图
            if (modelAndView != null) {
                return modelAndView;
            }
        }
        return null;
    }

3)查看视图解析器

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;
    }

这里有个默认视图

private static final Map<Series, String> SERIES_VIEWS;

    static {
        Map<Series, String> views = new EnumMap<>(Series.class);
        views.put(Series.CLIENT_ERROR, "4xx");
        views.put(Series.SERVER_ERROR, "5xx");
        SERIES_VIEWS = Collections.unmodifiableMap(views);
    }

4)根据状态码解析视图

private ModelAndView resolve(String viewName, Map<String, Object> model) {
        String errorViewName = "error/" + viewName;
        TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
                this.applicationContext);
        if (provider != null) {
            return new ModelAndView(errorViewName, model);
        }
        //这里根据/error/404和model参数去获取视图
        return resolveResource(errorViewName, model);
    }

5)获取视图

private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
        //这里定义了视图的放置位置
        for (String location : this.resources.getStaticLocations()) {
            try {
                Resource resource = this.applicationContext.getResource(location);
                //看看位置下有没有比如error/404.html这样的问题
                resource = resource.createRelative(viewName + ".html");
                //有这样的文件直接根据html返回视图
                if (resource.exists()) {
                    return new ModelAndView(new HtmlResourceView(resource), model);
                }
            }
            catch (Exception ex) {
            }
        }
        return null;
    }

这里的位置有

0 = "classpath:/META-INF/resources/"
1 = "classpath:/resources/"
2 = "classpath:/static/"
3 = "classpath:/public/"