Spring Boot 404 没那么简单
概述
当我们在浏览器中输入了一个不存在的地址,spring boot 会给我们返回一个著名的 “white label page”,其内容如下:
<html>
<body>
<h1>Whitelabel Error Page</h1>
<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>
<div id='created'>Thu Jan 12 11:22:14 CST 2023</div>
<div>There was an unexpected error (type=Not Found, status=404).</div>
</body>
</html>
但是如果我们借助于 postman 发送一个请求到这个不存在的地址,则返回的内容变成了 json:
{
"timestamp": "2023-01-12T03:27:36.007+00:00",
"status": 404,
"error": "Not Found",
"path": "/hellox"
}
当我们在浏览器中请求了一个不存在的地址时,spring boot 内部究竟是怎么处理的呢?事情并没有那么简单,本文将跟大家一起探索下处理过程。
注:本文以 spring boot + tomcat 为例说明,不适用于其他应用服务器处理流程。// ...
表示此处省略一万行代码。
404 处理流程
当访问一个不存在的路由时,假设这不存在的路由是 /hellox
,spring boot 处理的大致过程如下:
- ①tomcat 接收到请求,并将请求转发给 spring boot 的 DispatcherServlet#doDispatch;
- ②doDispatch 通过
getHandler(request)
寻找能够处理该请求的 handler,由于没有任何 controller 能够处理/hellox
,但是 AbstractUrlHandlerMapping 中注册了/**
对应的处理器为 ResourceHttpRequestHandler,这个处理器其实是处理静态资源的,因此这一步大致可认为是找不到 controller 中的处理方法,就将请求处理器设置为静态资源处理器; - ③利用 ResourceHttpRequestHandler#handleRequest 处理请求,然而并没有找到名为 hellox 的静态资源,到这里响应的状态码被设置为 404,同时响应的 errorState 被标记为1,标记响应失败;
- ④由于上一步响应已被标记为失败,请求流转到 StandardHostValve#status 方法,该方法根据状态码查找状态码对应的错误页面,找到了错误页面为地址为
/error
; - ⑤请求被转发到
/error
,这之后请求又重新到达 DispatcherServlet#doDispatch 方法,然后到达 doDispatch,这次不同于上次,这次找到了/error
对应的处理器为BasicErrorController#error
或者BasicErrorController#errorHtml
; - ⑥我们假设请求在浏览器中打开,此时会包含请求头
Accept: text/html
,因此/error
请求会被分发到BasicErrorController#errorHtml
,此时的响应就是大名鼎鼎的 “white label page”;但是如果我们请求是通过 postman 发送过去的(假设没有设置 Accept),此时请求就会被分发到BasicErrorController#error
,返回 json 响应。
处理过程中的涉及的代码片段
下面让我们看下整个处理过程中涉及到的代码片段,首先是 tomcat 接收请求,最终到达 StandardHostValve#invoke 方法:
StandardHostValve#invoke 方法:
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// ...
if (!response.isErrorReportRequired()) {
// 最终会调用到 DispatcherServlet#doDispatch,对应上面的步骤①
context.getPipeline().getFirst().invoke(request, response);
}
// ...
// dispatch 处理完成后,看响应是否报错
if (response.isErrorReportRequired()) {
// If an error has occurred that prevents further I/O, don't waste time
// producing an error report that will never be read
AtomicBoolean result = new AtomicBoolean(false);
response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);
if (result.get()) {
if (t != null) {
throwable(request, response, t);
} else {
// 根据状态码找 error page,对应上面的步骤④
status(request, response);
}
}
}
}
上面步骤①,请求最终会被转发到 DispatcherServlet#doDispatch,以下是相关的代码片段:
DispatcherServlet#doDispatch
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// ...
// 对应上面的步骤②,找到的 handler 为 ResourceHttpRequestHandler
mappedHandler = getHandler(processedRequest);
// ...
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// ...
// Actually invoke the handler.
// 对应上面的步骤③,经过 ha.handle 之后,响应被标记为失败,同时响应状态码被标记为 404
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
}
StandardHostValve#status
经过 DispatcherServlet#doDispatch 之后,来到步骤④的 StandardHostValve#status
private void status(Request request, Response response) {
// ...
// 根据响应状态码寻找 error page,找到 page 为 /error
ErrorPage errorPage = context.findErrorPage(statusCode);
if (errorPage == null) {
// Look for a default error page
errorPage = context.findErrorPage(0);
}
// ...
if (errorPage != null && response.isErrorReportRequired()) {
// ...
// 在custom 方法中,请求被转发到 `/error`,对应上面的步骤⑤
if (custom(request, response, errorPage)) {
//...
}
}
}
StandardHostValve#custom
private boolean custom(Request request, Response response,
ErrorPage errorPage) {
RequestDispatcher rd =
servletContext.getRequestDispatcher(errorPage.getLocation());
if (response.isCommitted()) {
// ...
} else {
// Reset the response (keeping the real error code and message)
response.resetBuffer(true);
response.setContentLength(-1);
// 请求被转发到 `/error`,对应上面的步骤⑤
rd.forward(request.getRequest(), response.getResponse());
// If we forward, the response is suspended again
response.setSuspended(false);
}
}
BasicErrorController
巧了,spring boot 内置了 BasicErrorController,这个 controller 正好可以处理 /error
,于是请求被 error 或者 errorHtml处理,对应上面的步骤⑥
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
// ...
}
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
// ...
}
}
未捕获异常处理
上面说到了 404 错误处理,如果 controller 中抛出了异常,又会如何呢,我们这里只讨论 spring boot 的默认处理,也就是不讨论通过 @ExceptionHandler
或者全局异常处理主动处理异常的情况。
这次我们假设 /hellox 定义如下:
@RestController
class HelloController {
@GetMapping("hellox")
public void hello() {
throw new RuntimeException("hellox");
}
}
当我们在浏览器中打开 /hellox
页面时,仍然会得到著名的 “white label page”:
<html>
<body>
<h1>Whitelabel Error Page</h1>
<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>
<div id='created'>Thu Jan 12 15:59:29 CST 2023</div>
<div>There was an unexpected error (type=Internal Server Error, status=500).</div>
</body>
</html>
当我们在浏览器中打开 /hellox
页面时,spring boot 处理的大致过程如下:
- ①tomcat 接收到请求,并将请求转发给 spring boot 的 DispatcherServlet#doDispatch;
- ②doDispatch 通过
getHandler(request)
找到了 HelloController#hello 方法可以处理/hellox
请求,于是调用该方法处理请求,但是请求抛出了 RuntimeException; - ③调用 DispatcherServlet#processHandlerException 方法处理异常,该方法内部通过 DefaultHandlerExceptionResolver#doResolveException 处理异常,此方法只能处理常见的 ServletException 异常,由于这里抛出的是 RuntimeException,该方法并不能处理,只是简单的返回 null;
- ④经过上面步骤实际上异常并未被处理,异常被 processHandlerException 方法再次抛出,在 FrameworkServlet 中被捕获并包装为 NestedServletException 后再次抛出,随后在 StandardWrapperValve#invoke 中被捕获并通过
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, exception)
记录异常,同时将请求的响应码设置为 500,并且将响应标记为失败; - ⑤由于上一步响应已被标记为失败,且请求属性中记录了异常,因此知道请求是由于异常导致的响应失败,于是请求流转到 StandardHostValve#throwable 方法,该方法根据异常查找 error page,但是并未找到 error page,于是根据状态码查找 error page,之后请求被转发到
/error
,这之后请求又重新到达 DispatcherServlet#doDispatch 方法,然后到达 doDispatch,这次不同于上次,这次找到了/error
对应的处理器为BasicErrorController#error
或者BasicErrorController#errorHtml
; - ⑥我们假设请求在浏览器中打开,此时会包含请求头
Accept: text/html
,因此/error
请求会被分发到BasicErrorController#errorHtml
,此时的响应就是大名鼎鼎的 “white label page”;但是如果我们请求是通过 postman 发送过去的(假设没有设置 Accept),此时请求就会被分发到BasicErrorController#error
,返回 json 响应。
注:DefaultHandlerExceptionResolver#doResolveException 方法处理常见的 ServletException 异常类型,例如 HttpRequestMethodNotSupportedException、HttpRequestMethodNotSupportedException 和 HttpMediaTypeException 等。
以上就是 spring boot 默认的异常处理过程,代码就不贴了!希望对大家有帮助。
温馨提示:反馈需要登录