资讯专栏INFORMATION COLUMN

Spring Boot统一异常处理实践

call_me_R / 1762人阅读

摘要: SpringBoot异常处理。

原文:Spring MVC/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();
}

这是见过最多的异常处理方式了, 如果这是一个添加商品的方法, 前台通过 ajax 发送请求到后端, 期望返回 json 信息表示添加结果. 但如果这段代码出现了异常:

那么用户看到的场景就是点击了添加按钮, 但没有任何反应(其实是返回了 500 错误页面, 但这里前端没有监听 error 事件, 只监听了 success 事件. 但即使加上了error: function(data) {alert("添加失败");}) 又如何呢? 到底因为啥失败了呢, 用户也不得而知.

后台 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 something
        map.put(flag, true);
    } catch (Exception e) {
        e.printStackTrace();
        map.put("flag", false);
        map.put("message", e.getMessage());
    }
    reutrn map;
}

这种方式捕获异常后, 返回了错误信息, 且前台做了一定的处理, 看起来很完善? 但用 HashMap 中的 flagmessage 这种字符串来当键很容易处理, 例如你这里叫 message, 别人起名叫 msg, 甚至有时手抖打错了, 怎么办? 前台再改成 msg 或其他的字符?, 前端后端这样一直来回改?

更有甚者在情况 A 的情况下, 返回 json, 在情况 B 的情况下, 重定向到某个页面, 这就更乱了. 对于这种不统一的结构处理起来非常麻烦.

异常处理规范

既然要进行统一异常处理, 那么肯定要有一个规范, 不能乱来. 这个规范包含前端和后端.

不要捕获任何异常

对的, 不要在业务代码中进行捕获异常, 即 dao、service、controller 层的所以异常都全部抛出到上层. 这样不会导致业务代码中的一堆 try-catch 会混乱业务代码.

统一返回结果集

不要使用 Map 来返回结果, Map 不易控制且容易犯错, 应该定义一个 Java 实体类. 来表示统一结果来返回, 如定义实体类:

public class ResultBean {
    private int code;
    private String message;
    private Collection 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  ResultBean success(Collection data) {
        ResultBean resultBean = new ResultBean();
        resultBean.setCode(0);
        resultBean.setMessage("success");
        resultBean.setData(data);
        return resultBean;
    }

    // getter / setter 略
}

正常情况: 调用 ResultBean.success()ResultBean.success(Collection data), 不需要返回数据, 即调用前者, 需要返回数据, 调用后者. 如:

@RequestMapping("/goods/add")
@ResponseBody
public ResultBean getAllGoods() {
    List goods = goodsService.findAll();
    return ResultBean.success(goods);
}
@RequestMapping("/goods/update")
@ResponseBody
public ResultBean updateGoods(Goods goods) {
    goodsService.update(goods);
    return ResultBean.success();
}

一般只有查询方法需要调用 ResultBean.success(Collection data) 来返回 N 条数据, 其他诸如删除, 修改等方法都应该调用 ResultBean.success(), 即在业务代码中只处理正确的功能, 不对异常做任何判断. 也不需要对 update 或 delete 的更新条数做判断(个人建议, 实际需要根据业务). 只要没有抛出异常, 我们就认为用户操作成功了. 且操作成功的提示信息在前端处理, 不要后台返回 “操作成功” 等字段.

前台接受到的信息为:

{
    "code": 0,
    "message": "success",
    "data": [
        {
            "name": "商品1",
            "price": 50.00,
        },
        {
            "name": "商品2",
            "price": 99.99,
        }
    ]
}

抛出异常: 抛出异常后, 我们应该调用 ResultBean.error(int code, String message), 来将状态码和错误信息返回, 我们约定 code 为 0 表示操作成功, 12 等正数表示用户输入错误, -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("删除成功");
}

showErrorhandlerResult 是公共方法, 分别用来显示错误和统一处理结果集.

然后将主要精力放在发送请求和处理正确结果的方法上即可, 如这里的 deleteDone 函数, 用来处理操作成功给用户的提示信息, 正所谓各司其职, 前端负责操作成功的消息提示更合理, 而错误信息只有后台知道, 所以需要后台来返回.

后端统一处理异常

说了这么多, 还没讲到后端不在业务层捕获任何异常的事, 既然所有业务层都没有捕获异常, 那么所有的异常都会抛出到 Controller 层, 我们只需要用 AOP 对 Controller 层的所有方法处理即可.

