此篇文章来浅析一下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的通用配置,其往容器中也注册了一个HandlerExceptionResolver
bean;
可以看出,此配置类先尝试从容器中所配置的WebMvcConfigurer
bean,获取配置HandlerExceptionResolver
配置,如果未获取到则使用默认的配置。
默认配置:ExceptionHandlerExceptionResolver
、ResponseStatusExceptionResolver
、DefaultHandlerExceptionResolver
值得注意的是这里使用了HandlerExceptionResolverComposite
将解析器整合成了一个HandlerExceptionResolver
bean。
于是可以总结出以下流程初始化流程
另外,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的异常处理有了比较全面的认识。
回到开头的请求用例。笔者注册了一个ErrorController
bean,于是默认的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
;
我们只要继承此类就可通过重写方法的方式实现自定义的异常处理逻辑。
到此完结!