web九大组件之---HandlerExceptionResolver异常处理器使用详解【享学Spring MVC】

每篇一句

出世的智者,入世的强者,或者正常而阳光的普通人。

前言

任何程序都会有异常。无论你是做什么项目,对异常的处理都是非常有必要的,尤其是web项目,因为它一般直接面向用户,所以良好的异常处理就显得格外的重要。Spring MVC作为如此优秀的web层框架,自然考虑到了这一点,因此它从首个版本便提供了异常处理器HandlerExceptionResolver,这便是本文的主要议题。

Java异常体系简介

Java相较于其它大多数语言提供了一套非常完善的异常体系Throwable:分为ErrorException两大分支:

  1. Error:错误,对于所有的编译时期的错误以及系统错误都是通过Error抛出的,比如NoClassDefFoundErrorVirtual MachineErrorZipError、硬件问题等等。
  2. Exception:异常,是更为重要的一个分支,是程序员经常打交道的。异常定义为是程序的问题,程序本身是可以处理的。

ErrorException最大的区别是:异常是可以被程序处理的,而错误是没法处理的。

错误是不可查的,因为它们在应用程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况(比如类找不到NoClassDefFoundError

当然喽,异常Exception它本身还分为两大重要的分支:Checked Exception(可检查异常,如IOException)和Unchecked Exception(不可检查异常,如RuntimeException)。这部分不是本文关注的重点,此处只稍微提一下而已。

tips:RuntimeException不仅可以throw,也是可以throws的。只是若它throws的话,它人调用此方法时并不需要强制catch/继续throws(和IOException不同),所以我们一般不这么来用,但是语法上是允许的哦~

为何需要全局异常处理?

在web项目开发时,我们一般把业务代码(大量代码)写在Service层。作为面向返回的Controller层就需要关注一些异常情况了:如此一来,我们的Controller层就不得不进行try-catch,形如这样子:

@GetMapping("/test")
public String test() {
	try {
		... // 处理你的业务逻辑
		return "success";
	} catch (Exception e) {
		return "fail"; // 处理异常
	}
}

显然,这么处理至少有如下两大问题:

  1. Controller一般方法众多,那就需要写大量的try-catch代码,很难看也很难维护
  2. 在此处try-catch也只能捕获住Handler的异常,万一是view抛出异常了呢???

一句话:如果你能够保证你的程序不会出错(没有bug),那么你是不需要全局异常处理的,因为压根就不会发生异常嘛(nnp都不会哦~),很显然这太过于不现实了。
还有一个重要原因:即使你的程序出现了异常(因为避免不了),你总不能把一些只有程序员才能看懂的错误代码抛给用户去看吧,因此展现一个比较友好的错误页面就显得很有必要了,这就是全局异常处理。

我记得滴滴在创业早期出了这么一个"事故":那时滴滴、快的竞争白热化,滴滴司机在APP上提现时竟然弹出:“余额不足”的提示(虽然是真的滴滴账户余额不足了,但你也不能给出这种提示呀),这个提示差点葬送了滴滴的大好前程。从大了来讲,这其实也属于异常处理的范畴咯。

既然异常处理这么重要,那么本文就重点讨论Spring MVC它提供的对异常处理的支持。

古老的异常处理方式

在还没有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 MVC作为现在the most known的Web框架产品,优雅异常处理这块它当然提供了完善的支持。Spring MVC提供处理异常的方式主要分为两种:

  1. 实现HandlerExceptionResolver方式
  2. @ExceptionHandler注解方式。注解方式也有两种用法:
    1. 使用在Controller内部
    2. 配置@ControllerAdvice一起使用实现全局处理

本文作为入门篇,将先聚焦于第一种方式的使用和分析。

HandlerExceptionResolver

它是Spring首个版本就提供了的异常处理器接口,定义也非常的简单:

// @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;
		}
	}
}

此抽象类主要是提供setMappedHandlerssetMappedHandlerClasses让此处理器可以作用在指定类型/处理器上,因此子类只要继承了它都将会有这种能力,这也是为何我推荐自定义实现也继承于它的原因。它提供了shouldApplyTo()方法用于匹配逻辑,子类若想定制化匹配规则,亦可复写此方法。

SimpleMappingExceptionResolver

