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/"