QCon 演讲火热征集中,快来分享技术实践与洞见! 了解详情
写点什么

深入理解 Spring 异常处理

  • 2019-04-19
  • 本文字数:3661 字

    阅读完需:约 12 分钟

深入理解Spring异常处理

1.前言

相信我们每个人在 SpringMVC 开发中,都遇到这样的问题:当我们的代码正常运行时,返回的数据是我们预期格式,比如 json 或 xml 形式,但是一旦出现了异常(比如:NPE 或者数组越界等等),返回的内容却是服务端的异常堆栈信息,从而导致返回的数据不能使客户端正常解析; 很显然,这些并不是我们希望的结果。


我们知道,一个较为常见的系统,会涉及控制层,服务(业务)层、缓存层、存储层以及接口调用等,其中每一个环节都不可避免的会遇到各种不可预知的异常需要处理。如果每个步骤都单独 try…catch 会使系统显的很杂乱,可读性差,维护成本高;常见的方式就是,实现统一的异常处理,从而将各类异常从各个模块中解耦出来;

2.常见全局异常处理

在 Spring 中常见的全局异常处理,主要有三种:


(1)注解 ExceptionHandler


(2)继承 HandlerExceptionResolver 接口


(3)注解 ControllerAdvice


在后面的讲解中,主要以 HTTP 错误码:400(请求无效)和 500(内部服务器错误)为例,先看一下测试代码以及没有任何处理的返回结果,如下:



图 1:测试代码



图 2:没有异常的错误返回

2.1 注解 ExceptionHandler

注解 ExceptionHandler 作用对象为方法,最简单的使用方法就是放在 controller 文件中,详细的注解定义不再介绍。如果项目中有多个 controller 文件,通常可以在 baseController 中实现 ExceptionHandler 的异常处理,而各个 contoller 继承 basecontroller 从而达到统一异常处理的目的。因为比较常见,简单代码如下:



图 3:Controller 中的 ExceptionHandler 使用


在返回异常时,添加了所属的类名,便于大家记忆理解。运行看一下结果:



图 4:添加 ExceptionHandler 之后的结果


  • 优点:ExceptionHandler 简单易懂,并且对于异常处理没有限定方法格式;

  • 缺点:由于 ExceptionHandler 仅作用于方法,对于多个 controller 的情况,仅为了一个方法,所有需要异常处理的 controller 都继承这个类,明明不相关的东西,强行给他们找个爹,不太好。

2.2 注解 ControllerAdvice

这里虽说是 ControllerAdvice 注解,其实是其与 ExceptionHandler 的组合使用。在上文中可以看到,单独使用 @ExceptionHandler 时,其必须在一个 Controller 中,然而当其与 ControllerAdvice 组合使用时就完全没有了这个限制。换句话说,二者的组合达到的全局的异常捕获处理。



图 5:注解 ControllerAdvice 异常处理代码


在运行之前,需将之前 Controller 中的 ExceptionHandler 注释掉,测试结果如下:



图 6:注解 ControllerAdvice 异常处理结果


通过上面结果可以看到,异常处理确实已经变更为 ExceptionHandlerAdvice 类。这种方法将所有的异常处理整合到一处,去除了 Controller 中的继承关系,并且达到了全局捕获的效果,推荐使用此类方式;

2.3 实现 HandlerExceptionResolver 接口

HandlerExceptionResolver 本身 SpringMVC 内部的接口,其内部只有 resolveException 一个方法,通过实现该接口我们可以达到全局异常处理的目的。



图 7:实现 HandlerExceptionResolver 接口


同样在执行之前,将上述两个方法的异常处理都注释掉,运行结果如下:



图 8:实现 HandlerExceptionResolver 接口运行结果


可以看到 500 的异常处理已经生效了,但是 400 的异常处理却没有生效,并且根没有异常前的返回结果一样。这是怎么回事呢?不是说可以做到全局异常处理的么?没办法要想知道问题的原因,我们只能刨根问底,往 Spring 的祖坟上刨,下面我们结合 Spring 的源码调试,去需要原因。

3.Spring 中异常处理源码分析

大家都知道,在 Spring 中第一个收到请求的类就是 DispatcherServlet,而该类中核心的方法就是 doDispatch,我们可以在该类中打断点,进而一步步跟进异常处理。

3.1 HandlerExceptionResolver 实现类处理流程

参照如下的跟进步骤,在 processHandlerException 中断点,跟踪的结果如下图:




图 9:processHandlerException 断点


可以看到在图中箭头【1】处,在遍历 handlerExceptionResolvers 进而来处理异常,而在箭头【2】处,看到 handlerExceptionResolvers 中共有 4 个元素,其中最后一个就是 2.3 方法定义的异常处理类


当前的请求 query 请求,根据上述现象可以推测出,该异常处理应该是在前 3 个异常处理中被处理了,从而跳过我们自定义的异常;带着这样的猜测,我们 F8 继续跟进,可以跟踪到该异常是被第三个,即 DefaultHandlerExceptionResolver 所处理。


  • DefaultHandlerExceptionResolver:SpringMVC 默认装配了 DefaultHandlerExceptionResolver,该类的 doResolveException 方法中主要对一些特殊的异常进行处理,并将这类异常转换为相应的响应状态码。而 query 请求触发的异常为 MissingServletRequestParameterException,其恰好也是被 DefaultHandlerExceptionResolver 所针对的异常,故会在该类中被异常捕获。


