阅读完需:约 35 分钟
Java异常体系简介
Java相较于其它大多数语言提供了一套非常完善的异常体系Throwable:分为Error和Exception两大分支:
-
Error
:错误,对于所有的编译时期的错误以及系统错误都是通过Error
抛出的,比如NoClassDefFoundError、Virtual MachineError、ZipError
、硬件问题等等。 -
Exception
:异常,是更为重要的一个分支,是程序员经常打交道的。异常定义为是程序的问题,程序本身是可以处理的。
Error和Exception最大的区别是:异常是可以被程序处理的,而错误是没法处理的。
错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况(比如类找不到NoClassDefFoundError
)
当然,异常Exception
它本身还分为两大重要的分支:Checked Exception
(可检查异常,如IOException)和Unchecked Exception
(不可检查异常,如RuntimeException)。
为何需要全局
异常处理?
在web项目开发时,我们一般把业务代码(大量代码)写在Service
层。作为面向返回的Controller
层就需要关注一些异常情况了:如此一来,我们的Controller
层就不得不进行try-catch
,形如这样子:
@GetMapping("/test")
public String test() {
try {
... // 处理你的业务逻辑
return "success";
} catch (Exception e) {
return "fail"; // 处理异常
}
}
显然,这么处理至少有如下两大问题:
-
Controller
一般方法众多,那就需要写大量的try-catch
代码,很难看也很难维护 - 在此处
try-catch
也只能捕获住Handler
的异常,万一是view抛出异常了呢?
古老的异常处理方式
在还没有Spring
,更无Spring Boot
时,开发使用的是源生的Servlet + tomcat
容器。其实它也是提供了通用的异常的处理配置方式的(自己控制response的方式不在本文讨论访问内)。如果你是“老”程序员,你应该在web.xml
里看到过如下配置:
<!-- 根据状态码 -->
<error-page>
<error-code>500</error-code>
<location>/500.jsp</location>
</error-page>
<!-- 根据异常类型 -->
<error-page>
<exception-type>java.lang.RuntimeException</exception-type>
<location>/500.jsp</location>
</error-page>
配置上的效果很容易理解,这里就不赘述。但是显然这种做法已经完全落伍了,毕竟web.xml
都已经被淘汰了嘛,所以我此处把它称为古老的异常处理方式。
Spring MVC处理异常
版本:Spring-mvcweb:5.3.7
Spring MVC
作为现在the most known
的Web框架产品,优雅异常处理这块它当然提供了完善的支持。Spring MVC
提供处理异常的方式主要分为两种:
- 实现
HandlerExceptionResolver
方式 -
@ExceptionHandler
注解方式。注解方式也有两种用法:
1. 使用在Controller
内部
2. 配置@ControllerAdvice
一起使用实现全局处理
HandlerExceptionResolver
// @since 22.11.2003
public interface HandlerExceptionResolver {
// 注意:handler是有可能为null的,比如404
@Nullable
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
继承树:
处理方法返回一个ModelAndView
视图:既可以是json,也可以是页面。从接口参数上可以发现的是:它只能处理Exception
,因为Error
是程序处理不了的(注意:Error
也是可以捕获的),因此入参类型若写成Throwable
是不合适的。
有人会问为何不捕获Error呢?此处简答一下:因为出现Error的情况会造成程序直接无法运行,所以捕获了也没有任何意义。
HandlerExceptionResolverComposite
这种模式的类已经非常熟悉了,就不用再分析了,它实现的是短路效果:只要有一个Resolver
返回了不为null的视图就截止了,否则继续处理。多个处理器的顺序可用Ordered
控制(需要注意的是:若你是HandlerExceptionResolverComposite#add
进来的,那order是不生效的请手动控制此ArrayList)
AbstractHandlerExceptionResolver
可以看到所有其它子类的实现都是此抽象类的子类,所以若我们自定义异常处理器,我也推荐从此处去继承,它是Spring3.0
后才有的。它主要是提供了对异常更细粒度的控制:此Resolver
可只处理指定类型的异常。
// @since 3.0
public abstract class AbstractHandlerExceptionResolver implements HandlerExceptionResolver, Ordered {
...
private int order = Ordered.LOWEST_PRECEDENCE;
// 可以设置任何的handler,表示只作用于这些Handler们
@Nullable
private Set<?> mappedHandlers;
// 表示只作用域这些Class类型的Handler们~~~
@Nullable
private Class<?>[] mappedHandlerClasses;
// 以上两者若都为null,那就是匹配素有。但凡有一个有值,那就需要精确匹配(并集的关系)
... // 省略所有的get/set方法
@Override
@Nullable
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
// 这个作用匹配逻辑很简答
// 若mappedHandlers和mappedHandlerClasses都为null永远返回true
// 但凡配置了一个就需要精确匹配(并集关系)
// 需要注意的是:shouldApplyTo方法,子类AbstractHandlerMethodExceptionResolver是有复写的
if (shouldApplyTo(request, handler)) {
// 是否执行;response.addHeader(HEADER_CACHE_CONTROL, "no-store") 默认是不执行的
prepareResponse(ex, response);
// 此抽象方法留给子类去完成~~~~~
ModelAndView result = doResolveException(request, response, handler, ex);
return result;
} else { // 若此处理器不处理,就返回null呗
return null;
}
}
}
此抽象类主要是提供setMappedHandlers
和setMappedHandlerClasses
让此处理器可以作用在指定类型/处理器上,因此子类只要继承了它都将会有这种能力,这也是为何我推荐自定义实现也继承于它的原因。它提供了shouldApplyTo()
方法用于匹配逻辑,子类若想定制化匹配规则,亦可复写此方法。
SimpleMappingExceptionResolver
顾名思义它就是通过简单映射关系来决定由哪个错误视图来处理当前的异常信息。它提供了多种映射关系可以使用:
- 通过异常类型
Properties exceptionMappings;
映射。它的key可以是全类名、短名称,同时还有继承效果:比如key是Exception那将匹配所有的异常。value是view name视图名称 - 若有需要,可以配合
Class[] excludedExceptions
来一起使用
通过状态码Map statusCodes
匹配。key是view name,value是http状态码
它的源码部分,我们只需要关心下面这一个方法就可以了:
SimpleMappingExceptionResolver:
@Override
@Nullable
protected ModelAndView doResolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
// 根据异常类型去exceptionMappings匹配到一个viewName
// 实在木有匹配到,就用的defaultErrorView(当然defaultErrorView也可能为null没配置,不过建议配置)
String viewName = determineViewName(ex, request);
if (viewName != null) {
// 如果匹配上了一个视图后,再去使用视图匹配出一个statusCode
// 若没匹配上就用defaultStatusCode(当然它也有可能为null)
Integer statusCode = determineStatusCode(request, viewName);
if (statusCode != null) {
// 执行response.setStatus(statusCode)
applyStatusCodeIfPossible(request, response, statusCode);
}
// new ModelAndView(viewName) 设置好viewName
// 并且,并且,并且:mv.addObject(this.exceptionAttribute, ex)把异常信息放进去。exceptionAttribute的值默认为:exception
return getModelAndView(viewName, ex, request);
} else {
return null;
}
}
此类是Spring
首个版本就内置的,其它的均是Spring3.0+
才出现。此简单映射功能还算强大,但使用起来有诸多不便,因此Spring MVC
默认情况下并没有装配上它(它几乎处于一个被弃用的状态,基本可忽略)。
ResponseStatusExceptionResolver
若抛出的异常类型上有@ResponseStatus
注解,那么此处理器就会处理,并且状态码会返给response。Spring5.0
还能处理ResponseStatusException
这个异常(此异常是5.0新增)。
// 实现了接口MessageSourceAware,方便拿到国际化资源,方便错误消息的国际化
// @since 3.0
public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver implements MessageSourceAware {
@Nullable
private MessageSource messageSource;
@Override
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
@Override
@Nullable
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
try {
// 若异常类型是,那就处理这个异常
// 处理很简单:response.sendError(statusCode, resolvedReason)
// 当然会有国际化消息的处理。最终new一个空的new ModelAndView()供以返回
if (ex instanceof ResponseStatusException) {
return resolveResponseStatusException((ResponseStatusException) ex, request, response, handler);
}
// 若异常类型所在的类上标注了ResponseStatus注解,就处理这个状态码
//(可见:异常类型优先于ResponseStatus)
// 处理方式同上~~~~
ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
if (status != null) {
return resolveResponseStatus(status, request, response, handler, ex);
}
// 这里有个递归:如果异常类型是Course里面的,也会继续处理,所以需要注意这里的递归处理
if (ex.getCause() instanceof Exception) {
return doResolveException(request, response, handler, (Exception) ex.getCause());
}
} catch (Exception resolveEx) { // 处理失败,就记录warn日志(非info哦~)
if (logger.isWarnEnabled()) {
logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", resolveEx);
}
}
return null;
}
}
这里有个处理的小细节:递归调用了doResolveException()
方法,也就是说若有coouse
原因也是异常,那就继续会尝试处理的。
另外请注意:@ResponseStatus
标注在异常类上此处理器才会处理,而不是标注在处理方法上,或者所在类上哦,所以一般用于自定义异常时使用。
DefaultHandlerExceptionResolver
默认的异常处理器。它能够处理标准的Spring MVC
异常们,并且把它转换为对应的HTTP status codes,一般作为兜底处理,Spring MVC
默认也注册了此处理器。它能处理的异常非常之多,简单列出来如下:
-
MissingPathVariableException
500 -
ConversionNotSupportedException
500 -
HttpMessageNotWritableException
500 -
AsyncRequestTimeoutException
503 -
MissingServletRequestParameterException
400 -
ServletRequestBindingException
400 -
TypeMismatchException
400 -
HttpMessageNotReadableException
400 -
MethodArgumentNotValidException
400 -
MissingServletRequestPartException
400 -
BindException
400 -
NoHandlerFoundException
404 -
HttpRequestMethodNotSupportedException
405 -
HttpMediaTypeNotAcceptableException
406 -
HttpMediaTypeNotSupportedException
415
// @since 3.0
public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionResolver {
public DefaultHandlerExceptionResolver() {
setOrder(Ordered.LOWEST_PRECEDENCE);
setWarnLogCategory(getClass().getName()); // 不同的日志采用不同的记录器是个很好的习惯
}
@Override
@Nullable
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
try {
if (ex instanceof HttpRequestMethodNotSupportedException) {
return handleHttpRequestMethodNotSupported(
(HttpRequestMethodNotSupportedException) ex, request, response, handler);
} else if (ex instanceof HttpMediaTypeNotSupportedException) {
return handleHttpMediaTypeNotSupported(
(HttpMediaTypeNotSupportedException) ex, request, response, handler);
} ... // 省略其它的else if
// 多有的handle方法几乎一样的,都是response.sendError()
// 有的还会esponse.setHeader("Accept", MediaType.toString(mediaTypes));等等
}
}
它对这些异常的处理,亦可参考内置的ResponseEntityExceptionHandler
实现,它提供了基于@ExceptionHandler
的很多异常类型的处理。
DispatcherServlet
对它的初始化和应用
因为Spring MVC
对请求的整个处理流程都是由DispatcherServlet
来控制的,异常处理也属于请求的一部分,所以它的初始化和应用都在此处。
初始化
虽然异常处理非常重要,但绝大多数情况下你可能并不知道Spring MVC
它内置就自动给我们配置好了一些异常处理器。DispatcherServlet
初始化它的相关代码如下:
DispatcherServlet:
protected void initStrategies(ApplicationContext context) {
...
initHandlerExceptionResolvers(context); // 第六步
...
}
// 寻找逻辑(detectAllHandlerExceptionResolvers默认值是true表示回去容器里寻找):
// 1、若detect = true(默认是true),去容器里找出所有`HandlerExceptionResolver`类型的Bean们,找到后排序
// 2、若detect = false(可手动更改),那就拿名称为`handlerExceptionResolver`这单独的一个Bean(context.getBean())
// 3、如果一个都木有找到,那就走默认策略getDefaultStrategies(),详见下面截图~~~
private void initHandlerExceptionResolvers(ApplicationContext context) {
this.handlerExceptionResolvers = null;
if (this.detectAllHandlerExceptionResolvers) {
// Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
// We keep HandlerExceptionResolvers in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
}
} else {
try {
HandlerExceptionResolver her = context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
this.handlerExceptionResolvers = Collections.singletonList(her);
} catch (NoSuchBeanDefinitionException ex) {
// Ignore, no HandlerExceptionResolver is fine too.
}
}
// Ensure we have at least some HandlerExceptionResolvers, by registering
// default HandlerExceptionResolvers if no other resolvers are found.
if (this.handlerExceptionResolvers == null) {
this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerExceptionResolvers declared in servlet '" + getServletName() + "': using default strategies from DispatcherServlet.properties");
}
}
}
- 开启
@EnableWebMvc
后,使用的异常处理器是HandlerExceptionResolverComposite
- 若不开启
@EnableWebMvc
,就执行默认策略
应用流程
请求交给Handler
处理后得到返回结果Result
,但result可能会有异常,因此DispatcherServlet
会针对性对result
做处理:
DispatcherServlet:
// 处理request请求
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
... // 全部处理完成后,这中间可以是真正结果,也有可能有异常,交给结果处理器
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
... // 执行拦截器的AfterCompletion方法
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
boolean errorView = false;
// 不等于null,说明有异常哦~~~~ 那就处理异常
if (exception != null) {
// 此种异常属于Spring MVC内部的异常
if (exception instanceof ModelAndViewDefiningException) {
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
} else {
// 若是普通异常,就交给方法processHandlerException()去统一处理
// 从而得到一个异常视图ModelAndView,并且标注errorView = true(若不为null的话)
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
...
// 渲染此错误视图(若不为null)
render(mv, request, response)
...
}
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception {
...
ModelAndView exMv = null;
// 核心处理办法就在此处,exMv 只有有一个视图返回了,就立马停止(短路效果)
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
... // 后面处理viewName等等~~~~~~~
}
从应用流程上看是比较简单的,但是了解了此处理流程对我们后续使用、定制会有很好的促进作用。
自定义HandlerExceptionResolver
处理异常
上面两个案例都是使用Spring MVC
内置的异常处理器,显然用户体验均非常不友好。所以在实际生产环境,必须是需要自己来处理异常(页面)的,下面采用自定义HandlerExceptionResolver
方式给出Demo案例,仅供参考:
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
// 自定义异常处理器一般请放在首位
exceptionResolvers.add(0, new AbstractHandlerExceptionResolver() {
@Override
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 若是自定义的业务异常,那就返回到单页面异常页面
if (ex instanceof BusinessException) {
return new ModelAndView("/business.jsp");
} else { // 否则统一到统一的错误页面
return new ModelAndView("/error.jsp");
}
}
});
}
}
@ExceptionHandler
我们可以通过自定义HandlerExceptionResolver
实现来处理程序异常,当然Spring MVC也内置了一些实现来对异常处理进行支持。估计已经很少人知道HandlerExceptionResolver
这个异常处理器接口(更有甚者连ModelAndView
都没听说过也大有人在啊),虽然这不应该,但存在即合理。因此从现象上可以认为使用自定义HandlerExceptionResolver
实现的方式去处理异常已经out了,它已经被新的方式所取代:@ExceptionHandler
方式。
回忆HandlerExceptionResolver
,你是否疑问过这个问题:通过HandlerExceptionResolver
如何返回一个json串呢?其实这个问题雷同于:源生Servlet
如何给前端返回一个json串呢?
HandlerExceptionResolver如何返回JSON格式数据
自定义了一个异常处理器来处理Handler抛出的异常,示例中返回的是一个页面ModelAndView
。但是通常情况下我们的应用都是REST应用,我们的接口返回的都是一个JSON串,那么若接口抛出异常的话我们处理好后也同样的返回一个JSON串比返回一个页面更为合适。
这时若你项目较老,使用的仍旧是HandlerExceptionResolver
方式处理异常的话,我在本处提供两种处理方式,供以参考:
方式一:response直接输出json
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
// 自定义异常处理器一般请放在首位
exceptionResolvers.add(0, new AbstractHandlerExceptionResolver() {
@Override
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
try {
String jsonStr = "";
if (ex instanceof BusinessException) {
response.setStatus(HttpStatus.OK.value());
jsonStr = "{'code':100001,'message':'业务异常,请联系客服处理'}";
} else {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
jsonStr = "{'code':500,'message':'服务器未知异常'}";
}
response.getWriter().print(jsonStr);
response.getWriter().flush();
response.getWriter().close();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
});
}
}
注意事项:
- 因为
return null
,所以后面若还有处理器将继续执行。但因为本处已把response close
了,因此请确保后面不会再使用此response
- 若所有Resolver处理完后还是return null,那Spring MVC将直接throw ex,因此你看到的效果是:控制台上有异常栈,但是前段页面上显示是友好的json串。
- 因为没有
ModelAndView
(值为null),所以不会有渲染步骤,因此后续步骤Spring MVC也不会再使用到response(自定义的拦截器除外)。
方式二:借助MappingJackson2JsonView
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
// 自定义异常处理器一般请放在首位
exceptionResolvers.add(0, new AbstractHandlerExceptionResolver() {
@Override
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView mv = new ModelAndView();
MappingJackson2JsonView view = new MappingJackson2JsonView();
view.setJsonPrefix("fsxJson"); // 设置JSON前缀,有的时候很好用的哦
//view.setModelKey(); // 让只序列化指定的key
mv.setView(view);
// 这样添加key value就非常方便
mv.addObject("code", "100001");
mv.addObject("message", "业务异常,请联系客服处理");
return mv;
}
});
}
}
显然这种使用JsonView的方式代码看起来更加舒服,使用起来更加的面向对象。
这两种方式都是基于自定义HandlerExceptionResolver
实现类的方式来处理异常,最终给前端返回一个json串。
@ExceptionHandler
此注解是Spring 3.0
后提供的处理异常的注解,整个Spring
在3.0+
中新增了大量的能力来对REST
应用提供支持,此注解便是其中之一。
它(只能)标注在方法上,可以使得这个方法成为一个异常处理器,处理指定的异常类型。
// @since 3.0
@Target(ElementType.METHOD) // 只能标注在方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
// 指定异常类型,可以多个
Class<? extends Throwable>[] value() default {};
}
上面讲解HandlerExceptionResolver
的原理部分讲到了,DispatcherServlet
对异常的处理最终都是无一例外的交给了HandlerExceptionResolver
异常处理器,因此很容易想到@ExceptionHandler
它的底层实现原理其实也是一个异常处理器,它便是:ExceptionHandlerExceptionResolver
。
在分析它之前,需要先前置介绍两个类:AbstractHandlerMethodExceptionResolver
和ExceptionHandlerMethodResolver
AbstractHandlerMethodExceptionResolver
它是ExceptionHandlerExceptionResolver
的抽象父类,服务于处理器类型是HandlerMethod
类型的抛出的异常,它并不规定实现方式必须是@ExceptionHandler
。它复写了抽象父类AbstractHandlerExceptionResolver
的shouldApplyTo
方法:
// @since 3.1 专门处理HandlerMethod类型是HandlerMethod类型的异常
public abstract class AbstractHandlerMethodExceptionResolver extends AbstractHandlerExceptionResolver {
// 只处理HandlerMethod这种类型的处理器抛出的异常~~~~~~
@Override
protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
if (handler == null) {
return super.shouldApplyTo(request, null);
} else if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 可以看到最终getBean表示最终哪去验证的是它所在的Bean类,而不是方法本身
// 所以异常的控制是针对于Controller这个类的~
handler = handlerMethod.getBean();
return super.shouldApplyTo(request, handler);
} else {
return false;
}
}
@Override
@Nullable
protected final ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
return doResolveHandlerMethodException(request, response, (HandlerMethod) handler, ex);
}
@Nullable
protected abstract ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception ex);
}
此抽象类非常简单:规定了只处理HandlerMethod
抛出的异常。
ExceptionHandlerMethodResolver
(重要)
它是一个会在Class及Class的父类中找出带有@ExceptionHandler
注解的类,该类带有key为Throwable
,value为Method
的缓存属性,提供匹配效率。
// @since 3.1
public class ExceptionHandlerMethodResolver {
// A filter for selecting {@code @ExceptionHandler} methods.
public static final MethodFilter EXCEPTION_HANDLER_METHODS = method -> AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);
// 两个缓存:key:异常类型 value:目标方法Method
private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16);
private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = new ConcurrentReferenceHashMap<>(16);
// 唯一构造函数
// detectExceptionMappings:传入method,找到这个Method可以处理的所有的异常类型们(注意此方法的逻辑)
// addExceptionMapping:把异常类型和Method缓存进mappedMethods里
public ExceptionHandlerMethodResolver(Class<?> handlerType) {
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
addExceptionMapping(exceptionType, method);
}
}
}
// 找到此Method能够处理的所有的异常类型
// 1、detectAnnotationExceptionMappings:本方法或者父类的方法上标注有ExceptionHandler注解,然后读取出其value值就是它能处理的异常们
// 2、若value值木有指定,那所有的方法入参们的异常类型,就是此方法能够处理的所有异常们
// 3、若最终还是空,那就抛出异常:No exception types mapped to " + method
private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {
List<Class<? extends Throwable>> result = new ArrayList<>();
detectAnnotationExceptionMappings(method, result);
if (result.isEmpty()) {
for (Class<?> paramType : method.getParameterTypes()) {
if (Throwable.class.isAssignableFrom(paramType)) {
result.add((Class<? extends Throwable>) paramType);
}
}
}
if (result.isEmpty()) {
throw new IllegalStateException("No exception types mapped to " + method);
}
return result;
}
// 对于添加方法一样有一句值得说的:
// 若不同的Method表示可以处理同一个异常,那是不行的:"Ambiguous @ExceptionHandler method mapped for ["
// 注意:此处必须是同一个异常(比如Exception和RuntimeException不属于同一个...)
private void addExceptionMapping(Class<? extends Throwable> exceptionType, Method method) {
Method oldMethod = this.mappedMethods.put(exceptionType, method);
if (oldMethod != null && !oldMethod.equals(method)) {
throw new IllegalStateException("Ambiguous @ExceptionHandler method mapped for [" + exceptionType + "]: {" + oldMethod + ", " + method + "}");
}
}
// 给指定的异常exception匹配上一个Method方法来处理
// 若有多个匹配上的:使用ExceptionDepthComparator它来排序。若木有匹配的就返回null
@Nullable
public Method resolveMethod(Exception exception) {
return resolveMethodByThrowable(exception);
}
// @since 5.0 递归到了couse异常类型 也会处理
@Nullable
public Method resolveMethodByThrowable(Throwable exception) {
Method method = resolveMethodByExceptionType(exception.getClass());
if (method == null) {
Throwable cause = exception.getCause();
if (cause != null) {
method = resolveMethodByExceptionType(cause.getClass());
}
}
return method;
}
//1、先去exceptionLookupCache找,若匹配上了直接返回
// 2、再去mappedMethods这个缓存里找。很显然可能匹配上多个,那就用ExceptionDepthComparator排序匹配到一个最为合适的
// 3、匹配上后放进缓存`exceptionLookupCache`,所以下次进来就不需要再次匹配了,这就是缓存的效果
// ExceptionDepthComparator的基本理论上:精确匹配优先(按照深度比较)
@Nullable
public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
Method method = this.exceptionLookupCache.get(exceptionType);
if (method == null) {
method = getMappedMethod(exceptionType);
this.exceptionLookupCache.put(exceptionType, method);
}
return method;
}
}
对于本类的功能,可总结如下:
- 找到指定Class类(可能是Controller本身,也可能是
@ControllerAdvice
)里面所有标注有@ExceptionHandler
的方法们 - 同一个Class内,不能出现同一个(注意理解同一个的含义)异常类型被多个Method处理的情况,否则抛出异常:Ambiguous @ExceptionHandler method mapped for …,相同异常类型处在不同的Class内的方法上是可以的,比如常见的一个在Controller内,一个在@ControllerAdvice内~
提供缓存: -
mappedMethods
:每种异常对应的处理方法(直接映射代码上书写的异常-方法映射) -
exceptionLookupCache
:经过按照深度逻辑精确匹配上的Method方法
既能处理本身的异常,也能够处理getCause()导致的异常
ExceptionDepthComparator
的匹配逻辑是按照深度匹配。比如发生的是NullPointerException
,但是声明的异常有Throwable和Exception,这是它会根据异常的最近继承关系找到继承深度最浅的那个异常,即Exception。
ExceptionHandlerExceptionResolver
(重要)
该子类实现就是用于处理标注有@ExceptionHandler
注解的HandlerMethod
方法的,是@ExceptionHandler
功能的实现部分。请注意命名上和ExceptionHandlerMethodResolver
做区分
// @since 3.1
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver implements ApplicationContextAware, InitializingBean {
// 这个熟悉:用于处理方法入参的(比如支持入参里可写HttpServletRequest等等)
@Nullable
private List<HandlerMethodArgumentResolver> customArgumentResolvers;
@Nullable
private HandlerMethodArgumentResolverComposite argumentResolvers;
// 用于处理方法返回值(ModelAndView、@ResponseBody、@ResponseStatus等)
@Nullable
private List<HandlerMethodReturnValueHandler> customReturnValueHandlers;
@Nullable
private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
// 消息处理器和内容协商管理器
private List<HttpMessageConverter<?>> messageConverters;
private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
// 通知(因为异常是可以做全局效果的)
private final List<Object> responseBodyAdvice = new ArrayList<>();
@Nullable
private ApplicationContext applicationContext;
// 缓存:异常类型对应的处理器
// 它缓存着Controller本类,对应的异常处理器(多个@ExceptionHandler)~~~~
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap<>(64);
// 它缓存ControllerAdviceBean对应的异常处理器(@ExceptionHandler)
private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache = new LinkedHashMap<>();
// 唯一构造函数:注册上默认的消息转换器
public ExceptionHandlerExceptionResolver() {
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
...
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}
... // 省略所有的get/set方法
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBodyAdvice beans
// 这一步骤同RequestMappingHandlerAdapter#initControllerAdviceCache
// 目的是找到项目中所有的`ResponseBodyAdvice`,然后缓存起来。
// 并且把它里面所有的标注有@ExceptionHandler的方法都解析保存起来
// exceptionHandlerAdviceCache:每个advice切面对应哪个ExceptionHandlerMethodResolver(含多个@ExceptionHandler处理方法)
//并且,并且若此Advice还实现了接口:ResponseBodyAdvice。那就还可干预到异常处理器的返回值处理上(基于body)
//可见:若你想干预到异常处理器的返回值body上,可通过ResponseBodyAdvice来实现哟~~~~~~~~~
// 可见ResponseBodyAdvice连异常处理方法也是生效的,但是`RequestBodyAdvice`可就木有啦。
initExceptionHandlerAdviceCache();
// 注册默认的参数处理器。支持到了@SessionAttribute、@RequestAttribute
// ServletRequest/ServletResponse/RedirectAttributes/ModelMethod等等(当然你还可以自定义)
if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
// 支持到了:ModelAndView/Model/View/HttpEntity/ModelAttribute/RequestResponseBody
// ViewName/Map等等这些返回值 当然还可以自定义
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
...
// 处理HandlerMethod类型的异常。它的步骤是找到标注有@ExceptionHandler匹配的方法
// 然后执行此方法来处理所抛出的异常
@Override
@Nullable
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
// 这个方法是精华,是关键。它最终返回的是一个ServletInvocableHandlerMethod可执行的方法处理器
// 也就是说标注有@ExceptionHandler的方法最终会成为它
// 1、本类能够找到处理方法,就在本类里找,找到就返回一个ServletInvocableHandlerMethod
// 2、本类木有,就去ControllerAdviceBean切面里找,匹配上了也是欧克的
// 显然此处会判断:advice.isApplicableToBeanType(handlerType) 看此advice是否匹配
// 若两者都木有找到,那就返回null。这里的核心其实是ExceptionHandlerMethodResolver这个类
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
if (exceptionHandlerMethod == null) {
return null;
}
// 给该执行器设置一些值,方便它的指定(封装参数和处理返回值)
if (this.argumentResolvers != null) {
exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
}
...
// 执行此方法的调用(比couse也传入进去了)
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod);
... // 下面处理model、ModelAndView、view等等。最终返回一个ModelAndView
// 这样异常梳理完成。
}
对它的功能,总结如下:
-
@ExceptionHandler
的处理和执行是由本类完成的,同一个Class上的所有@ExceptionHandler
方法对应着同一个ExceptionHandlerExceptionResolver
,不同Class上的对应着不同的~ - 标注有
@ExceptionHandler
的方法入参上可写:具体异常类型、ServletRequest/ServletResponse/RedirectAttributes/ModelMethod
等等,注意:入参写具体异常类型时只能够写一个类型。(若有多种异常,请写公共父类,你再用instanceof
来辨别,而不能直接写多个) - 返回值可写:
ModelAndView/Model/View/HttpEntity/ModelAttribute/RequestResponseBody/@ResponseStatus
等等 -
@ExceptionHandler
只能标注在方法上。既能标注在Controller本类内的方法上(只对本类生效),也可配合@ControllerAdvice
一起使用(对全局生效) - 对步骤4的两种情况,执行时的匹配顺序如下:优先匹配本类(本Controller),再匹配全局的。
- 有必要再强调一句:
@ExceptionHandler
方式并不是只能返回JSON串,步骤4也说了,它返回一个ModelAndView也是ok的
异常处理优先级
在实际生成环境中,我们的项目中一般确实也会存在多个HandlerExceptionResolver
异常处理器,那么对于抛出的一个异常,它的处理顺序到底是怎样的呢?
理解了DispatcherServlet
默认注册的异常处理器们和它们的执行原理后,再去解答这个问题就易如反掌了。这是DispatcherServlet
默认注册的异常处理器们:
所以在我们没有自定义HandlerExceptionResolver
来干扰这种顺序的情况下(绝大部分情况下我们都不会干扰它),最最最最先执行的便是@ExceptionHandler
方式的异常处理器,只有匹配不上才会继续执行其它的处理器。根据此规律,我从使用层面总结出一个结论:
-
@Controller + @ExceptionHandler
优先级最高 -
@ControllerAdvice + @ExceptionHandler
次之 -
HandlerExceptionResolver
最后(一般是DefaultHandlerExceptionResolver
)
全局异常示例
在很多Spring MVC
项目中你或许都可以看到一个名字叫GlobalExceptionHandler
(名字大同小异)的类,它的作用一般被标注上了@ControllerAdvice/@RestControllerAdvice
用于处理全局异常。
大多数项目对此类的设计是相当不完善的,它只做了一个通用处理:处理Exception
类型。显然这种宽泛的处理是很不优雅的,理应做细分。
@Slf4j
@RestControllerAdvice // 全部返回JSON格式,因为大都是REST项目
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
// 处理所有不可知的异常,作为全局的兜底
@ExceptionHandler(Exception.class)
AppResponse handleException(Exception e){
log.error(e.getMessage(), e);
AppResponse response = new AppResponse();
response.setFail("未知错误,操作失败!");
return response;
}
// 处理所有业务异常(一般为手动抛出)
@ExceptionHandler(BusinessException.class)
AppResponse handleBusinessException(BusinessException e){
log.error(e.getMessage(), e);
AppResponse response = new AppResponse();
response.setFail(e.getMessage());
return response;
}
// 处理所有接口参数的数据验证异常(此处特殊处理了这个异常)
@ExceptionHandler(MethodArgumentNotValidException.class)
AppResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e){
log.warn(e.getMessage(), e); //此处我不建议使用error异常...
// 关于校验的错误信息的返回,此处我知识简单处理,具体你可以加强
AppResponse response = new AppResponse();
response.setFail(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
return response;
}
// 自己定制化处理HttpRequestMethodNotSupportedException这个异常类型喽
@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
log.warn(ex.getMessage());
String method = ex.getMethod();
String[] supportedMethods = ex.getSupportedMethods();
Map<String, Object> map = new HashMap<>();
map.put("code", status.value());
map.put("message", "不支持的请求类型:" + method + ",支持的请求类型:" + Arrays.toString(supportedMethods));
return super.handleExceptionInternal(ex, map, headers, status, request);
}
}
有人并不清楚为何我要继承ResponseEntityExceptionHandler
这个类,下面我就简单介绍一下它。
ResponseEntityExceptionHandler
它是个抽象类,可谓是Spring 3.2后对REST应用异常支持的一个暖心举动。它包装了各种Spring MVC在处理请求时可能抛出的异常的处理,处理结果都是封装成一个ResponseEntity
对象。通过ResponseEntity我们可以指定需要响应的状态码、header和body等信息
因为它是个抽象类,所以我们要使用它只需要定义一个标注有@ControllerAdvice
的类继承于它便可(如上示例):
加上全局处理前(被DefaultHandlerExceptionResolver
处理的结果):
若你是REST应用,可以在全局异常处理类上都设计为继承自此类,做兜底使用。它能处理的异常类型如下(同DefaultHandlerExceptionResolver
处理的异常类型):
ResponseEntityExceptionHandler:
@ExceptionHandler({
HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class,
HttpMediaTypeNotAcceptableException.class,
MissingPathVariableException.class,
MissingServletRequestParameterException.class,
ServletRequestBindingException.class,
ConversionNotSupportedException.class,
TypeMismatchException.class,
HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
MethodArgumentNotValidException.class,
MissingServletRequestPartException.class,
BindException.class,
NoHandlerFoundException.class,
AsyncRequestTimeoutException.class
})
@Nullable
public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception { ... }
处理异常时又发生了异常怎么办呢
若处理器内部又抛出异常,一般就会交给tomcat处理把异常栈输出到前端,显示非常不友好的页面。因此:请务必保证你的异常处理程序中不要出现任何异常,保证健壮性。(当然最最最最为兜底的方案就是架构师统一设计一个HandlerExceptionResolver
放在末位,用最简单、最不会出bug的代码来处理一切前面不能处理的异常)
如何优雅统一处理Filter异常
因为我们无法通过@ControllerAdvice+@ExceptionHandler
的方式去处理Filter过滤器抛出的异常(理由希望读者自己能明白),所以此处我提供较为优雅的处理方式作为参考。
传统Spring MVC
- catch住Filter所有异常
- 把
Exception
放进请求attr属性里 - 把该请求forward转发到专门处理错误的Controller里
- 该Controller里拿出异常throw出去,从而便可交给全局异常统一处理了
@Component("helloFilter")
@WebFilter(urlPatterns = "/*")
public class HelloFilter extends OncePerRequestFilter {
@Override
protected void initFilterBean() throws ServletException {
System.out.println("HelloFilter初始化...");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
System.out.println(1 / 0);
filterChain.doFilter(request, response);
} catch (Exception e) { // 捕获所有异常做转发用
request.setAttribute(ErrorController.EXCEPTION_ATTR, e);
request.getRequestDispatcher(ErrorController.ERROR_URL).forward(request, response);
}
}
}
ErrorController
@Slf4j
@RestController
public class ErrorController {
public static final String ERROR_URL = "/do/filter/errors";
public static final String EXCEPTION_ATTR = ErrorController.class.getName() + ".error";
/**
* 把Filter里的异常同意交给全局异常处理
*/
@GetMapping(value = "/do/filter/errors")
public void doFilterErrors(HttpServletRequest request) throws Exception {
throw Exception.class.cast(request.getAttribute(EXCEPTION_ATTR));
}
}
GlobalExceptionHandler
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
// 处理所有不可知的异常,作为全局的兜底
@ExceptionHandler(Exception.class)
Object handleException(Exception e) {
log.error(e.getMessage(), e);
return "hello error";
}
}
Spring Boot
本文针对性的特别提出了SpringBoot case下的解决方案。因为SpringBoot
它会把所有的异常情况都转换为请求/error
,所以扩展它还是容易些的:
Filter:没必要自己catch了,交给SpringBoot全局处理即可
@Component("helloFilter")
@WebFilter(urlPatterns = "/*")
public class HelloFilter extends OncePerRequestFilter {
@Override
protected void initFilterBean() throws ServletException {
System.out.println("HelloFilter初始化...");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println(1 / 0);
filterChain.doFilter(request, response);
}
}
@RestController
public class MyErrorController extends BasicErrorController {
// 最终使用的是此构造函数,所以魔方着只需要使用它即可
// return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers);
public MyErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties) {
super(errorAttributes, serverProperties.getError());
}
@RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorJson(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
body.put("myErrorType", "this is my diy error");
return new ResponseEntity<>(body, status);
}
}
若你在SpringBoot采用上面Spring MVC方式处理,优先级是更高的。