Spring Boot 中统一异常处理最佳实践介绍
前言
在 Web
开发中, 我们经常会需要处理各种异常, 这是一件棘手的事情, 对于很多人来说, 可能对异常处理有以下几个问题:
- 什么时候需要捕获(
try-catch
)异常, 什么时候需要抛出(throws
)异常到上层. - 在
dao
层捕获还是在service
捕获, 还是在controller
层捕获. - 抛出异常后要怎么处理. 怎么返回给页面错误信息.
异常处理反例
既然谈到异常, 我们先来说一下异常处理的反例, 也是很多人容易犯的错误, 这里我们同时讲到前端处理和后端处理 :
捕获异常后只输出到控制台
前端代码
$.ajax({type: "GET",url: "/user/add",dataType: "json",success: function(data){alert("添加成功");}
});
后端代码
try {// do something
} catch (Exception e) {e.printStackTrace();
}
那么用户看到的场景就是点击了添加按钮, 但没有任何反应(其实是返回了 500 错误页面, 但这里前端没有监听 error 事件, 只监听了 success 事件. 但即使加上了error: function(data) {alert("添加失败");}
) 又如何呢? 到底因为啥失败了呢, 用户也不得而知.这是见过最多的异常处理方式了, 如果这是一个添加商品的方法, 前台通过 ajax 发送请求到后端, 期望返回 json 信息表示添加结果. 但如果这段代码出现了异常:
- 后台
e.printStackTrace()
打印在控制台的日志也会在漫漫的日志中被埋没, 很可能会看不到输出的异常. 但这并不是最糟的情况, 更糟糕的事情是连e.printStackTrace()
都没有,catch
块中是空的, 这样后端的控制台中更是什么都看不到了, 这段代码会像一个隐形的炸弹一样一直埋伏在系统中.
混乱的返回方式
前端代码
$.ajax({type: "GET",url: "/goods/add",dataType: "json",success: function(data) {if (data.flag) {alert("添加成功");} else {alert(data.message);}},error: function(data){alert("添加失败");}
});
后端代码
@RequestMapping("/goods/add")
@ResponseBody
public Map add(Goods goods) {Map map = new HashMap();try {// do somethingmap.put(flag, true);} catch (Exception e) {e.printStackTrace();map.put("flag", false);map.put("message", e.getMessage());}reutrn map;
}
异常处理规范这种方式捕获异常后, 返回了错误信息, 且前台做了一定的处理, 看起来很完善? 但用 HashMap
中的 flag
和 message
这种字符串来当键很容易处理, 例如你这里叫 message
, 别人起名叫 msg
, 甚至有时手抖打错了, 怎么办? 前台再改成 msg
或其他的字符?, 前端后端这样一直来回改?
更有甚者在情况 A 的情况下, 返回 json, 在情况 B 的情况下, 重定向到某个页面, 这就更乱了. 对于这种不统一的结构处理起来非常麻烦.
既然要进行统一异常处理, 那么肯定要有一个规范, 不能乱来. 这个规范包含前端和后端.
不要捕获任何异常
对的, 不要在业务代码中进行捕获异常, 即 dao、service、controller 层的所以异常都全部抛出到上层. 这样不会导致业务代码中的一堆 try-catch
会混乱业务代码.
统一返回结果集
不要使用 Map 来返回结果, Map 不易控制且容易犯错, 应该定义一个 Java 实体类. 来表示统一结果来返回, 如定义实体类:
public class ResultBean<T> {private int code;private String message;private Collection<T> data;private ResultBean() {}public static ResultBean error(int code, String message) {ResultBean resultBean = new ResultBean();resultBean.setCode(code);resultBean.setMessage(message);return resultBean;}public static ResultBean success() {ResultBean resultBean = new ResultBean();resultBean.setCode(0);resultBean.setMessage("success");return resultBean;}public static <V> ResultBean<V> success(Collection<V> data) {ResultBean resultBean = new ResultBean();resultBean.setCode(0);resultBean.setMessage("success");resultBean.setData(data);return resultBean;}// getter / setter 略
}
- 正常情况: 调用
ResultBean.success()
或ResultBean.success(Collection<V> data)
, 不需要返回数据, 即调用前者, 需要返回数据, 调用后者. 如:
@RequestMapping("/goods/add")
@ResponseBody
public ResultBean<Goods> getAllGoods() {List<Goods> goods = goodsService.findAll();return ResultBean.success(goods);
}
一般只有查询方法需要调用 ResultBean.success(Collection<V> data)
来返回 N 条数据, 其他诸如删除, 修改等方法都应该调用 ResultBean.success()
, 即在业务代码中只处理正确的功能, 不对异常做任何判断. 也不需要对 update 或 delete 的更新条数做判断(个人建议, 实际需要根据业务). 只要没有抛出异常, 我们就认为用户操作成功了. 且操作成功的提示信息在前端处理, 不要后台返回 “操作成功” 等字段.
@RequestMapping("/goods/update")
@ResponseBody
public ResultBean updateGoods(Goods goods) {goodsService.update(goods);return ResultBean.success();
}
前台接受到的信息为:
{"code": 0,"message": "success","data": [{"name": "商品1","price": 50.00,},{"name": "商品2","price": 99.99,}]
}
- 前台接受到的信息为:抛出异常: 抛出异常后, 我们应该调用
ResultBean.error(int code, String message)
, 来将状态码和错误信息返回, 我们约定code
为 0 表示操作成功,1
或2
等正数表示用户输入错误,-1
,-2
等负数表示系统错误.
复制
{"code": -1,"message": "XXX 参数有问题, 请重新填写","data": null
}
返回的结果集规范后, 前端就很好处理了:前端统一处理:
/*** 显示错误信息* @param result: 错误信息*/
function showError(s) {alert(s);
}/*** 处理 ajax 请求结果* @param result: ajax 返回的结果* @param fn: 成功的处理函数 ( 传入data: fn(result.data) )*/
function handlerResult(result, fn) {// 成功执行操作,失败提示原因if (result.code == 0) {fn(result.data);}// 用户操作异常, 这里可以对 1 或 2 等错误码进行单独处理, 也可以 result.code > 0 来粗粒度的处理, 根据业务而定.else if (result.code == 1) {showError(result.message);}// 系统异常, 这里可以对 -1 或 -2 等错误码进行单独处理, 也可以 result.code > 0 来粗粒度的处理, 根据业务而定.else if (result.code == -1) {showError(result.message);}// 如果进行细粒度的状态码判断, 那么就应该重点注意这里没出现过的状态码. 这个判断仅建议在开发阶段保留用来发现未定义的状态码.else {showError("出现未定义的状态码:" + result.code);}
}/*** 根据 id 删除商品*/
function deleteGoods(id) {$.ajax({type: "GET",url: "/goods/delete",dataType: "json",success: function(result){handlerResult(result, deleteDone);}});
}function deleteDone(data) {alert("删除成功");
}
然后将主要精力放在发送请求和处理正确结果的方法上即可, 如这里的 deleteDone 函数, 用来处理操作成功给用户的提示信息, 正所谓各司其职, 前端负责操作成功的消息提示更合理, 而错误信息只有后台知道, 所以需要后台来返回.showError
和 handlerResult
是公共方法, 分别用来显示错误和统一处理结果集.
后端统一处理异常
说了这么多, 还没讲到后端不在业务层捕获任何异常的事, 既然所有业务层都没有捕获异常, 那么所有的异常都会抛出到 Controller 层, 我们只需要用 AOP 对 Controller 层的所有方法处理即可.
好在 Spring 为我们提供了一个注解, 用来统一处理异常:
@ControllerAdvice
@ResponseBody
public class WebExceptionHandler {private static final Logger log = LoggerFactory.getLogger(WebExceptionHandler.class);@ExceptionHandlerpublic ResultBean unknownAccount(UnknownAccountException e) {log.error("账号不存在", e);return ResultBean.error(1, "账号不存在");}@ExceptionHandlerpublic ResultBean incorrectCredentials(IncorrectCredentialsException e) {log.error("密码错误", e);return ResultBean.error(-2, "密码错误");}@ExceptionHandlerpublic ResultBean unknownException(Exception e) {log.error("发生了未知异常", e);// 发送邮件通知技术人员.return ResultBean.error(-99, "系统出现错误, 请联系网站管理员!");}
}
总结
在这里统一配置需要处理的异常, 同样, 对于未知的异常, 一定要及时发现, 并进行处理. 推荐出现未知异常后发送邮件, 提示技术人员.
总结一下统一异常处理的方法:
- 不使用随意返回各种数据类型, 要统一返回值规范.
- 不在业务代码中捕获任何异常, 全部交由
@ControllerAdvice
来处理.
Spring Boot 中统一异常处理最佳实践介绍相关推荐
- 基于spring boot的统一异常处理
基于spring boot的统一异常处理 参考文章: (1)基于spring boot的统一异常处理 (2)https://www.cnblogs.com/knyel/p/7804237.html 备 ...
- Spring Boot学习——统一异常处理
Spring Boot学习--统一异常处理 参考文章: (1)Spring Boot学习--统一异常处理 (2)https://www.cnblogs.com/aston/p/7258834.html ...
- Spring Boot中的一些常用配置介绍!
这篇教程将为你介绍Spring Boot 中的一些常用配置,比如:自定义 Banner.配置日志.关闭特定的自动配置等. 自定义Banner 在 Spring Boot 启动的时候会有一个默认的启动图 ...
- Spring Boot项目整合Retrofit最佳实践,最优雅的HTTP客户端工具!
点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达今日推荐:2020年7月程序员工资统计,平均14357元,又跌了,扎心个人原创100W+访问量博客:点击前往,查看更多 转自 ...
- 无返回值_只需一步,在Spring Boot中统一Restful API返回值格式与处理异常
统一返回值 在前后端分离大行其道的今天,有一个统一的返回值格式不仅能使我们的接口看起来更漂亮,而且还可以使前端可以统一处理很多东西,避免很多问题的产生. 比较通用的返回值格式如下: public cl ...
- Spring Cloud中统一异常处理是怎么做的?
作者:BNDong www.cnblogs.com/bndong/p/10135370.html 在启动应用时会发现在控制台打印的日志中出现了两个路径为 {[/error]} 的访问地址,当系统中发送 ...
- spring boot +mybatis plus +html 最佳实践项目
源码地址 https://github.com/javanan/slife slife spring boot 搭建的一个企业级快速开发脚手架. 技术栈 Spring Boot MySQL Freem ...
- 读《Spring Boot 3核心技术与最佳实践》有感
我是谁?
- Spring Boot中Web应用的统一异常处理
为什么80%的码农都做不了架构师?>>> 我们在做Web应用的时候,请求处理过程中发生错误是非常常见的情况.Spring Boot提供了一个默认的映射:/error,当处理中抛 ...
- spring 2.0核心技术与最佳实践 pdf_推荐 Spring Boot 实践学习案例大全 数据缓存 和中间件 安全权限...
概况 spring boot 实践学习案例 spring boot 初学者及核心技术巩固的最佳实践 目录 『 Spring Boot 2 快速教程 』 Spring Boot 2:WebFlux集成 ...
最新文章
- 9.1 基于内容的推荐系统-机器学习笔记-斯坦福吴恩达教授
- 79年后,中国人口将不到10亿,我国的生育率为何上不去了?
- UITableViewCell 设置
- AngularJS中关于ng-class和*ngIf指令
- 282. Expression Add Operators
- 你用的那些开源项目可能已经被放弃维护!
- Linux——给history命令加上用户和时间
- crowd counting_[crowd_counting]-SFANet-arxiv1902
- caffe 使用cudnn 加速报错
- hive使用适用场景_数据分析之hive学习(四):面试题——场景输出(row_number)...
- redis依赖包_解决痛点:二方包稳定性测试实践
- 自学python入门训练营 李笑来_如何看待李笑来发布的Python教程《自学是门手艺》?...
- MySQL表数据导入导出
- C语言公交车线路信息查询系统
- 概率收敛、分布收敛、Lp收敛
- maxima学习笔记(一)
- php获取中文字符拼音首字母 阿星小栈
- U3D_API_解析
- 计算机科学与技术专业实践范文,计算机科学与技术专业毕业实习报告范文.docx...
- 了解HTML标记语言
热门文章
- 22. Element lang 属性
- 电压放大倍数公式运放_三点出发,学习运算放大器不再“犯难”
- 6. 集成学习(Ensemble Learning)算法比较
- Java输出字符串格式问题 .UnknownFormatConversionException
- Microsoft Endpoint Protection for Windows Azure客户技术预览版可供免费下载
- 使用Emit动态调用方法(技术原型)
- Winform中 System.Drawing.Color颜色对照表
- C# 在winform中如何为按钮设置快捷键( F1)
- SqlServer复制表结构:从另一张表中获取某些字段和类型并添加到一张已经存在的表中...
- 运行uiautomatorviewer.bat报错No suitable Java found