HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

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

评论

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

简化管理,提升效率:统一数据视角的力量

可观测技术

数据分析

蓝易云 - nodejs发布静态https服务器

百度搜索:蓝易云

云计算 运维 https Node 云服务器

Compressor for Mac(视频转码编辑工具) v4.7中文激活版

Mac相关知识分享

Agisoft Metashape Professional for Mac(专业3D建模软件)中文版

Mac相关知识分享

不容错过的 CentOS 迁移替换专场!分享安全保障、最佳案例等技术 | 龙蜥大讲堂

OpenAnolis小助手

centos 操作系统 龙蜥大讲堂 CentOS迁移替换

淘宝商品详情API:商品关联推荐算法的解读

技术冰糖葫芦

API 安全 API 文档 API 测试 API 优先

永劫光遇等40+鸿蒙原生游戏首次亮相CJ 2024 技术赋能精品游戏体验

最新动态

观测云:构筑数字化时代的IT监控堡垒

可观测技术

监控

GPT-4o版「Her」终于来了!英伟达股价两周内下跌23%!|AI日报

可信AI进展

人工智能

数据特征采样在 MySQL 同步一致性校验中的实践

快乐非自愿限量之名

MySQL 数据库

焱融全闪存储助力视源垂直大模型训练效率提升 6 倍

焱融科技

碳课堂|什么是碳盘查、碳核查?

AMT企源

碳管理 碳核算

开源与商业的平衡:观测云的开放策略

可观测技术

开源

体育直播平台开发与运营:差异化与选择跟随战略的实践案例

软件开发-梦幻运营部

LeetCode题解:2073. 买票需要的时间,直接计算,JavaScript,详细注释

Lee Chen

《清远折叠》,数智广东第一个SPN政务专网故事

脑极体

通信

喜报 | 极限科技入选北京市 2024 年第一批科技中小企业名单

极限实验室

蓝易云 - 跨境电商SEO网站被黑了,如何做好安全防御?

百度搜索:蓝易云

云计算 运维 云服务器 跨境电商 独立站

JPA乐观锁改悲观锁遇到的一些问题与思考

不在线第一只蜗牛

数据库 oracle 乐观锁 jap

基于“日志审计应用”的 DNS 日志洞察实践

阿里巴巴云原生

阿里云 云原生

fx框架上手-进阶篇

FunTester

Linux内存不够了?看看如何开启虚拟内存增加内存使用量

快乐非自愿限量之名

Java 数据库 Linux

首届「中国可观测日」圆满落幕

观测云

可观测性

产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像

OpenAnolis小助手

操作系统 AI容器镜像 AC2

ai写作ppt软件有哪些?这5款AI生成工具值得一试!

彭宏豪95

效率工具 职场 科技 办公软件 AI生成PPT

LLM智能体工程落地思考

数由科技

人工智能 低代码 数据科学 LLM AI Agent

蓝易云 - 解决SpringBoot项目war部署到tomcat下无法Nacos中注册服务问题

百度搜索:蓝易云

云计算 运维 Spring Boot 云服务器 服务器租用

观测云:技术栈兼容性助力企业数字化转型

可观测技术

技术栈

数据虚拟化和传统数据集成方式(如 ETL)有何区别?

Aloudata

Data 数据虚拟化 数据编织 逻辑数据平台

如何在Python中使用网页抓取API获得Google搜索结果

幂简集成

Python API

结束了~

XIAOJUSURVEY

开源 程序员 代码 贡献者 活动运营

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