目录
HandlerExceptionResolver干啥的
1.1、前言
1.2、HandlerExceptionResolver组件
1.3、核心方法
二、AbstractHandlerExceptionResolver类
2.1、resolveException方法
2.2、shouldApplyTo方法
三、DefaultHandlerExceptionResolver 类
四、SimpleMappingExceptionResolver类
4.1、示例
4.1.1、xml配置
4.1.2、代码示例
4.1.3、效果展示
4.2、分析
4.2.1、SimpleMappingExceptionResolver概述
4.2.2、doResolveException方法
五、ResponseStatusExceptionResolver类
5.1、示例
5.2、分析
六、ExceptionHandlerExceptionResolver 类
6.1、示例
配置异常解析组件
异常处理类
6.2、分析
6.2.1、shouldApplyTo方法
6.2.2、doResolveHandlerMethodException方法
6.2.3、getExceptionHandlerMethod方法
6.2.4、getExceptionHandlerMethod方法
在了解HandlerExceptionResolver相关组件作用之前,项目难免会有异常,不管是系统交互过程中的常见异常NullPointerException,还在业务自定义的异常处理,对于异常处理常规操作是进行try-catch 包裹 catch中进行异常处理操作。这样代码中充斥着大量的重复try-catch代码,且不符合我们要求,那么对于异常我们想要进行统一的处理,且提供友好的视图信息或者json格式对于这种场景springMVC(Spring 3.2 带来的新特性) 提供了HandlerExceptionResolver组件来帮我们解决这样的问题。
如上问题尤其是web项目,因为它一般直接面向用户,所以良好的异常处理就显得格外的重要。Spring MVC作为如此优秀的web层框架,自然考虑到了这一点,其提供了异常处理器HandlerExceptionResolver。HandlerExceptionResolver主要用于解析request请求进入Controller(Handler)对象处理过程中对于抛出异常的解析处理从上面描述中可以知道其处于springMVC范畴的组件统一异常处理过程中出现的异常和视图解析过程中的异常时无法处理的。
HandlerExceptionResolver 接口有一个抽象方法AbstractHandlerExceptionResolver主要有四大类 HandlerExceptionResolverComposite(包含多个不同HandlerExceptionResolver 排除)
其和 @ExceptionHandler+@ControllerAdvice进行全局的异常处理配合使用进行。下面我们从示例和源码的角度来学习springMVC的九大组件之一的异常处理解析器组件HandlerExceptionResolver。
ExceptionHandlerExceptionResolver只有一个核心方法resolveException,该方法主要作用处理对应请求Handler过程中产生的异常并创建对应的ModelView返回。
/**
* springMVC异常解析的核心方法
* @param request http请求信息
* @param response http响应信息
* @param handler 使用请求url映射的处理请求逻辑的Handler处理器
* @param ex 对应的需要解析的异常父类
* @return 异常解析完成返回的视图对象
*/
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
其对应在springMVC调用的地方为DispatcherServlet的入口类:
org.springframework.web.servlet.DispatcherServlet#processDispatchResult()方法中的某个分支对应异常处理盗用其类processHandlerException()方法。
AbstractHandlerExceptionResolver对接口进行了一个抽象,实现其中的通用逻辑,并暴露对应的doResolveException方法交由子类实现
public ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
//先判断该异常处理类是否支持处理handler执行过程中产生的异常
if (shouldApplyTo(request, handler)) {
//设置response禁用缓存 使用preventResponseCaching(默认是false不经用缓存)
prepareResponse(ex, response);
//交由子类进行真实的异常处理
ModelAndView result = doResolveException(request, response, handler, ex);
if (result != null) {
//打印日志
if (logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
logger.debug("Resolved [" + ex + "]" + (result.isEmpty() ? "" : " to " + result));
}
// Explicitly configured warn logger in logException method.
logException(ex, request);
}
return result;
}
else {
return null;
}
}
抽象类中的实现了异常解析接口的核心方法,提供了一个通用的逻辑处理,1、比如是否支持处理handler对应的异常的shouldApplyTo方法,开发过程可以自定义的配置该异常处理器可以处理的异常(下面给出了源码),2、设置响应信息(这里禁用缓存)3、调用子类实现的doResolveException() 真正解析异常处理,抽象方法子类实现,4、自定义日志输出
其中对于doResolveException方法的分析我们会在后面根据不同的子类进行分别分析。这里按下不表。
使用该方法判断是否支持处理Handler(请求参数) 产生的异常
//该方法使用两个参数来判断是否支持handler异常处理 分别是 mappedHandlers(包含支持handler异常处理的set集合)
//mappedHandlerClasses(包含Handler处理器对应类的class属性) 这两个属性可以在配置解析类的时候自定义设置
protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
//判断mappedHandlers是否包含handler(true表示支持该handler的异常处理)
if (handler != null) {
if (this.mappedHandlers != null && this.mappedHandlers.contains(handler)) {
return true;
}
//判断mappedHandlerClasses是否包含handler对应类的class(true表示支持该handler的异常处理)
if (this.mappedHandlerClasses != null) {
for (Class> handlerClass : this.mappedHandlerClasses) {
if (handlerClass.isInstance(handler)) {
return true;
}
}
}
}
//如果两个属性不设置 则支持所有的异常处理
return (this.mappedHandlers == null && this.mappedHandlerClasses == null);
}
DefaultHandlerExceptionResolver 类实现了抽象类AbstractHandlerExceptionResolver的doResolveException方法主要负责处理一些Http请求常见错误,比如请求方法不支持,请求传参方式不支持等。
在配置中设置了不处理空指针异常,对于ClassCastException异常跳转到classCastExp.jsp页面、IOException跳转到ioExp.jsp,*ServiceException异常跳转到error/index.jsp,对于其他的异常走默认的defaultErrorVIew 对应的error/index.jsp。
java.lang.NullPointerException
error/classCastExp
error/ioExp
error/index
//异常测试 需要注释或者放开指定的代码片段
@RequestMapping(value = "rederException", method = RequestMethod.GET)
@ResponseBody
public String rederException(String type) throws FileNotFoundException,AServiceException, BServiceException, CServiceException {
//1、测试空指针异常
// String nullStr = null;
// nullStr.split(",");
//2、测试ClassCastException 类型转换异常
// nullStr = "13213";
// Map data = new HashMap();
// data.put("key",nullStr);
// Integer num = (Integer)data.get("key");
//测试IO异常
File file = new File("D:\\doc\\需求开发\\信审新增相关字段\\工作.txt");
FileInputStream in = new FileInputStream(file);
//测试异常的模糊匹配
if(type.equals("A")){
throw new AServiceException();
}
if(type.equals("B")){
throw new BServiceException();
}
if(type.equals("C")){
throw new CServiceException();
}
return "helloView";
}
NullPointerException异常
空指针不进行异常处理则异常信息直接暴露到浏览器(不友好)
fileNotFoundException异常会获取到IOExcpetion对应错误页面。
AServiceException、BServiceException、CServiceException异常模糊查找到对应的error/index.jsp页面。
该类可以配置程序出现的异常和对应的视图名,其有三个相关
protected ModelAndView doResolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
//具体根据异常获取视图名的核心操作
String viewName = determineViewName(ex, request);
if (viewName != null) {
//如果查找到则获取视图名对应的响应码
Integer statusCode = determineStatusCode(request, viewName);
if (statusCode != null) {
//将响应码设置到response对象中
applyStatusCodeIfPossible(request, response, statusCode);
}
//根据视图名获取视图并将异常信息放入ModelAndView对象中
return getModelAndView(viewName, ex, request);
}
else {
return null;
}
}
下面我们来分析一下其核心方法determineViewName做了那些操作,先猜测一下无非就是先过滤掉不进行处理的异常,根据具体的Exception通过映射关系获取到对应的视图对象,没找到使用默认视图名然后一些后续的处理。
protected String determineViewName(Exception ex, HttpServletRequest request) {
String viewName = null;
//过滤掉不用处理的异常 通过该excludedExceptions属性判断
if (this.excludedExceptions != null) {
for (Class> excludedEx : this.excludedExceptions) {
if (excludedEx.equals(ex.getClass())) {
return null;
}
}
}
// 从配置好的异常和视图映射关系中查找异常的对应的视图
//判断逻辑是 捕获的异常名 包含exceptionMappings任何一个 如果当前异常没有匹配上
//则递归调用方法 获取其父类异常是否匹配
if (this.exceptionMappings != null) {
viewName = findMatchingViewName(this.exceptionMappings, ex);
}
//无法匹配则使用默认视图.
if (viewName == null && this.defaultErrorView != null) {
if (logger.isDebugEnabled()) {
logger.debug("Resolving to default view '" + this.defaultErrorView + "'");
}
viewName = this.defaultErrorView;
}
return viewName;
}
哈哈和我们猜想一致。
当程序发生异常时,ResponseStatusExceptionResolver异常解释器用来解析@ResponseStatus标注的异常类,并把异常的状态码返回给客户端。DispatcherServlet默认装配了ResponseStatusExceptionResolver 。
1、Xml配置
效果
其实该类的doResolveException方法逻辑很简答,进入该方法后 获取对应的异常是否被@ResponseStatus注解修饰,如果是则获取其中的code作为statusCodes,reason作为响应原因,此处较为简单不贴代码。
接下来到我们的重头戏也是我们在真是业务场景中最使用的异常解析类,其主要使用场景是使用@ExceptionHandler注解触发该异常解析类,同时可以使用@ControllerAdvice注解记性全局异常处理。通过@ControllerAdvice+@ExceptionHandler组合的形式可以无侵入的形式进行统一、灵活、全局的异常处理。
异常处理有局部异常处理使用@ExceptionHandler注解修饰在某些Controller类的方法上,则该异常只局限于该Controller中的所有请求产生的异常,全局异常处理需要设置@ControllerAdvice注解(控制增强器)+@ExceptionHandler 处理所有请求产生的异常,笔者这里是配置了全局异常处理模式
/**
* @ClassName ServiceExceptionHandlerAdvice
* @Desc springMVC全局异常处理器
* @Author xieqx
* @Date 2020/11/28 9:57
* 定义了三个方法分别处理AServiceException、BServiceException、CServiceException
**/
@ControllerAdvice
public class ServiceExceptionHandlerAdvice {
//不使用注解则返回的是对应的视图名
//@ResponseBody
String handleAServiceException(AServiceException e){
//return "AServiceException Deal! 异常码:"+e.getCode()+"异常信息:" + e.getMsg();
return "/error/index";
}
@ExceptionHandler()
@ResponseBody
String handleBServiceException(BServiceException e){
return "BServiceException Deal! 异常码:"+e.getCode()+"异常信息:" + e.getMsg();
}
}
//对应的Controller接口抛出了异常
http://localhost:8081/spsm_war_exploded/exception/rederException?type=A
http://localhost:8081/spsm_war_exploded/exception/rederException?type=B
ExceptionHandlerExceptionResolver类继承自AbstractHandlerMethodExceptionResolver,
该类同时继承我们上面提到的AbstractHandlerExceptionResolver类其中抽象类中重写了shouldApplyTo()方法,在该方法中主要对@RequestMapping修饰的HandlerMethod进行处理,注意不在是Handler类了而是方法,同时重写doResolveException方法里面就一个抽象方法
doResolveHandlerMethodException交由子类ExceptionHandlerExceptionResolver 重写。
@Override
protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
//如果Handler为null 直接交由父类处理
if (handler == null) {
return super.shouldApplyTo(request, null);
}
//不为空的情况 判断是不是HandlerMethod(@RequestMapping等注解修饰的Controller类中的普通方法)
//是method级别的Handler,则需要获取该方法对应的bean对象,交由父类判断是否支持该bean对象的异常处理
else if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
handler = handlerMethod.getBean();
return super.shouldApplyTo(request, handler);
}
//对于不为null且不是HandlerMethod则无法处理
//则表明该异常处理类只能处理HandlerMethod对应的处理器对象
else {
return false;
}
}
doResolveHandlerMethodException方法是我们进行异常处理的核心方法,通篇代码梳理袭来其就像一个简配版本的HandlerAdapter处理逻辑相同,将找到的异常处理方法包装成,ServletInvocableHandlerMethod,设置参数解析组件,返回值处理组件,调用获取视图信息并转换为ModerAndView对象。
//进行异常处理的核心逻辑
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
//先根据异常类型和Handler获取复合条件的ServletInvocableHandlerMethod 是不是很熟悉
//笔者在之前博客介绍HandlerDapter组件的时候讲解过这个类,最终异常处理的方法Method会被包装成
//ServletInvocableHandlerMethod类去进行调用,具体查找进行异常处理方法逻辑比较复杂 设计到局部异常 全局异常
//对于全局异常则需要在初始化的时候进行缓存,便于此时的获取
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
if (exceptionHandlerMethod == null) {
return null;
}
//ServletInvocableHandlerMethod类设置进行解析参数的解析器
if (this.argumentResolvers != null) {
exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
//ServletInvocableHandlerMethod类设置进行调用生成结果的返回值处理解析器
if (this.returnValueHandlers != null) {
exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
//上面涉及到的全局异常缓存,argumentResolvers、returnValueHandlers在初始化afterPropertiesSet方法中进行处理
//创建视图,请求对象象 为了ServletInvocableHandlerMethod调用的时候进行参数解析,响应设置以及视图对象设置
ServletWebRequest webRequest = new ServletWebRequest(request, response);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
//调用异常对应的方法,如果有异常原因cause则会将异常原因也返回
try {
if (logger.isDebugEnabled()) {
logger.debug("Using @ExceptionHandler " + exceptionHandlerMethod);
}
Throwable cause = exception.getCause();
if (cause != null) {
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod);
}
else {
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
}
}
catch (Throwable invocationEx) {
// Any other than the original exception (or its cause) is unintended here,
// probably an accident (e.g. failed assertion or the like).
if (invocationEx != exception && invocationEx != exception.getCause() && logger.isWarnEnabled()) {
logger.warn("Failure in @ExceptionHandler " + exceptionHandlerMethod, invocationEx);
}
// Continue with default processing of the original exception...
return null;
}
//如果响应的为json序列化数据则返回空视图
if (mavContainer.isRequestHandled()) {
return new ModelAndView();
}
//否则从mavContainer获取视图和数据信息转换为ModelAndView返回
else {
ModelMap model = mavContainer.getModel();
HttpStatus status = mavContainer.getStatus();
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status);
mav.setViewName(mavContainer.getViewName());
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
if (model instanceof RedirectAttributes) {
Map flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
return mav;
}
}
在上面的分析中,关于如何根据HandlerMethod和异常类型对象获取对应的异常处理方法包装类的处理方法getExceptionHandlerMethod并没有详细说明,以及对应argumentResolvers、
returnValueHandlers以及全局异常处理类的集合exceptionHandlerAdviceCache初始化数据的
afterPropertiesSet方法在后面我们进行叙述
//根据Method级别Handler以及对应的Exception来获取异常处理方法的包装对象
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
@Nullable HandlerMethod handlerMethod, Exception exception) {
Class> handlerType = null;
if (handlerMethod != null) {
//先获取局部异常处理对象(就是在本Controller类中的@ExceptionHandler对象)
handlerType = handlerMethod.getBeanType();
//先从缓存中获取,没有根据Controller类 创建一个ExceptionHandlerMethodResolver保存到缓存中
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
if (resolver == null) {
//在创建该类的时候会解析handlerType对象中所有使用@ExceptionHandler注解修饰的异常处理发女
resolver = new ExceptionHandlerMethodResolver(handlerType);
this.exceptionHandlerCache.put(handlerType, resolver);
}
//根据异常类型获取其中匹配的异常处理
Method method = resolver.resolveMethod(exception);
//如果获取到了异常处理方法这使用局部异常处理方法处理异常 否则查看全局异常处理
if (method != null) {
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
}
// For advice applicability check below (involving base packages, assignable types
// and annotation presence), use target class instead of interface-based proxy.
//对于对象可以由代理增强情况 获取其真是的对象类型而非代理对象
if (Proxy.isProxyClass(handlerType)) {
handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
}
}
//查找全局异常处理的,其中是从缓存对象exceptionHandlerAdviceCache中获取对应的异常处理方法的
//key 为ControllerAdviceBean全局异常处理Advice类,value为ExceptionHandlerMethodResolver 异常处理方法获取的解析器
//全局获取到后直接返回,否则不进行异常处理
for (Map.Entry entry : this.exceptionHandlerAdviceCache.entrySet()) {
ControllerAdviceBean advice = entry.getKey();
if (advice.isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = entry.getValue();
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
}
}
}
return null;
}
在该方法中主要详细介绍了如何根据异常信息获取到匹配的异常处理方法,先查找局部异常处理方法,否则查询全局异常处理方法,其中涉及到了一个新的对象ExceptionHandlerMethodResolver该类的作用是根据异常获取到匹配的异常处理方法,判断方法是不是异常处理方法是判断方法是否使用了@ExceptionHandler注解修饰,如果使用该注解修饰则说明该方法是一个异常处理方法。
上面使用的argumentResolvers、returnValueHandlers以及全局异常处理类的集合exceptionHandlerAdviceCache初始化逻辑
public void afterPropertiesSet() {
//初始化全局异常处理类,spring容器中所有使用注解@ControllerAdvice修饰的bean实例均属于
initExceptionHandlerAdviceCache();
//初始化参数解析组件
if (this.argumentResolvers == null) {
List resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
//初始化方法执行返回组件
if (this.returnValueHandlers == null) {
List handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}