详解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 中的 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);
}
@RequestMapping("/goods/update")
@ResponseBody
public ResultBean updateGoods(Goods goods) {
  goodsService.update(goods);
  return ResultBean.success();
}

一般只有查询方法需要调用 ResultBean.success(Collection<V> 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 表示操作成功, 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("删除成功");
}

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

然后将主要精力放在发送请求和处理正确结果的方法上即可, 如这里的 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, "系统出现错误, 请联系网站管理员!");
  }
}

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

总结

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

  1. 不使用随意返回各种数据类型, 要统一返回值规范.
  2. 不在业务代码中捕获任何异常, 全部交由 @ControllerAdvice 来处理.

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Spring MVC异常处理机制示例详解

    前言 在Spring MVC中,当一个请求发生异常(Controller抛出一个异常时), DispatcherServlet 采用委托的方式交给一个处理链来处理或者解析这个抛出的异常,这是在request和Servlet Container之间的一道屏障,所以我们可以在这里做一些处理工作,如转换异常,转换成友好的error page或者http 状态码等. 核心接口 这个处理机制在Spring是以HandlerExceptionResolver接口为核心的,该接口只有一个处理方法: Model

  • Spring MVC Controller返回值及异常的统一处理方法

    旧的设计方案 开发api的时候,需要先定义好接口的数据响应结果.如下是一个很简单直接的Controller实现方法及响应结果定义. @RestController @RequestMapping("/users") public class UserController { @Inject private UserService userService; @GetRequest("/{userId:\\d+}") public ResponseBean signin

  • Spring MVC全局异常处理和单元测试_动力节点Java学院整理

    在spring MVC的配置文件中: <!-- 总错误处理--> <bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <property name="defaultErrorView"> <value>/error/error</

  • 详解使用Spring MVC统一异常处理实战

    1 描述 在J2EE项目的开发中,不管是对底层的数据库操作过程,还是业务层的处理过程,还是控制层的处理过程,都不可避免会遇到各种可预知的.不可预知的异常需要处理.每个过程都单独处理异常,系统的代码耦合度高,工作量大且不好统一,维护的工作量也很大. 那么,能不能将所有类型的异常处理从各处理过程解耦出来,这样既保证了相关处理过程的功能较单一,也实现了异常信息的统一处理和维护?答案是肯定的.下面将介绍使用Spring MVC统一处理异常的解决和实现过程. 2 分析 Spring MVC处理异常有3种方

  • 基于SpringMVC的全局异常处理器介绍

    近几天又温习了一下SpringMVC的运行机制以及原理 我理解的springmvc,是设计模式MVC中C层,也就是Controller(控制)层,常用的注解有@Controller.@RequestMapping.@Autowared.@Component,今天呢,我所要写的是SpringMVC的全局异常处理器,关联的接口有HandlerExceptionResolver(Eclipse用户可以按Ctrl+Shift+T进行搜索该接口),什么是全局异常处理器?为什么要用它呢? 在企业开发中,各种

  • SpringMVC异常处理知识点总结

    ResultCode CommonCode UserCode CustomException ExceptionCatch <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.9.RELEASE</version> <relativeP

  • springmvc如何进行异常处理

    异常处理 局部处理(直接写在处理器中) @ExceptionHandler public ModelAndView error(Exception exception) { ModelAndView mv = new ModelAndView(); mv.addObject("error", exception.getMessage()); mv.setViewName("forward:../error.jsp"); return mv; } 全局处理(新建一个类

  • Spring MVC中异常处理的三种方式

    前言 在 SpringMVC, SpringBoot 处理 web 请求时, 若遇到错误或者异常,返回给用户一个良好的错误信息比 Whitelabel Error Page 好的多. SpringMVC 提供了三种异常处理方式, 良好的运用它们可以给用户提供可读的错误信息. 1. 实现 HandlerExceptionResolver public class AppHandlerExceptionResolver implements HandlerExceptionResolver { @O

  • 详解Spring MVC/Boot 统一异常处理最佳实践

    前言 在 Web 开发中, 我们经常会需要处理各种异常, 这是一件棘手的事情, 对于很多人来说, 可能对异常处理有以下几个问题: 什么时候需要捕获(try-catch)异常, 什么时候需要抛出(throws)异常到上层. 在 dao 层捕获还是在 service 捕获, 还是在 controller 层捕获. 抛出异常后要怎么处理. 怎么返回给页面错误信息. 异常处理反例 既然谈到异常, 我们先来说一下异常处理的反例, 也是很多人容易犯的错误, 这里我们同时讲到前端处理和后端处理 : 捕获异常后

  • Spring Boot统一异常处理最佳实践(拓展篇)

    前言 之前一篇文章介绍了基本的统一异常处理思路: Spring MVC/Boot 统一异常处理最佳实践. 上篇文章也有许多人提出了一些问题: 如何区分 Ajax 请求和普通页面请求, 以分别返回 JSON 错误信息和错误页面. 如何结合 HTTP 状态码进行统一异常处理. 今天这篇文章就主要来讲讲这些, 以及其他的一些拓展点. 区分请求方式 其实 Spring Boot 本身是内置了一个异常处理机制的, 会判断请求头的参数来区分要返回 JSON 数据还是错误页面. 源码为: org.spring

  • 详解Spring mvc ant path的使用方法

    详解Spring mvc ant path的使用方法 概要: 任何一个WEB都需要解决URL与请求处理器之间的映射,spring MVC也是一样,但Spring MVC就像Spring所作的一切一样(灵活,可以配置各种东西,但是也造成了很多复杂性),肯定不会只有一种方法来映射URL和 Controller之间的关系,并且在实际上,允许你自己创建映射规则和实现,而不仅仅依赖URL映射. 1.Spring path match Spring MVC中的路径匹配要比标准的web.xml要灵活的多.默认

  • 详解Spring MVC的拦截器与异常处理机制

    目录 1.SpringMVC拦截器 1.1拦截器(interceptor)的作用 1.2拦截器和过滤器的区别 1.3拦截器的快速入门 1.4多拦截器操作 1.5拦截器方法说明 2.SpringMVC异常处理 2.1异常处理的思路 2.2异常处理的两种方式 2.3简单的异常处理器SimpleMappingExceptinResolver 2.4自定义异常处理步骤 总结 1. SpringMVC拦截器 1.1 拦截器(interceptor)的作用 Spring MVC 的拦截器类似于 Servle

  • 详解Spring MVC如何测试Controller(使用springmvc mock测试)

    在springmvc中一般的测试用例都是测试service层,今天我来演示下如何使用springmvc mock直接测试controller层代码. 1.什么是mock测试? mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法. 2.为什么要使用mock测试? 使用Mock O bject进行测试,主要是用来模拟那些在应用中不容易构造(如HttpServletRequest必须在Servlet容器中才能构造出来)或者比较复杂的对象(如J

  • 详解SpringCloud Finchley Gateway 统一异常处理

    SpringCloud Finchley Gateway 统一异常处理 全文搜索[@@]搜索重点内容标记 1 . 问题:使用SpringCloud Gateway时,会出现各种系统级异常,默认返回HTML. 2 . Finchley版本的Gateway,使用WebFlux形式作为底层框架,而不是Servlet容器,所以常规的异常处理无法使用 翻阅源码,默认是使用DefaultErrorWebExceptionHandler这个类实现结构如下: 可以实现参考DefaultErrorWebExcep

  • 详解Spring MVC的异步模式(高性能的关键)

    什么是异步模式 要知道什么是异步模式,就先要知道什么是同步模式,先看最典型的同步模式: 浏览器发起请求,Web服务器开一个线程处理,处理完把处理结果返回浏览器.好像没什么好说的了,绝大多数Web服务器都如此般处理.现在想想如果处理的过程中需要调用后端的一个业务逻辑服务器,会是怎样呢? 调就调吧,上图所示,请求处理线程会在Call了之后等待Return,自身处于阻塞状态.这也是绝大多数Web服务器的做法,一般来说这样做也够了,为啥?一来"长时间处理服务"调用通常不多,二来请求数其实也不多

  • 详解spring mvc对异步请求的处理

    在spring mvc3.2及以上版本增加了对请求的异步处理,是在servlet3的基础上进行封装的. 1.修改web.xml <?xml version="1.0" encoding="UTF-8"?> <web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001

  • 详解spring mvc(注解)上传文件的简单例子

    spring mvc(注解)上传文件的简单例子. 这有几个需要注意的地方 1.form的enctype="multipart/form-data" 这个是上传文件必须的 2.applicationContext.xml中 <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/> 关于

  • 详解Spring MVC自动为对象注入枚举类型

    如果一个对象里面有枚举类型的话,则spring MVC是不能够直接进行注入的,因为它只实现了一些基本的数据类型的自动转换注入,但是其也提供了可扩展的接口,可以根据自己的需要来进行扩展.下面是一个示例: 首先:这是一个枚举类: /** * 新闻类别 * @author: ShangJianguo * 2014-6-11 上午10:51:07 */ public enum ENews { company("0"), // 企业新闻 industry("1");// 行业

随机推荐