1.提供一个错误的地址 http://localhost:8087/aaaaaaaaa
1)浏览器访问
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)浏览器访问
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文件
2)继续浏览器访问2个接口
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它拦截处理
这里说明下:为什么浏览器请求返回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/"