此篇文章来浅析一下Spring Web异常处理机制;
在此之前来看一下一个有意思的请求用例
api与用例如下
1 |
|

我们知道http请求时参数缺失时,一般是400状态码,明明请求接口存在,为何此处却是404呢?
其实是因为笔者在spring容器中注册了这么一个bean:ErrorController
1 | import org.springframework.boot.web.servlet.error.ErrorController; |
正是这个bean导致了Spring Web的异常处理能力出现了“bug”,且来看看其如何运作。
HandlerExceptionResolver
Spring Web中通过HandlerExceptionResolver接口来处理请求中产生的异常,解析出一个ModelAndView,以给到客户端合适的响应。
1 | public interface HandlerExceptionResolver { |
该接口的resolveException方法的实现约定为:
- 如果返回的ModelAndView为null,说明此异常解析器无法解析此异常
- 如果返回的ModelAndView不为null,说明异常已经处理,衍生出以下约定
- 若ModelAndView为空,即ModelAndView.isEmpty() == true,说明异常不返回视图,例如只返回http状态码给到客户端
- 若ModelAndView不为空,则要返回视图,即找到对应的视图,响应http正文给到客户端
以下是异常解析器在DispatcherServlet中的调用时机;
可以debug查看DispatcherServlet源码方法调用链,这里直接给出:
DispatcherServlet#doXXX -> DispatcherServlet#processRequest -> DispatcherServlet#doService -> DispatcherServlet#doDispatch -> DispatcherServlet#processDispatchResult -> DispatcherServlet#processHandlerException

从HandlerExceptionResolver的约定和源码中可以看出,HandlerExceptionResolver在框架内部可以配置多个,DispatcherServlet会根据配置顺序找到可用的解析器(返回的ModelAndView不为null)。
在DispatcherServlet#processDispatchResult 方法中可以看到,DispatcherServlet在拿到视图后会尝试进行渲染

大致流程如下

HandlerExceptionResolver配置
既然HandlerExceptionResolver最终交给了DispatcherServlet,那么来看看DispatcherServlet何时初始化了HandlerExceptionResolver。
从源码中可以看出以下初始化机制:
spring容器加载完毕时初始化各个组件,即也初始化了HandlerExceptionResolver

从容器中获取HandlerExceptionResolver的bean

而在spring mvc中,WebMvcConfigurationSupport是mvc的通用配置,其往容器中也注册了一个HandlerExceptionResolverbean;

可以看出,此配置类先尝试从容器中所配置的WebMvcConfigurerbean,获取配置HandlerExceptionResolver配置,如果未获取到则使用默认的配置。
默认配置:ExceptionHandlerExceptionResolver 、ResponseStatusExceptionResolver 、DefaultHandlerExceptionResolver

值得注意的是这里使用了HandlerExceptionResolverComposite将解析器整合成了一个HandlerExceptionResolverbean。
于是可以总结出以下流程初始化流程

另外,DispatcherServlet从容器中获取到的异常解析器,除了WebMvcConfigurationSupport提供的以外,还有ErrorMvcAutoConfiguration自动装配提供的DefaultErrorAttributes,其不提供视图,仅记录异常信息(实现了ErrorAttributes接口)

ErrorPage
默认的HandlerExceptionResolver有三个且顺序如下:
ExceptionHandlerExceptionResolver该异常解析器提供对
@ExceptionHandler注解的支持,通常内部自主处理异常,返回空ModelAndView(无需定向到视图页面)ResponseStatusExceptionResolver该异常解析器提供对ResponseStatusException异常和
@ResponseStatus注解的支持,内部调用response.sendError设置http状态码,返回空ModelAndView(无需定向到视图页面)DefaultHandlerExceptionResolver该异常解析器提供了spring web中常见的
ServletException的子类异常的处理支持,比如将HttpRequestMethodNotSupportedException异常处理为405响应等。内部调用response.sendError设置国际化消息,返回空ModelAndView(无需定向到视图页面)
可以看到上述这三个默认的异常解析器都不返回视图,且分为了两种处理方式:
- 异常解析器内部自行写入响应(
ExceptionHandlerExceptionResolver) - 异常解析器通过response.sendError发送(通知)异常
前者很好理解,如通常使用@ExceptionHandler注解返回一个pojo以json形式写入response.body,那后者具有什么作用呢?
参考HttpServletResponse.sendError的注释可以知道,调用该方法是向response通知异常,在后续流程中web容器会寻找error页面响应给客户端。
根据笔者的debug,这段逻辑在tomcat中的org.apache.catalina.core.StandardHostValve的invoke方法中

往后调用status方法,获取error页

调用custom方法将请求转发到error页

即流程如下:

在默认情况下,ErrorPage的路径为“/error”

回到最初提及的ErrorController,spring boot中其默认实现为BasicErrorController。其通过ErrorMvcAutoConfiguration注册到spring中(见源码)。
1 |
|
而在BasicErrorController提供了对/error的响应支持:
- errorHtml:若客户端请求的资源是页面,则返回error页面
- error:若客户端请求的资源不是页面,则将异常信息写入response.body

这里用的就是ErrorMvcAutoConfiguration提供的DefaultErrorAttributes来对异常信息进行读取。

其中读取异常信息时,由server.error开头的配置觉得读取异常的哪些信息(见ErrorProperties配置类)。
到此,算是对spring的异常处理有了比较全面的认识。
回到开头的请求用例。笔者注册了一个ErrorControllerbean,于是默认的BasicErrorController将不会再注册,当请求发生异常时,根据流程,tomcat会将请求转发到”/error”,由于web容器中不存在此地址,所以最终响应便是404。
ResponseEntityExceptionHandler
在如今的开发潮流下,java spring作为后端更多是提供rest的数据接口,而非提供静态资源和html页面。
在默认ErrorController中,若是处理rest请求的异常时,通常结构体如下
1 | { |
如果我们想自定义响应结构该如何做呢?
对于异常处理,最适宜的方式应该是@ExceptionHandler注解的方式,其不仅支持和controller一样的参数自动转换和绑定,书写以及后期扩展起来比明显自定义实现一个ErrorController更加方便。
那么我们知道,在web mvc 框架中,如对于参数不存在或者请求方法不支持(Get方式请求Post接口)等场景,其设计了对应的异常类型。如果我们自己通过@ExceptionHandler注解挨个声明处理这些框架内的异常类型岂不是非常累人。
于是,spring提供了这些框架内部异常类型的注解处理的模版,便是org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
我们只要继承此类就可通过重写方法的方式实现自定义的异常处理逻辑。

到此完结!