写点什么

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

评论

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

BOE(京东方)联手故宫再度打造沉浸式数字展 引领数字艺术文化新篇章

爱极客侠

接龙活动管理系统(源码+文档+部署+讲解)

深圳亥时科技

1688商品类目API接口的开发应用与收益

科普小能手

数据挖掘 阿里巴巴 电商 API 接口 1688 API

RAG应用在得物开放平台的智能答疑的探索

得物技术

AI 前端

日志服务 SQL 引擎全新升级

阿里巴巴云原生

阿里云 云原生

量子计算遇上人工智能:突破算力瓶颈的关键?

天津汇柏科技有限公司

量子计算 AI 人工智能

乡村振兴数据平台(源码+文档+部署+讲解)

深圳亥时科技

国内“最强”具身智能创始团队,发布首个端到端RL具身模型

机器人头条

大模型 人形机器人 具身智能 机器人大脑

SecureCRT & SecureFX 9.6.1 for macOS, Linux, Windows - 跨平台的多协议终端仿真和文件传输

sysin

securecrt

京东按图搜索商品(拍立淘)API接口系列(京东API)

tbapi

京东API接口 京东图片搜索接口 京东拍立淘接口 京东以图搜货接口

链路诊断最佳实践:1 分钟定位错慢根因

阿里巴巴云原生

阿里云 云原生

某讯一面,有点难度

王中阳Go

数据库 kafka 缓存 面试

让跨 project 联查更轻松,SLS StoreView 查询和分析实践

阿里巴巴云原生

阿里云 云原生

网络分析与监控:阿里云拨测方案解密

阿里巴巴云原生

阿里云 云原生

AI技术实现口语练习功能

北京木奇移动技术有限公司

AI技术 软件外包公司 口语学习

AI技术在英语发音的应用

北京木奇移动技术有限公司

英语学习 AI技术 软件外包公司

AI技术在帮助残障人士的应用

北京木奇移动技术有限公司

AI技术 软件外包公司 AI助残

编译时插桩,Go 应用监控最佳选择

阿里巴巴云原生

阿里云 云原生

o3 发布了,摔碎了码农的饭碗

不在线第一只蜗牛

架构

网页结构建模在低质采集站上的识别应用

百度Geek说

百度 防作弊 大模型、

京东商品列表API接口系列(京东API)

tbapi

京东API接口 关键词搜索京东商品列表 京东商品列表接口

《手把手教你做PC》课程即将启动!深开鸿引领探索KaihongOS笔记本电脑开发实战

科技热闻

阿里巴巴1688 API接口深度解析:商品详情与关键词搜索商品的高效应用与实战代码

代码忍者

房产经纪人管理系统(源码+文档+部署+讲解)

深圳亥时科技

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