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等等
- 1. 注意:入参写具体异常类型时只能够写一个类型。(若有多种异常,请写公共父类,你再用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 { ... }
处理异常时又发生了异常怎么办呢?
这个问题你是否曾思考过呢?其实在理解了上文和本文的内容后,此问题的答案也就浮出水面了,强烈建议有兴趣的同学在本地调试出这种case,有助于你的理解~
结论:若处理器内部又抛出异常,一般就会交给tomcat处理把异常栈输出到前端,显示非常不友好的页面。因此:请务必保证你的异常处理程序中不要出现任何异常,保证健壮性。(当然最最最最为兜底的方案就是架构师统一设计一个HandlerExceptionResolver放在末位,用最简单、最不会出bug的代码来处理一切前面不能处理的异常)