到此真相大白了,可以看到我们的自定义类 MyHandlerExceptionResolver 确实可以做到全局处理异常,只不过对于 query 请求的异常,中间被 DefaultHandlerExceptionResolver 插了一脚,所以就跳过了 MyHandlerExceptionResolver 类的处理,从而出现 400 的返回结果。而对于 calc 请求,中间没有阻拦,所以就达到了预期效果。

3.2 三类异常的处理顺序

到此我们一共介绍了 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,在异常处理时需要对齐遍历查找处理,进而达到全局处理的目的。

3.3 咸鱼翻身

介绍了这么多,简单画张图总结一下。蓝色的部分是 Spring 默认添加的 3 类异常处理器,黄色部分是我们添加的异常处理以及其所被调用的位置和顺序。看看哪里还有不太清楚的,往回翻翻看(ResponseStatusExceptionResolver 是针对 @ResponseStatus 注解,这里不再详述)。



图 13:异常总结


如果有需要将 MyHandlerExceptionResolver 提前处理,甚至排在 ExceptionHandlerExceptionResolver 之前,能做到么?答案是肯定的,在 Spring 中如果想将 MyHandlerExceptionResolver 异常处理提前,需要再实现一个 Ordered 接口,实现里面的 getOrder 方法即可,这里返回-1,将其放在最上面,这次咸鱼终于可以翻身了。



图 14:实现 Ordered 接口


运行看一下结果是不是符合预期,提醒一下,我们三个异常处理都是生效的,如下图:



图 15:实现 Ordered 接口运行结果

4.总结

本文主要通过介绍 SpringMVC 中三类常见的全局异常处理,在调试中发现了问题,进而引发去 Spring 源码中去探究原因,最终解决问题,希望大家能有所收获。当然 Spring 异常处理类不止介绍的这些,有兴趣的童鞋请自行探索!

参考链接:

[1] http://www.cnblogs.com/fangjian0423/p/springMVC-request-mapping.html


[2] https://blog.csdn.net/mll999888/article/details/77621352


2019-04-19 08:009158

评论

发布
暂无评论
发现更多内容

redis数据类型以及它们的底层实现数据结构

想要飞的猪

LeetCode-448. 找到所有数组中消失的数字(Java)

bug菌

9月日更 Leet Code 9月月更

【JS】两种实现-懒加载的方式-附无限滚动案例

Sam9029

JavaScript 前端 懒加载 9月月更

云渲染为设计行业带来哪些福利?

3DCAT实时渲染

2022 WAIC 闭幕,融云提供分论坛元宇宙直播技术支持

融云 RongCloud

直播 元宇宙

「趣学前端」今日祝福不限量,批量导入在路上

叶一一

前端 设计思维 9月月更

全球新兴市场移动应用报告

易观分析

移动应用

Nodejs安装及环境配置

nodejs NVM 9月月更

使用 Angular Transfer State 的一个具体例子

汪子熙

typescript 前端开发 angular Web Component 9月月更

LeetCode-316. 去除重复字母&&1081.不同字符的最小子序列(Java实现)

bug菌

9月日更 Leet Code 9月月更

C++学习------cmath头文件的源码学习02

桑榆

c++ 9月月更

【C语言深度剖析】重点详解函数的形参和实参、传值和传址

Albert Edison

开发语言 传值 C语音 9月月更 传址

【JS】防抖与节流---在定义时返回的是回调函数

Sam9029

JavaScript 前端 防抖节流 9月月更

库调多了,都忘了最基础的概念 《锁与线程2终结篇》

知识浅谈

线程 9月月更

支撑全产业AI,需要怎样的算力服务?

脑极体

Web & Electron 平台即时通讯产品的技术选型

融云 RongCloud

Web Electron 即时通讯

社招前端二面常见面试题

coder2028

JavaScript 前端

2022-09-09:给定一个正整数 n,返回 连续正整数满足所有数字之和为 n 的组数 。 示例 1: 输入: n = 5 输出: 2 解释: 5 = 2 + 3,共有两组连续整数([5],[2,

福大大架构师每日一题

算法 rust 福大大

Python 教程之数据分析(7)—— Jupyter Notebook 入门

海拥(haiyong.site)

Python 9月月更

这个中秋,国潮元宇宙的A新玩法是……?

文心大模型

学人工智能难吗?怎么用它赚到钱?

felix

人工智能 赚钱 副业赚钱 算法

当代用电行为大赏:有人心疼电费,有人靠屋顶光伏“理财”

脑极体

数据治理(十二):Ranger2.1.0源码编译

Lansonli

数据治理 9月月更

ERP是什么?

优秀

ERP

为什么低代码和专业代码走向融合才能破解低代码困境?

牛刀专业低代码

低代码 低代码平台

哈希索引

急需上岸的小谢

9月月更

Java进阶(十)tomcat中context配置

No Silver Bullet

tomcat Context 9月月更

Python 教程之变量(1)—— 变量、表达式、条件和函数

海拥(haiyong.site)

Python 9月月更

Ansible如何使用lookup插件模板化外部数据

山河已无恙

ansible 9月月更

Java进阶(十三)servlet监听器

No Silver Bullet

Java Servlet 9月月更

「工作小记」多个页面的相似操作公共化设计方案

叶一一

前端 设计思维 9月月更

深入理解Spring异常处理_编程语言_张远航_InfoQ精选文章