1. 异常的产生和抛出
Spring MVC
前置处理器DispatcherServlet
处理一个请求时,会尝试捕获handler
匹配过程,handler adapter
匹配过程和handler
执行过程中的异常,该异常记做dispatchException
,如下代码所示 :
protected void doDispatch(HttpServletRequest request, HttpServletResponse response)
throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
// 记录 handler 成功执行结果
ModelAndView mv = null;
// 记录请求处理过程中出现的异常 : handler匹配过程, handler adapter 匹配过程, handler执行过程
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
// 这种情况下,noHandlerFound方法会根据配置决定是抛出异常,
// 还是直接 response.sendError(404)
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
// handler adapter 匹配过程
// 如果 handler adapter 匹配失败,则会抛出异常 ServletException
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
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;
}
// Actually invoke the handler. 对 handler 的真正调用,
// 通过所匹配到的 handler adapter 完成
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
// 发生的任何异常都记录为 dispatchException
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
// 发生的任何异常都记录为 dispatchException
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 上面的逻辑对请求进行了处理,该处理逻辑可以分为如下四个方面 :
// 1. 匹配目标 handler
// 2. 匹配目标 handler adapter
// 3. 使用目标 handler adapter 执行对目标 handler 的调用
// 4. 捕获整个过程中可能发生的异常到 dispatchException
// 上面的处理流程可能有两个结果 :
// 1. 请求被标记为完成 (比如控制器方法上使用了@ResponseStatus注解并且控制器方法正常执行完成)
// 2. 其他情况,请求未被标记为完成
// 对于上面第1种情况,现在 mv 是 null, dispatchException 为 null
// 对于上面第2种情况, 现在 mv 或者 dispatchException 至少有一个不是 null
// 现在针对以上所有情况,综合使用 processDispatchResult 进行处理,决定如何渲染响应给浏览器端
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
2. 将异常转换成HTTP
状态码
上面doDispatch
方法对请求处理的主体流程可以简单地理解为两个步骤 :
- 寻找
handler
并执行以处理请求(request
)- 整个细分为以下几个方面
- 目标
handler
匹配过程, - 目标
handler adapter
匹配过程, - 目标
handler
执行过程 - 成功处理结果记录到
ModelAndView mv
- 异常捕获并记录到
Exception dispatchException
- 目标
- 整个细分为以下几个方面
- 加工
handler
请求处理结果做出响应(response
)
上面第二部分由processDispatchResult
方法完成 。 该方法接收第一阶段的结果,无论是ModelAndView
对象,还是请求处理过程中遇到的Exception
。processDispatchResult
方法实现如下 :
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) {
// 这种异常中定义了将要渲染给浏览器端的 ModelAndView 对象, 直接使用它即可
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
// 这种情况下需要使用 processHandlerException 将异常翻译成一个 ModelAndView 对象
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
// 如果 mv 不为 null 则说明有异常解析器处理了该异常并且找到了相应的异常视图
// 如果 mv 为 null 则说明有异常解析器处理了该异常但是没有相应的异常视图
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
// 渲染视图给浏览器端
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
processDispatchResult
的处理流程中调用了processHandlerException
来将异常转换成ModelAndView
,该方法的实现如下 :
/**
* Determine an error ModelAndView via the registered HandlerExceptionResolvers.
* @param request current HTTP request
* @param response current HTTP response
* @param handler the executed handler, or null if none chosen at the time of the exception
* (for example, if multipart resolution failed)
* @param ex the exception that got thrown during handler execution
* @return a corresponding ModelAndView to forward to
* @throws Exception if no error ModelAndView found
*/
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request,
HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// Success and error responses may use different content types
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null;
// 使用每一个注册的 HandlerExceptionResolver 尝试解析该异常到一个 ModelAndView 对象,
// 如果某个 HandlerExceptionResolver 能解析该异常到一个 ModelAndView 对象,则停止并使用
// 该 ModelAndView 对象
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
// 如果当前 resolver 能解析该异常,它会对响应做相应的处理并返回一个 ModelAndView
// 对象。该 ModelAndView 对象 exMv 可能为空或者不为空。为空表明该 resolver
// 处理了该异常。
//
// 什么是一个空的 ModelAndView 对象 ?
// 1. 不是 null
// 2. 对象的 view 属性为 null 并且 model 属性为空 (比如是 new ModelAndView() )
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
// 如果 exMv 不为 null, 但是为空 ModelAndView 对象,也就是其 view 为 null 并且 model 为空,
// 则说明该异常已经被处理,返回 null ModelAndView 即可
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
// We might still need view name translation for a plain error model...
if (!exMv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
exMv.setViewName(defaultViewName);
}
}
if (logger.isTraceEnabled()) {
logger.trace("Using resolved error view: " + exMv, ex);
}
if (logger.isDebugEnabled()) {
logger.debug("Using resolved error view: " + exMv);
}
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
// 找到了针对该异常的异常视图
return exMv;
}
// 如果针对指定异常没有异常解析器能够处理它,则继续抛出该异常
throw ex;
}
在processHandlerException
对异常进行解析的过程中,使用到了属性handlerExceptionResolvers
,这是一组在应用启动时初始化到DispatcherServlet
的HandlerExceptionResolver
。这组HandlerExceptionResolver bean
的定义来自WebMvcConfigurationSupport
,缺省情况下,它们是以下类型的3个HandlerExceptionResolver bean
:
ExceptionHandlerExceptionResolver
ResponseStatusExceptionResolver
DefaultHandlerExceptionResolver
这3个bean
中,DefaultHandlerExceptionResolver
是对Spring MVC
标准异常进行处理的,它会将能处理的某种异常根据异常语义转换成一个HTTP
状态码(会通过调用response.sendError()
),并返回一个空ModelAndView
表明该异常已经被处理。下表是它能处理的异常和对应要转换成的HTTP
状态码:
Spring MVC 标准异常 | HTTP状态码 | 介绍 |
---|---|---|
HttpRequestMethodNotSupportedException | 405 (SC_METHOD_NOT_ALLOWED ) | HTTP 方法不支持 |
HttpMediaTypeNotSupportedException | 415 (SC_UNSUPPORTED_MEDIA_TYPE ) | MIME 类型不支持 |
HttpMediaTypeNotAcceptableException | 406 (SC_NOT_ACCEPTABLE ) | MIME 类型不被接受 |
MissingPathVariableException | 500 (SC_INTERNAL_SERVER_ERROR ) | 路径参数缺失 |
MissingServletRequestParameterException | 400 (SC_BAD_REQUEST ) | Servlet 请求参数缺失 |
ServletRequestBindingException | 400 (SC_BAD_REQUEST ) | Servlet 请求处理过程中的绑定异常 |
ConversionNotSupportedException | 500 (SC_INTERNAL_SERVER_ERROR ) | 不支持该类型转换 |
TypeMismatchException | 400 (SC_BAD_REQUEST ) | 类型不匹配 |
HttpMessageNotReadableException | 400 (SC_BAD_REQUEST ) | HTTP 消息不可读使用者: HttpMessageConverter#read |
HttpMessageNotWritableException | 500 (SC_INTERNAL_SERVER_ERROR ) | HTTP 消息不可写使用者: HttpMessageConverter#write |
MethodArgumentNotValidException | 400 (SC_BAD_REQUEST ) | 方法参数值验证失败 |
MissingServletRequestPartException | 400 (SC_BAD_REQUEST ) | multipart/form-data 请求时,part 名称指定的part 数据不存在 |
BindException | 400 (SC_BAD_REQUEST ) | Spring 验证框架中的绑定异常 |
NoHandlerFoundException | 404 (SC_NOT_FOUND ) | 没有找到能处理该请求的handler |
AsyncRequestTimeoutException | 503 (SC_SERVICE_UNAVAILABLE ) | 异步请求超时 |
3. 总结
基于以上分析,现在我们就不难看出Spring MVC
中HTTP 状态码 400
错误(其实不仅仅是400
错误,还有其他状态码错误)是怎么发生的了。可以简单总结如下 :
Spring MVC
应用启动过程中加载了一个DefaultHandlerExceptionResolver
;
该HandlerExceptionResolver
能解析Spring MVC
标准异常并将其转换成HTTP
状态码;DispatcherServlet
处理请求过程中会抛出各种异常;DefaultHandlerExceptionResolver
会被调用用于解析某个异常并向响应设置相应的HTTP
状态码。
这里使用的异常到HTTP
状态码的转换规则就来自上面提到的表格。
参考文章
Spring MVC DispatcherServlet 策略初始化 – initHandlerExceptionResolvers
Spring MVC : WebMvcConfigurationSupport 中定义的 HandlerExceptionResolver 组件