【Spring MVC】Filter 过滤器异常处理 HandlerExceptionResolver 分析

文章目录

前言

最近在开发过滤器功能的时候遇到一个有趣的问题:

FilterChain#doFilter 方法之前抛出了一个异常,但是这个异常无法被异常处理器捕获,需要使用 HandlerExceptionResolver 来处理异常。

因此本文来分析一下这个问题。

版本说明

本文使用的版本如下:

  • Spring Bootv3.1.5
  • Lombokv1.18.26
  • Hutoolv5.8.15

测试 Demo

Demo 打成了压缩包(在文章开头)可以自行下载使用。

1、自定义过滤器 DemoFilter

2、自定义业务异常 ServiceException

3、自定义异常处理类 DemoExceptionHandler

4、DemoController

5、请求测试

请求 URL:

http://localhost:8080/hello

请求结果:

返回了错误码 500,且错误信息是:

json 复制代码
{
  "timestamp": "2023-11-26T03:43:07.655+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "path": "/hello"
}

这里并没有返回理想中的结果:

在自定义异常处理器重定义的错误码是 400,返回信息应该是 ServiceException: / by zero

问题分析

1、日志打印记录

根据日志的打印结果来 Debug 分析一下整个流程:

2023-11-26T11:49:25.841+08:00  INFO 5748 --- [  XNIO-1 task-2] space.zlyx.demo.mvc.filter.DemoFilter    : DemoFilterBefore: 在请求经过过滤器之前的操作
2023-11-26T11:49:25.842+08:00 ERROR 5748 --- [  XNIO-1 task-2] io.undertow.request                      : UT005023: Exception handling request to /hello

space.zlyx.demo.mvc.exception.ServiceException: ServiceException: / by zero
# 省略详细报错(完整日志可查看文末附录)

