先说结论
默认情况下在Spring Boot框架中访问不存在的接口时会触发对"/error"路径的访问,这是由Spring Boot框架的默认错误处理机制导致的,核心是ErrorMvcAutoConfiguration自动配置类在起作用。
追根溯源
如下以Spring Boot 2.6.13版本源码进行解读,更多Spring Boot版本详见:spring-boot/docs/及spring-boot#support。
访问接口不存在时是否会执行到Interceptor
我们知道,客户端发起的HTTP请求处理顺序为:Servlet容器 -> Filter -> Servlet -> Interceptor -> Controller(参考:Spring拦截器HandlerInterceptor与Filter方法执行顺序探究),因此,当访问一个不存在的接口时,并不会真正到达Controller层。但是是否会执行到Interceptor,跟工程的配置参数有关。具体到实现来说,是根据DispatcherServlet.getHandler()方法返回值决定的。
// org.springframework.web.servlet.DispatcherServlet
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {// 省略代码...// Determine handler for the current request.mappedHandler = getHandler(processedRequest);if (mappedHandler == null) {// 当getHandler方法返回值为空时,会执行noHandlerFound方法,返回HTTP状态码为404noHandlerFound(processedRequest, response);return;}// 省略代码...if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}// 省略代码...
}/*** No handler found → set appropriate HTTP response status.* @param request current HTTP request* @param response current HTTP response* @throws Exception if preparing the response failed*/
protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {if (pageNotFoundLogger.isWarnEnabled()) {pageNotFoundLogger.warn("No mapping for " + request.getMethod() + " " + getRequestUri(request));}if (this.throwExceptionIfNoHandlerFound) {throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),new ServletServerHttpRequest(request).getHeaders());}else {// 响应给客户端的HTTP状态码为404response.sendError(HttpServletResponse.SC_NOT_FOUND);}
}
实际上,DispatcherServlet.getHandler()方法返回值跟Spring Boot工程的配置参数有关系:
1.默认情况下,参数spring.mvc.static-path-pattern值为/**,此时DispatcherServlet.getHandler()方法返回值为org.springframework.web.servlet.HandlerExecutionChain实例对象(其中的handler成员变量为org.springframework.web.servlet.resource.ResourceHttpRequestHandler实例对象,interceptorList成员变量即为Interceptor列表),不为null,就会继续执行HandlerExecutionChain.applyPreHandle()方法,这样客户端请求就会执行到Interceptor层,如下:
// org.springframework.web.servlet.HandlerExecutionChain
/*** Apply preHandle methods of registered interceptors.* @return {@code true} if the execution chain should proceed with the* next interceptor or the handler itself. Else, DispatcherServlet assumes* that this interceptor has already dealt with the response itself.*/
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {for (int i = 0; i < this.interceptorList.size(); i++) {HandlerInterceptor interceptor = this.interceptorList.get(i);// 执行各个Interceptor拦截器实例中的preHandle()方法if (!interceptor.preHandle(request, response, this.handler)) {triggerAfterCompletion(request, response, null);return false;}this.interceptorIndex = i;}return true;
}
2.如果在项目工程中明确指定为参数spring.mvc.static-path-pattern指定了其他值,比如:/static/**,此时如果请求一个不存在的接口(如:/api/hello),由于请求接口路径跟参数spring.mvc.static-path-pattern值不匹配,所以DispatcherServlet.getHandler()返回值为null,就无法执行到Interceptor层了。
剖析接口不存在时完整请求流程
如下通过源码解读的方式梳理当客户端发起一个不存在的接口请求时,是如何触发/error路径访问的。
我们已经知道,客户端请求首先到达的是Servlet容器,所以如下源码解析均基于Tomcat 9.0.68阐述(更多Tomcat版本详见:Apache Archive Distribution Directory)。
具体来说是以org.apache.catalina.core.StandardHostValve作为Tomcat容器的执行入口,完整的请求执行路径为:
-> org.apache.catalina.core.StandardHostValve.invoke()
-> org.apache.catalina.authenticator.AuthenticatorBase.invoke()
-> org.apache.catalina.core.StandardContextValve.invoke()
-> org.apache.catalina.core.StandardWrapperValve.invoke()
-> Filter列表
-> javax.servlet.http.HttpServlet.service()
-> org.springframework.web.servlet.FrameworkServlet.doGet()
-> org.springframework.web.servlet.FrameworkServlet.processRequest()
-> org.springframework.web.servlet.DispatcherServlet.doService()
-> org.springframework.web.servlet.DispatcherServlet.doDispatch()
1.在org.springframework.web.servlet.DispatcherServlet.doDispatch()方法中有2地方会检查到路径是否存在,其一:getHandler()方法返回值为null表示请求路径不存在,其二:在org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter.handle()方法中会真正调用org.springframework.web.servlet.resource.ResourceHttpRequestHandler.handleRequest()方法,进而通过org.springframework.web.servlet.resource.ResourceHttpRequestHandler.getResource()方法判断请求路径是否真正存在。
2.如果请求路径不存在,就会设置响应状态码为404,具体是调用org.apache.catalina.connector.Response.sendError()方法设置HTTP响应状态码,响应消息以及其他状态值,供后续在org.apache.catalina.core.StandardHostValve.invoke()方法中使用
回到org.apache.catalina.core.StandardHostValve.invoke()方法中继续执行。
// org.apache.catalina.core.StandardHostValve.invoke()// 省略其他代码...// Look for (and render if found) an application level error page
if (response.isErrorReportRequired()) { // 这个判断条件就是在org.springframework.web.servlet.DispatcherServlet.doDispatch()方法中设置的// If an error has occurred that prevents further I/O, don't waste time// producing an error report that will never be readAtomicBoolean result = new AtomicBoolean(false);response.getCoyoteResponse().action(ActionCode.IS_IO_ALLOWED, result);if (result.get()) {if (t != null) {throwable(request, response, t);} else {status(request, response); // 最终会执行到这里}}
}// 省略其他代码...
在org.apache.catalina.core.StandardHostValve.status()方法中会根据响应状态码查找对应的错误页面,最终调用org.apache.catalina.core.ApplicationDispatcher.forward()方法将请求跳转到错误页面路径(默认为/error)。
// org.apache.catalina.core.StandardHostValve.status()// 省略其他代码...// 根据在org.springframework.web.servlet.DispatcherServlet.doDispatch()方法中设置的错误响应码查找对于的错误页面对象
ErrorPage errorPage = context.findErrorPage(statusCode);
if (errorPage == null) {// Look for a default error pageerrorPage = context.findErrorPage(0);
}// 省略其他代码...// 调用custom()方法请求错误页面路径
if (custom(request, response, errorPage)) {response.setErrorReported();try {response.finishResponse();} catch (ClientAbortException e) {// Ignore} catch (IOException e) {container.getLogger().warn("Exception Processing " + errorPage, e);}
}
// org.apache.catalina.core.StandardHostValve.custom()// 省略其他代码...RequestDispatcher rd = servletContext.getRequestDispatcher(errorPage.getLocation());// 省略其他代码...rd.forward(request.getRequest(), response.getResponse());// 省略其他代码...
回顾上述执行流程可知,当客户端请求一个不存在的接口时会触发对/error路径的访问,这个过程是由Spring Boot框架跟Servlet容器一起配合实现的。
实际上通过对Spring Boot框架源码进行跟踪调试后发现,不仅仅是访问不存在的接口时会触发对/error路径的访问,当在Filter.doFilter()方法中抛出异常时,也会触发对/error路径的访问。
另外也确认了一个事实:可以为不同的HTTP错误响应状态码设置对应的页面。
那么,为什么在Spring Boot框架中触发访问的路径是/error,而不是其他路径呢?
为什么请求到/error路径
这要从Spring Boot框架的自动配置类org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration说起,本质上是通过它设定了默认的错误访问路径。
// org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration// 省略其他代码...@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}// 省略其他代码...
通过org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration自动配置类注入了组件org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration.ErrorPageCustomizer,该组件中的方法registerErrorPages()会在Spring Boot框架启动时调用,从而设定了默认错误访问路径为/error(即org.springframework.boot.autoconfigure.web.ErrorProperties类中的path变量,默认值为/error)。
实际上,这个默认的错误访问路径/error是可以通过参数server.error.path指定的。
server:error:path: /error
如下通过源码解析的方式阐述错误访问路径(默认为/error)是如何在Spring Boot框架启动时设置的。
-> org.springframework.context.support.AbstractApplicationContext.refresh()
-> org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh()
-> org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer()
-> org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.getWebServerFactory()
-> org.springframework.boot.web.server.ErrorPageRegistrarBeanPostProcessor.postProcessBeforeInitialization()
-> org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration.ErrorPageCustomizer.registerErrorPages()
在Spring Boot框架中对应参数server.error.path指定URL路径的Controller为org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController,它实现了接口:org.springframework.boot.web.servlet.error.ErrorController。
// org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {// 省略其他代码...@RequestMapping(produces = "text/html")public ModelAndView errorHtml(HttpServletRequest request,HttpServletResponse response) {HttpStatus status = getStatus(request);Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));response.setStatus(status.value());ModelAndView modelAndView = resolveErrorView(request, response, status, model);return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);}@RequestMapping@ResponseBodypublic ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {HttpStatus status = getStatus(request);if (status == HttpStatus.NO_CONTENT) {return new ResponseEntity<>(status);}Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));return new ResponseEntity<>(body, status);}// 省略其他代码...
}
另外,org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController也是通过自动配置类org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration注入到Spring容器中的。
// org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration// 省略其他代码...@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()));
}// 省略其他代码...
在项目开发中,还可以根据需要自定义新的org.springframework.boot.web.servlet.error.ErrorController实现。例如:在Filter中抛出异常时也会触发对/error路径的访问,但是在Filter中抛出的异常信息无法被全局异常拦截器捕获,此时可以在自定义org.springframework.boot.web.servlet.error.ErrorController中把在异常信息抛出来,统一实现异常在全局异常拦截器(使用@ControllerAdvice注解标记)中进行处理。
@RequestMapping("/")
public class DefaultErrorController implements ErrorController {@RequestMapping("/error")public void handleError(HttpServletRequest req) throws Throwable {Object exceptAttr = req.getAttribute("javax.servlet.error.exception");if (exceptAttr != null) {// 将异常抛出去统一在全局异常拦截器中处理throw (Throwable) exceptAttr;}}
}
关于/error路径的总结
在Spring Boot框架中,关于/error路径总结如下:
1.可以通过参数server.error.path为org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController指定访问路径,默认值为:/error。
2.当出现如下两种场景时都会触发对/error路径的访问:
- 访问不存在的接口,同时HTTP响应状态码为404
- 在Filter中抛出异常时
至此,关于在Spring Boot框架中触发访问/error路径的原因和执行流程分析完毕。