好在 Spring 为我们提供了一个注解, 用来统一处理异常:

@ControllerAdvice
@ResponseBody
public class WebExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(WebExceptionHandler.class);

    @ExceptionHandler
    public ResultBean unknownAccount(UnknownAccountException e) {
        log.error("账号不存在", e);
        return ResultBean.error(1, "账号不存在");
    }

    @ExceptionHandler
    public ResultBean incorrectCredentials(IncorrectCredentialsException e) {
        log.error("密码错误", e);
        return ResultBean.error(-2, "密码错误");
    }

    @ExceptionHandler
    public ResultBean unknownException(Exception e) {
        log.error("发生了未知异常", e);
        // 发送邮件通知技术人员.
        return ResultBean.error(-99, "系统出现错误, 请联系网站管理员!");
    }
}

在这里统一配置需要处理的异常, 同样, 对于未知的异常, 一定要及时发现, 并进行处理. 推荐出现未知异常后发送邮件, 提示技术人员.

总结

总结一下统一异常处理的方法:

不使用随意返回各种数据类型, 要统一返回值规范.

不在业务代码中捕获任何异常, 全部交由 @ControllerAdvice 来处理.

一个简单的演示项目: https://github.com/zhaojun1998/exception-handler-demo

本文作者: 赵俊
本文链接: http://www.zhaojun.im/springboot-exception/
版权声明: 本博客所有文章除特别声明外,均采用BY-NC-SA许可协议。转载请注明出处!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73040.html

相关文章

  • Spring Boot & Spring MVC 异常处理的N种方法

    摘要:定制特定异常返回结果根据官方文档的例子,可以使用和对特定异常返回特定的结果。下面是用浏览器和访问的结果无输出注意上方表格的错误,产生这个的原因前面已经讲过。不过需要注意的是,无法通过设定,由或者容器决定里一律是。 github:https://github.com/chanjarste... 参考文档: Spring Boot 1.5.4.RELEASE Documentation ...

    h9911 评论0 收藏0
  • Spring Boot 2.x 系列教程:WebFlux REST API 全局异常处理 Error

    摘要:挺多人咨询的,异常处理用切面注解去实现去全局异常处理。全局异常处理类,代码如下代码解析如下抽象类是用来处理全局错误时进行扩展和实现注解标记的切面排序,值越小拥有越高的优先级,这里设置优先级偏高。 本文内容 为什么要全局异常处理? WebFlux REST 全局异常处理实战 小结 摘录:只有不断培养好习惯,同时不断打破坏习惯,我们的行为举止才能够自始至终都是正确的。 一、为什么要全局...

    BicycleWarrior 评论0 收藏0
  • [Spring cloud 一步步实现广告系统] 4. 通用代码模块设计

    摘要:对的配置和行为进行定制修改匹配路由请求规则注册自定义的和添加静态资源处理器添加自定义视图控制器添加自定义方法参数处理器配置消息转换器清空所有转换器做一个好人。博客园掘金简书头条知乎 一个大的系统,在代码的复用肯定是必不可少的,它能解决: 统一的响应处理(可以对外提供统一的响应对象包装) showImg(https://segmentfault.com/img/remote/146000...

    since1986 评论0 收藏0
  • Spring Boot 最流行的 16 条实践解读!

    摘要:来源是最流行的用于开发微服务的框架。以下依次列出了最佳实践,排名不分先后。这非常有助于避免可怕的地狱。推荐使用构造函数注入这一条实践来自的项目负责人。保持业务逻辑免受代码侵入的一种方法是使用构造函数注入。 showImg(https://mmbiz.qpic.cn/mmbiz_jpg/R3InYSAIZkHQ40ly9Oztiart2lESCyjCH0JwFRp3oErlYobhibM...

    Ethan815 评论0 收藏0
  • SpringBoot统一响应体解决方案

    摘要:前言最近在优化自己之前基于的统一响应体的实现方案。但是的状态码数量有限,而随着业务的增长,状态码无法很好地表示业务中遇到的异常情况。 前言 最近在优化自己之前基于Spring AOP的统一响应体的实现方案。 什么是统一响应体呢?在目前的前后端分离架构下,后端主要是一个RESTful API的数据接口。 但是HTTP的状态码数量有限,而随着业务的增长,HTTP状态码无法很好地表示业务中遇...

    figofuture 评论0 收藏0

发表评论

0条评论

call_me_R

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<