准确简单理解 SpringMVC 的异常处理机制
橘长
读完需要
速读仅需 3 分钟
大家好,我是橘长,一个 Java 开发工程师。
用简单且有逻辑的话把一个复杂的技术问题讲清楚是项能力,能力要想形成自然是需要刻意练习,今天和大家聊聊 SpringMVC 的异常处理机制。
/ 一、背景介绍 /
1
一)历史遗留项目
近期接手其他同事遗留下来的一个项目,打开 controller 层代码的时候傻了眼,橘长第一反应是这人是不是疯子,第二反应是自己是不是拉错代码仓库了,第三反应是这人可能是实习生,第四反应是写篇文章记录下这丑陋的代码吧。
为啥觉得这样写非常丑陋?
每一个 api 都加上 try-catch 逻辑,代码重复,其次 Java 的异常处理机制有一个缺陷那就是侵入代码,导致业务代码和异常处理代码耦合,可读性差,在左耳听风读源码篇有一个技巧那就是剔除掉这些异常处理代码,减少干扰。
还有一些场景,比方说不想把数据库层面的异常暴露给用户。
2
二)回顾 Java 异常处理机制
假定我们的系统都处于理想状态下运行,这个想法自然是不可能实现的,那么在代码层面就需要防备和处理错误问题,Java 使用了一种称为异常处理 exception handing 的错误捕获机制,具体的内容可以点击下方文章。
入口:
/ 二、SpringMVC 是如何处理异常的 /
1
一)思路扩展
推荐花上半小时自行查看 SpringMVC 官方文档。
在上述模块中我们认为在每一个 controller 中都加上大量的 try - catch 模板代码会显得很笨,问题抛出来了,我们如何思考解决这个不太好的写法呢?
在整个代码处理流程中,当某个节点抛出异常的时候,它可以通过 try - catch 自行内部处理,catch 中也可以做异常转换,还有一种方式就是自己不处理,向上抛,谁调用谁处理异常,我们来看点代码就理解了,为了演示用,包命名等规范忽略。
private String sendTemplateMsg(JSONObject bodyData) {
String requestUrl = "xxxx";
String responseBody = HttpRequest.post(requestUrl).body(bodyData.toString()).timeout(3000).execute().body();
log.info("xxxxx : [{}]", responseBody);
return responseBody;
}
上述代码实现的业务是调用第三方的 api 发送模板消息,抛个问题,第三方服务挂了这代码会怎么走?可能是 ConnectionTimeoutException,也可能是其他异常,那么如何调整修改呢?
解决方式一:不管,任由它向上抛,最终抛给 JVM 层面,让 JVM 去做异常转换,不建议;
解决方式二:方法内部消化,通过 try - catch 的套路,如下所示
private String sendTemplateMsg(JSONObject bodyData) {
String requestUrl = "xxxx";
try {
String responseBody = HttpRequest.post(requestUrl).body(bodyData.toString()).timeout(3000).execute().body();
log.info("xxxxx : [{}]", responseBody);
return responseBody;
} catch (Exception e) {
// log print
// return null?
// throw new XxxException ?
throw new RuntimeException("调用 xxx 平台 api 异常!");
}
}
这里存在一个值得商榷的问题,异常处理逻辑是返回 null 还是抛出自定义异常,具体可以看一下这篇文章。
入口:
引入一个新的问题,很多时候我们业务开发需要做异常转换,也就是说本来代码抛出的是 A 异常,但为了让我们系统做统一管理,我们需要将 A 异常,转换成 B 异常,比方下列代码,可能捕获的是 ConnectTimeoutException 连接超时异常,但我们需要转换为 XxxPlateformApiException。
那其实还是回到老问题,谁来处理这个异常,什么时机来处理。
2
二)具体解决
Spring 4 实战一书有一段话:不管发生什么事情,不管是好的还是坏的,Servlet 请求的输出都是一个 Servlet 响应,如果在处理请求的时候,出现了异常,那它的输出依然会是 Servlet 异常,异常必须要以某种方式转换为响应。
Spring 底层提供了很多默认机制,比方说服务端处理异常报 500,SpringBoot 会响应如下页面,Spring 底层提供了一种映射处理机制,Spring 框架抛出异常,这个异常处理组件捕获后,根据 key-value 映射自动转换。
2.1
1、解决方法一:@ResponseStatus 修饰自定义异常类
@ResponseStatus(value = HttpStatus.BAD_REQUEST,
reason = "AliPlateformApi Call Exception!")
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class AliPlateformApiException extends RuntimeException {
private static final long serialVersionUID = -7177402586405964501L;
private int code;
private String message;
}
直接用浏览器或 http 工具发起测试,代码和结果如下:
@GetMapping("/demo1/{id}")
public ResponseEntity<?> demo1(@PathVariable Long id) {
if (id <= 0) {
throw new AliPlateformApiException();
}
return ResponseEntity.ok().body("success");
}
很显然我们在 controller 抛出了一个 AliPlateformAPIException 异常,如果不处理给到 JVM 层面,会转换为 500 的状态码,但很显然结果显示是 400,因为我们用 @ResponseStatus 注解做了转换。
继续扩展,会发现 @ResponseStatus 注解的 value 字段要求的类型是 HttpStatus,那么自定义如何处理,利用 extends 继承就好了。
2.2
2、解决方法二:@ExceptionHandler 注解
首先可以点进这个注解的源码,Target 显示是 Method,怎么利用这个来做异常处理呢,在 controller 内部加一个私有方法,@ExceptionHandler 可以指定要处理的异常类型,当业务抛出对应异常的时候,框架会调用这个方法来做处理,进而实现异常处理。
@ExceptionHandler(value = AliPlateformApiException.class)
private ResponseEntity<?> dealAliPlateformApiException() {
log.info("开始处理 AliPlateformAPIException 异常信息!");
return ResponseEntity.badRequest().body("error!");
}
测试的话自行解决即可,继续深挖问题,这种方式有什么好处和坏处,跨 controller 类的话还生效吗?SpringMVC 到底是怎么借助这个组件处理的?处理时机是如何?
关注橘长,后续会慢慢带来源码层面的剖析。
2.3
3、解决方法三:控制器通知
上述两种解决方案其实都有一定的缺陷,那么是否存在更通用的方案呢?Spring 框架从 3.2 版本开始提供了控制器通知的方式。
控制器通知是任意带有 @ControllerAdvice 注解类,这个类会包含一个或多个如下类型的方法(@ExceptionHandler 注解标注的方法、@InitBinder 注解标注的方法、@ModelAttribute 注解标注的方法),简单理解,那就是带有 @ControllerAdvice 注解及其子注解的类是专门用来处理含有另外三类注解注明的方法。
相当于找了一个统一的处理模块来做,类似 Aop 切面的思想,如何做呢,具体代码如下:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AliPlateformApiException.class)
public ResponseEntity<?> dealAliPlateformApiException(AliPlateformApiException aliPlateformApiException) {
// log print why
JSONObject responseData = JSONUtil.createObj().putOnce("code", aliPlateformApiException.getCode()).putOnce("message", aliPlateformApiException.getMessage());
return ResponseEntity.ok().body(responseData);
}
}
测试的话自行发起,但记得一定要测试,这也是 Spring 推荐的异常处理方式。
/ 三、总结 /
今天橘长用一个真实工作中遇到的代码层层分析为什么要做异常处理,以及 Spring 框架提供的三种方式,推荐控制器通知的方式,不侵入业务代码,优雅处理,其实还有很多挖掘点,比方 Spring 是如何落地代码的,今日代码是以 SpringBoot 框架构建的,还有各种源码级别的问题。
关注橘长,用简单的话讲清楚每一个技术问题,喜欢就点赞转发,分享给你们的朋友们。