顾名思义它就是通过简单映射关系来决定由哪个错误视图来处理当前的异常信息。它提供了多种映射关系可以使用:

  1. 通过异常类型Properties exceptionMappings;映射。它的key可以是全类名、短名称,同时还有继承效果:比如key是Exception那将匹配所有的异常。value是view name视图名称
    1. 若有需要,可以配合Class<?>[] excludedExceptions来一起使用
  2. 通过状态码Map<String, Integer> 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默认情况下并没有装配上它(so它几乎处于一个被弃用的状态,基本可忽略)。

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等等~~~~~~~
	}

从应用流程上看是比较简单的,但是了解了此处理流程对我们后续使用、定制会有很好的促进作用。

使用示例

简单的演示一下使用的Demo:

@Controller
@RequestMapping
public class HelloController {

    @ResponseBody
    @GetMapping("/test/exception")
    public int testException(@RequestParam(required = false, defaultValue = "1") Integer value) {
        if (value == 100) {
            // 状态码设置为501 方便识别
            throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED, "服务端发生异常啦");
        }
        return 10 / value;
    }
}

访问以及结果的截图如下:
在这里插入图片描述
异常处理过程分析:

  • value值传0,处理器(HandlerMethod)会抛出计算异常:ArithmeticException(/ by zero)。此异常交给默认注册上去的三个异常处理器进行处理,发现:没有一个HandlerExceptionResolver可以处理这种异常类型,所以经过DispatcherServlet#processHandlerException处理后得到的ModelAndView为null(无异常视图),因此最终的结果是:执行完afterCompletion()方法后,throw ex交给web容器去处理。这就是我们如上截图看到的结果页面,连异常代码都有,极度的不友好有木有。

若value值换位100,这样异常类型能能被处理器处理的,结果如何呢?看下截图:
在这里插入图片描述
异常处理过程分析:

  • 此异常经过ResponseStatusExceptionResolver处理后得到一个ModelAndView,但exMv.isEmpty() = true的,因此异常不会throw出来而是return null。所以最终因为ModelAndView是空的但异常也没throw出来,最终就是把response返回喽(已经设置好返回状态码和错误消息的response),显然这种方式虽然比上面稍好(没有错误代码了),但对用户来说也是不友好的。

备注:本文并不演示最为强大、复杂的ExceptionHandlerExceptionResolver,也就是不演示@ExceptionHandler方式,因为它的详解在此处专文讲解

自定义HandlerExceptionResolver处理异常

上面两个案例都是使用Spring MVC内置的异常处理器,显然用户体验均非常不友好。所以在实际生产环境,必须是需要自己来处理异常(页面)的,下面采用自定义HandlerExceptionResolver方式给出Demo案例,仅供参考:

1、自定义异常处理类并配置上去(本例以匿名内部类方式处理):

@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");
                }
            }
        });
    }
}

2、对应的准备两个错误页面
在这里插入图片描述
3、请求示例截图如下(value=0 or 100):
在这里插入图片描述
在这里插入图片描述

相关阅读

@ExceptionHandler or HandlerExceptionResolver?如何优雅处理全局异常?【享学Spring MVC】

总结

我书写本文的目的主要是让大家"善待异常"和重视异常的处理,我认为一个好的程序员应该对异常的处理都是相对妥当的,而不是坐而不理。我相信你在工作中一定遇到过异常处理不妥当而到处吐槽你的团队的现象,那么看了本文后,动起来吧,重视起来吧,处理起来吧,而不是只剩下抱怨了…

不客气的说一句话:微服务的难点在于它的治理,若你统一了技术栈,规范了各种情况的处理(比如远程调用、异常处理、状态码返回、熔断处理等),它将会让你的开发效率、监控、报警问题、定位成本大大减少。所以从某种程度上,它是服务开发、服务治理非常非常重要的一环,所以"代码架构师"真的是非常重要的,而不是什么都只做事后补救,事前预防或许来得更有效。

当然,本文介绍的方案还是不够的,因为更为重要的@ExceptionHandler方式我放在了下一篇文章单独讲解。在当下Rest流行的生产环境下,使用更加强大、更方便的@ExceptionHandler方式是首选。对于它的解释说明和使用Demo,出门右拐可以给你惊喜。同时此时你是否疑问过这个问题:若异常处理器里面抛出了异常,将会怎样呢?答案也一样的出门右拐吧~

发布了274 篇原创文章 · 获赞 431 · 访问量 36万+

猜你喜欢

转载自blog.csdn.net/f641385712/article/details/101840833