AICon 上海站|日程100%上线,解锁Al未来! 了解详情
写点什么

深入理解 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:009210

评论

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

[Mac/win数据库最新版]Navicat for MySQL 永久激活教程

Rose

Navicat for MySQL 数据库管理开发 Navicat数据库软件下载

TikTok海外直播专线:优化你的海外直播体验

Ogcloud

直播 直播优化 TikTok

业务无忧:稳定云虚拟主机让您的在线业务更加顺畅

一只扑棱蛾子

虚拟主机 云虚拟主机

软件测试开发/全日制/测试管理丨用户端 App 自动化测试

测试人

软件测试 自动化测试 测试开发 app自动化测试

医疗机构如何释放数据要素价值 推动数据资产化

用友BIP

数据资产

实时获取建材网商品数据:API实现详解与代码示例

Noah

AE脚本-快速创建微风摇曳摆动波浪动画 Breeze

Rose

AE脚本-图层分布路径形状高级控制 Tweaks

Rose

UV贴图和展开初学者指南

3D建模设计

3D渲染 3D材质编辑 3D材质纹理贴图 UV纹理贴图 UV映射

什么是多边形网格以及如何编辑它?

3D建模设计

3D渲染 3D材质编辑 3D材质纹理贴图 UV纹理贴图 UV映射

如何选择适合自己的外贸独立站域名?

九凌网络

PostgreSQL数据库开发工具Navicat for PostgreSQL中文版

Rose

数据库设计 Navicat for PostgreSQL PostgreSQL数据库开发

MongoDB 数据库管理和开发Navicat for MongoDB【Mac/win】

Rose

MongoDB数据库 Navicat数据库下载 Navicat for MongoDB中文

使用AI搭建SpringBoot服务

X.F

AI Openjdk Java' openai Bard

亚马逊云科技助力施耐德电气加速AI技术在制造场景的落地与创新

财见

UV映射技巧和窍门

3D建模设计

3D渲染 3D材质编辑 3D材质纹理贴图 UV纹理贴图 UV映射

NFTScan | 01.01~01.07 NFT 市场热点汇总

NFT Research

NFT NFT\ NFTScan

了解什么是UV纹理?

3D建模设计

3D渲染 3D材质编辑 3D材质纹理贴图 UV纹理贴图 UV映射

关于 IntelliJ IDEA 中 Schedule for Addition 的问题

Rose

IntelliJ IDEA

软件测试开发/全日制/测试管理丨接口功能测试

测试人

软件测试 自动化测试 接口测试 测试开发 测试管理

OpenAI 也在 996?一位离职员工自白:代码贡献第四,经常工作 6 天丨 RTE 开发者日报 Vol.121

声网

Avdshare Audio Converter for Mac(性能超强的音频格式转换器)

Rose

苹果软件下载 Avdshare Audio Converter Mac音频格式转换器

2023年国内AI Agent下项目大盘点,科技大厂与创业公司齐头并进

王吉伟频道

创业 融资 大语言模型 AI Agent AI智能体

2024年最热门的15个科技工作岗位

互联网工科生

程序员 科技 岗位

阿里云 EMAS & 魔笔:12月产品动态

移动研发平台EMAS

GaussDB(for MySQL)新特性TDE发布:支持透明数据加密

华为云开发者联盟

数据库 后端 华为云 华为云开发者联盟 华为云GaussDB(for MySQL)

软件测试开发/全日制/测试管理丨用户端 Web 自动化测试

测试人

软件测试 自动化测试 测试开发 Web自动化测试 web测试

如何选择适合自己的外贸独立站域名?

九凌网络

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