2023-11-26T11:49:25.842+08:00 DEBUG 5748 --- [  XNIO-1 task-2] o.s.web.servlet.DispatcherServlet        : "ERROR" dispatch for GET "/error", parameters={}
2023-11-26T11:49:25.843+08:00 DEBUG 5748 --- [  XNIO-1 task-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
2023-11-26T11:49:25.843+08:00 DEBUG 5748 --- [  XNIO-1 task-2] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Using 'application/json', given [*/*] and supported [application/json, application/*+json]
2023-11-26T11:49:25.844+08:00 DEBUG 5748 --- [  XNIO-1 task-2] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Writing [{timestamp=Sun Nov 26 11:49:25 CST 2023, status=500, error=Internal Server Error, path=/hello}]
2023-11-26T11:49:25.845+08:00 DEBUG 5748 --- [  XNIO-1 task-2] o.s.web.servlet.DispatcherServlet        : Exiting from "ERROR" dispatch, status 500

日志信息可以看出异常抛出之后没有进入异常处理器,直接就写出了。

2、Debug 方法

org.springframework.web.servlet.DispatcherServlet#doService

org.springframework.web.servlet.DispatcherServlet#logRequest

org.springframework.web.servlet.DispatcherServlet#doDispatch

java 复制代码
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle

org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor#handleReturnValue

org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters

解决方案

1、修改自定义过滤器

在自定义过滤器中使用 HandlerExceptionResolver 处理异常信息:

2、请求测试

请求 URL:

http://localhost:8080/hello

请求结果:

修改之后返回了正确的错误信息以及错误码。

解决方案分析

1、日志打印记录

2023-11-26T13:02:11.053+08:00  INFO 11220 --- [  XNIO-1 task-2] space.zlyx.demo.mvc.filter.DemoFilter    : DemoFilterBefore: 在请求经过过滤器之前的操作
2023-11-26T13:02:11.053+08:00 DEBUG 11220 --- [  XNIO-1 task-2] .m.m.a.ExceptionHandlerExceptionResolver : Using @ExceptionHandler space.zlyx.demo.mvc.handler.DemoExceptionHandler#handleServiceException(ServiceException, HttpServletRequest)
2023-11-26T13:02:11.054+08:00 ERROR 11220 --- [  XNIO-1 task-2] s.z.d.mvc.handler.DemoExceptionHandler   : 业务异常捕获:Filter ServiceException: / by zero
2023-11-26T13:02:11.054+08:00 DEBUG 11220 --- [  XNIO-1 task-2] m.m.a.RequestResponseBodyMethodProcessor : Using 'text/plain', given [*/*] and supported [text/plain, */*, application/json, application/*+json]
2023-11-26T13:02:11.054+08:00 DEBUG 11220 --- [  XNIO-1 task-2] m.m.a.RequestResponseBodyMethodProcessor : Writing ["Filter ServiceException: / by zero"]
2023-11-26T13:02:11.055+08:00 DEBUG 11220 --- [  XNIO-1 task-2] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [ServiceException(code=null, message=Filter ServiceException: / by zero)]
2023-11-26T13:02:11.055+08:00 DEBUG 11220 --- [  XNIO-1 task-2] o.s.web.servlet.DispatcherServlet        : GET "/hello", parameters={}
2023-11-26T13:02:11.055+08:00 DEBUG 11220 --- [  XNIO-1 task-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to space.zlyx.demo.mvc.controller.DemoController#hello(HttpServletRequest, HttpServletResponse)
2023-11-26T13:02:11.055+08:00  INFO 11220 --- [  XNIO-1 task-2] s.z.demo.mvc.controller.DemoController   : DemoController hello
2023-11-26T13:02:11.055+08:00 DEBUG 11220 --- [  XNIO-1 task-2] m.m.a.RequestResponseBodyMethodProcessor : Found 'Content-Type:text/plain;charset=UTF-8' in response
2023-11-26T13:02:11.055+08:00 DEBUG 11220 --- [  XNIO-1 task-2] m.m.a.RequestResponseBodyMethodProcessor : Writing ["Hello World"]
2023-11-26T13:02:11.056+08:00 DEBUG 11220 --- [  XNIO-1 task-2] o.s.web.servlet.DispatcherServlet        : Completed 400 BAD_REQUEST
2023-11-26T13:02:11.056+08:00  INFO 11220 --- [  XNIO-1 task-2] space.zlyx.demo.mvc.filter.DemoFilter    : DemoFilterAfter: 在请求经过过滤器之后的操作

2、Debug 方法

org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#doResolveHandlerMethodException

org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle

DemoExceptionHandler#handleServiceException

org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#handleReturnValue

org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters

对比总结

经过两种不同的方式的调用对比,可以得出以下结论:

  1. FilterChain#doFilter 之前直接抛出异常,则过滤器链中断,抛出异常之后结果由 HttpEntityMethodProcessor 进行处理。
  2. 使用 HandlerExceptionResolver#resolveException 方法,异常会被 ExceptionHandlerExceptionResolver 进行处理,此时能够被异常拦截器捕获,最终返回的结果由 RequestResponseBodyMethodProcessor 进行处理。
  3. 使用 HandlerExceptionResolver 时,因为过滤器链没有中断,因此内部会继续执行 controller 方法,但是请求结果已经提前输出了。

扩展

Filter、Interceptor、ControllerAdvice、Aspect 执行顺序

在没有异常的情况下,各组件调用顺序如下(不包含 ControllerAdvice):

  • FilterChain#doFilter 之前
  • Interceptor#preHandle
  • Aspect#before
  • Controller
  • Aspect#afterReturning
  • Aspect#after
  • Interceptor#postHandle
  • Interceptor#afterCompletion
  • FilterChain#doFilter 之后

在本文基础上顺便测试了一下 Interceptor 以及 Aspect,并在各个组件中抛出异常,得到的结果如下:

注:ControllerAdvice 即异常处理器

异常位置 异常抛出方式 调用方法
Filter HandlerExceptionResolver FilterChain#doFilter 之前(异常) ControllerAdvice Interceptor#preHandle Aspect#before Controller Aspect#afterReturning Aspect#after Interceptor#postHandle Interceptor#afterCompletion FilterChain#doFilter 之后
Interceptor 直接抛异常 FilterChain#doFilter 之前 Interceptor#preHandle(异常) ControllerAdvice FilterChain#doFilter 之后
Aspect 直接抛异常 FilterChain#doFilter 之前 Interceptor#preHandle Aspect#before(异常) ControllerAdvice Interceptor#afterCompletion FilterChain#doFilter 之后
Controller 直接抛异常 FilterChain#doFilter 之前 Interceptor#preHandle Aspect#before Controller(异常) Aspect#afterThrowing Aspect#after ControllerAdvice Interceptor#afterCompletion FilterChain#doFilter 之后

请求进入执行顺序:
Filter ------ Interceptor ------ Aspect ------ Controller

异常抛出执行顺序:
Controller ------ Aspect ------ ControllerAdvice ------ Interceptor ------ Filter

附录

附录1:直接抛出异常的详细日志

2023-11-26T11:49:25.841+08:00  INFO 5748 --- [  XNIO-1 task-2] space.zlyx.demo.mvc.filter.DemoFilter    : DemoFilterBefore: 在请求经过过滤器之前的操作
2023-11-26T11:49:25.842+08:00 ERROR 5748 --- [  XNIO-1 task-2] io.undertow.request                      : UT005023: Exception handling request to /hello

space.zlyx.demo.mvc.exception.ServiceException: ServiceException: / by zero
	at space.zlyx.demo.mvc.filter.DemoFilter.doFilter(DemoFilter.java:40) ~[classes/:na]
	at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.0.13.jar:6.0.13]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.13.jar:6.0.13]
	at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.0.13.jar:6.0.13]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.13.jar:6.0.13]
	at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.0.13.jar:6.0.13]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.13.jar:6.0.13]
	at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:117) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) ~[undertow-core-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46) ~[undertow-core-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60) ~[undertow-core-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43) ~[undertow-core-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) ~[undertow-core-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.SendErrorPageHandler.handleRequest(SendErrorPageHandler.java:52) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43) ~[undertow-core-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:276) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:135) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:132) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:256) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:101) ~[undertow-servlet-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.server.Connectors.executeRootHandler(Connectors.java:393) ~[undertow-core-2.3.10.Final.jar:2.3.10.Final]
	at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:859) ~[undertow-core-2.3.10.Final.jar:2.3.10.Final]
	at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18) ~[jboss-threads-3.5.0.Final.jar:3.5.0.Final]
	at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513) ~[jboss-threads-3.5.0.Final.jar:3.5.0.Final]
	at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538) ~[jboss-threads-3.5.0.Final.jar:3.5.0.Final]
	at org.xnio.XnioWorker$WorkerThreadFactory$1$1.run(XnioWorker.java:1282) ~[xnio-api-3.8.8.Final.jar:3.8.8.Final]
	at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

2023-11-26T11:49:25.842+08:00 DEBUG 5748 --- [  XNIO-1 task-2] o.s.web.servlet.DispatcherServlet        : "ERROR" dispatch for GET "/error", parameters={}
2023-11-26T11:49:25.843+08:00 DEBUG 5748 --- [  XNIO-1 task-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
2023-11-26T11:49:25.843+08:00 DEBUG 5748 --- [  XNIO-1 task-2] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Using 'application/json', given [*/*] and supported [application/json, application/*+json]
2023-11-26T11:49:25.844+08:00 DEBUG 5748 --- [  XNIO-1 task-2] o.s.w.s.m.m.a.HttpEntityMethodProcessor  : Writing [{timestamp=Sun Nov 26 11:49:25 CST 2023, status=500, error=Internal Server Error, path=/hello}]
2023-11-26T11:49:25.845+08:00 DEBUG 5748 --- [  XNIO-1 task-2] o.s.web.servlet.DispatcherServlet        : Exiting from "ERROR" dispatch, status 500
相关推荐
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭22 分钟前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫38 分钟前
泛型(2)
java
超爱吃士力架43 分钟前
邀请逻辑
java·linux·后端
南宫生1 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石1 小时前
12/21java基础
java
李小白661 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp1 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea
装不满的克莱因瓶2 小时前
【Redis经典面试题六】Redis的持久化机制是怎样的?
java·数据库·redis·持久化·aof·rdb
n北斗2 小时前
常用类晨考day15
java
骇客野人2 小时前
【JAVA】JAVA接口公共返回体ResponseData封装
java·开发语言