相信咱们每一个人在SpringMVC开发中,都遇到这样的问题:当咱们的代码正常运行时,返回的数据是咱们预期格式,好比json或xml形式,可是一旦出现了异常(好比:NPE或者数组越界等等),返回的内容确实服务端的异常堆栈信息,从而致使返回的数据不能使客户端正常解析; 很显然,这些并非咱们但愿的结果。html
咱们知道,一个较为常见的系统,会涉及控制层,服务(业务)层、缓存层、存储层以及接口调用等,其中每个环节都不可避免的会遇到各类不可预知的异常须要处理。若是每一个步骤都单独try..catch会使系统显的很杂乱,可读性差,维护成本高;常见的方式就是,实现统一的异常处理,从而将各种异常从各个模块中解耦出来;spring
在Spring中常见的全局异常处理,主要有三种:json
(1)注解ExceptionHandler数组
(2)继承HandlerExceptionResolver接口缓存
(3)注解ControllerAdvice服务器
在后面的讲解中,主要以HTTP错误码:400(请求无效)和500(内部服务器错误)为例,先看一下测试代码以及没有任何处理的返回结果,以下:app
(图1:测试代码)源码分析
(图2:没有异常的错误返回)测试
注解ExceptionHandler做用对象为方法,最简单的使用方法就是放在controller文件中,详细的注解定义再也不介绍。若是项目中有多个controller文件,一般能够在baseController中实现ExceptionHandler的异常处理,而各个contoller继承basecontroller从而达到统一异常处理的目的。由于比较常见,简单代码以下:spa
(图3:Controller中的ExceptionHandler使用)
在返回异常时,添加了所属的类名,便于你们记忆理解。运行看一下结果:
(图4:添加ExceptionHandler以后的结果)
优势:ExceptionHandler简单易懂,而且对于异常处理没有限定方法格式;
缺点:因为ExceptionHandler仅做用于方法,对于多个controller的状况,仅为了一个方法,全部须要异常处理的controller都继承这个类,明明不相关的东西,强行给他们找个爹,不太好。
这里虽然说是ControllerAdvice注解,实际上是其与ExceptionHandler的组合使用。在上文中能够看到,单独使用@ExceptionHandler时,其必须在一个Controller中,然而当其与ControllerAdvice组合使用时就彻底没有了这个限制。换句话说,两者的组合达到的全局的异常捕获处理。
(图5:注解ControllerAdvice异常处理代码)
在运行以前,需将以前Controller中的ExceptionHandler注释掉,测试结果以下:
(图6:注解ControllerAdvice异常处理结果)
经过上面结果能够看到,异常处理确实已经变动为ExceptionHandlerAdvice类。这种方法将全部的异常处理整合到一处,去除了Controller中的继承关系,而且达到了全局捕获的效果,推荐使用此类方式;
HandlerExceptionResolver自己SpringMVC内部的接口,其内部只有resolveException一个方法,经过实现该接口咱们能够达到全局异常处理的目的。
(图7:实现HandlerExceptionResolver接口)
一样在执行以前,将上述两个方法的异常处理都注释掉,运行结果以下:
(图8:实现HandlerExceptionResolver接口运行结果)
能够看到500的异常处理已经生效了,可是400的异常处理却没有生效,而且根没有异常前的返回结果同样。这是怎么回事呢?不是说能够作到全局异常处理的么?没办法要想知道问题的缘由,咱们只能刨根问底,往Spring的祖坟上刨,下面咱们结合Spring的源码调试,去须要缘由。
你们都知道,在Spring中第一个收到请求的类就是DispatcherServlet,而该类中核心的方法就是doDispatch,咱们能够在该类中打断点,进而一步步跟进异常处理。
参照以下的跟进步骤,在processHandlerException中断点,跟踪的结果以下图:
(图9:processHandlerException断点)
能够看到在图中箭头【1】处,在遍历 handlerExceptionResolvers 进而来处理异常,而在箭头【2】处,看到handlerExceptionResolvers 中共有4个元素,其中最后一个就是2.3方法定义的异常处理类
当前的请求query请求,根据上述现象能够推测出,该异常处理应该是在前3个异常处理中被处理了,从而跳过咱们自定义的异常;带着这样的猜想,咱们F8继续跟进,能够跟踪到该异常是被第三个,即DefaultHandlerExceptionResolver所处理。
到此真相大白了,能够看到咱们的自定义类MyHandlerExceptionResolver确实能够作到全局处理异常,只不过对于query请求的异常,中间被DefaultHandlerExceptionResolver插了一脚,因此就跳过了MyHandlerExceptionResolver类的处理,从而出现400的返回结果。而对于calc请求,中间没有阻拦,因此就达到了预期效果。
到此咱们一共介绍了3类全局异常处理,按照上面的分析能够看出,实现HandlerExceptionResolver接口的方式是排在最后处理,那么@ExceptionHandler和@ControllerAdvice这两个的顺序谁先谁后呢? 将三类异常处理所有打开(以前注释掉了),运行一下看看效果:
(图10:异常处理全放开运行结果)
经过现象能够看到,Controller中单独@ExceptionHandle异常处理排在了首位,@ControllerAdvice排在了第二位。严谨的童鞋能够写个Controller02,将query和calc复制过去,异常处理就不要了,这样请求c02的方法时,异常捕获的所属类名就都是@ControllerAdvice所在类了。
以上都是咱们根据现象获得的结论,下面去Spring源码去找“证据”。在图9中,handlerExceptionResolvers中有4类处理器,而@ExceptionHandler和@ControllerAdvice的处理就在第一个ExceptionHandlerExceptionResolver中(以前断点跟进便可获知)。继续跟进直到进入ExceptionHandlerExceptionResolver类的doResolveHandlerMethodException方法,这里的HandlerMethod就是Spring将HTTP请求映射到指定Controller中的方法,而Exception就是须要被捕获的异常;继续跟进,看看使用这两个参数到底干了什么事儿。
(图11:doResolveHandlerMethodException断点)
继续跟进getExceptionHandlerMethod方法,发现有两个变量可能就是问题的关键:exceptionHandlerCache和exceptionHandlerAdviceCache。首先,二者的变量名很值得怀疑;其次,前者在代码中看,明显是经过类做为key,从而获得一个处理器(resolver),这刚好Controller中@ExceptionHandler处理规则相吻合;最后,这两个Cache的处理顺序,也符合以前的获得的结论。正如以前猜想的那样,Spring中确实是优先根据Controller类名去查找对应的ExceptionHandler,没有找到的话,再进行@ControllerAdvice异常处理。
(图12:两个异常处理Cache )
若有兴趣可继续深刻挖掘Spring的源码,这里针对 ExceptionHandlerExceptionResolver 简单作个总结:
exceptionHandlerCache中包含Controller中的ExceptionHandler异常处理,处理时经过HandlerMethod获得Controller,进而再找到异常处理方法,须要注意的是,其是在异常处理过程当中put值的;
exceptionHandlerAdviceCache则是在项目启动时初始化的,大概思路是找到带有@ControllerAdvice注解的bean,从而缓存bean中的ExceptionHandler,在异常处理时须要对齐遍历查找处理,进而达到全局处理的目的。
介绍了这么多,简单画张图总结一下。蓝色的部分是Spring默认添加的3类异常处理器,黄色部分是咱们添加的异常处理以及其所被调用的位置和顺序。看看哪里还有不太清楚的,往回翻翻看(ResponseStatusExceptionResolver是针对@ResponseStatus注解,这里再也不详述)。
(图13:异常总结)
若是有须要将MyHandlerExceptionResolver提早处理,甚至排在ExceptionHandlerExceptionResolver以前,能作到么?答案是确定的,在Spring中若是想将MyHandlerExceptionResolver异常处理提早,须要再实现一个Ordered接口,实现里面的getOrder方法便可,这里返回-1,将其放在最上面,此次咸鱼终于能够翻身了。
(图14:实现Ordered接口)
运行看一下结果是否是符合预期,提醒一下,咱们三个异常处理都是生效的,以下图:
(图15:实现Ordered接口运行结果)
本文主要经过介绍SpringMVC中三类常见的全局异常处理,在调试中发现了问题,进而引起去Spring源码中去探究缘由,最终解决问题,但愿你们能有所收获。固然Spring异常处理类不止介绍的这些,有兴趣的童鞋请自行探索!
[1] http://www.cnblogs.com/fangjian0423/p/springMVC-request-mapping.html
[2]https://blog.csdn.net/mll999888/article/details/77621352
做者:张远航 来源:宜信技术学院