引言

我们在做Web或者App应用的时候,请求处理过程中发生错误是非常常见的情况。Spring Boot提供了一个默认的映射:/error,当处理中抛出异常之后,会转到该请求中处理,并且该请求有一个全局的错误页面用来展示异常内容,虽然这个默认的页面对用户并不友好。

先说说在浏览器上的操作出来的结果

 @GetMapping("/user/{id:\\d+}")
    public User getInfo(@PathVariable String id) throws Exception {
        throw new Exception(); //直接抛一个异常
    }

当访问一个不存在的url时,可以看到类似下面的错误页面

errorpage

  • 当访问/user/1时

errorstatus

利用postman发送http请求的结果

{
    "timestamp": 1580180542477,
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/xxx"
}
{
    "timestamp": 1580180799857,
    "status": 500,
    "error": "Internal Server Error",
    "exception": "java.lang.RuntimeException",
    "message": "1",
    "path": "/user/1"
}

实现自己的异常处理与提示

浏览器访问时显示自定义错误页面

首先在resources文件夹下创建resources文件夹,再在resources文件夹下创建error文件夹,并且在error文件夹下创建异常页面,比如说404异常和500异常,如图
4214241
通过自定义404、500等错误页面,我们可以将错误页面修改为与原站相似的主题风格。

返回json格式的错误提示

通过浏览器可以清晰的显示错误页面,但当作为RESTful API时,返回的结果仍然是不够清晰明了的。

{
    "timestamp": 1580180799857,
    "status": 500,
    "error": "Internal Server Error",
    "exception": "java.lang.RuntimeException",
    "message": "1",
    "path": "/user/1"
}

要考虑的几个问题如下:

  • 什么时候需要捕获(try-catch)异常, 什么时候需要抛出(throws)异常到上层.
  • 在 dao 层捕获还是在 service 捕获, 还是在 controller 层捕获.
  • 抛出异常后要怎么处理. 怎么返回给页面错误信息.

异常处理反例

  • 捕获异常后只输出到控制台

前端代码

$.ajax({
    type: "POST",
    url: "/user",
    dataType: "json",
    success: function(data){
        alert("添加成功");
    }
});

后端代码

try {
    //某些操作
} catch (Exception e) {
    e.printStackTrace();
}

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

  1. 那么用户看到的场景就是点击了添加按钮, 但没有任何反应(其实是返回了 500 错误页面, 但这里前端没有监听 error 事件, 只监听了 success 事件. 但即使加上了error: function(data) {alert("添加失败");}) 也没什么用, 用户无法得知错误的原因。
  2. 后台 e.printStackTrace() 打印在控制台的日志也会在漫漫的日志中被埋没, 很可能会看不到输出的异常. 但这并不是最糟的情况, 更糟糕的事情是连 e.printStackTrace() 都没有, catch 块中是空的, 这样后端的控制台中更是什么都看不到了, 这段代码会像一个隐形的炸弹一样一直埋伏在系统中.
  • 混乱的返回方式

前端代码

$.ajax({
    type: "POST",
    url: "/user",
    dataType: "json",
    success: function(data) {
        if (data.flag) {
            alert("添加成功");
        } else {
            alert(data.message);
        }
    },
    error: function(data){
        alert("添加失败");
    }
});

后端代码

@PostMapping("/user")
@ResponseBody
public Map add(User user) {
    Map map = new HashMap();
    try {
        //某些操作
        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 data), 不需要返回数据, 即调用前者, 需要返回数据, 调用后者. 举个栗子
@GetMapping("/user")
@ResponseBody
public ResultBean<User> getAllUsers() {
    List<User> users = UserService.findAll();
    return ResultBean.success(users);
}
@PutMapping("/user")
@ResponseBody
public ResultBean updateGoods(User user) {
    UserService.update(user);
    return ResultBean.success();
}
前端统一处理
/**
 * 显示错误信息
 * @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);
    }
    // 用户操作异常, 根据规定的错误码进行处理
    else if (result.code == 1) {
        showError(result.message);
    }
    // 系统异常, 根据规定的错误码进行处理
    else if (result.code == -1) {
        showError(result.message);
    }
    // 如果进行细粒度的状态码判断, 那么就应该重点注意这里没出现过的状态码.
    else {
        showError("出现未定义的状态码:" + result.code);
    }
}

/**
 * 根据 id 获取用户
 */
function getUser(id) {
    $.ajax({
        type: "GET",
        url: "/user",
        dataType: "json",
        success: function(result){
            handlerResult(result, successFunction);
        }
    });
}

function successFunction(data) {
    //do something
}

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

后端统一异常处理

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

好在 Spring MVC 为我们提供了一些很方便的注解, 用来统一处理异常:
@ControllerAdvice定义统一的异常处理类,简单的说,就是专门用来处理异常的Controller。
@ExceptionHandler用来定义函数针对的异常类型,即不同的函数处理不同类型的异常,类似于url映射。

  • 先自定义一个异常
public class UserNotExistException extends Exception {
    private String id;

    public UserNotExistException(String id){
        super("用户不存在");
        this.id = id;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}
  • 为UserNotExistException异常创建对应的处理
@ControllerAdvice
public class ControllerExceptionHandler {
    @ExceptionHandler(UserNotExistException.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String,Object> handleUserNotExistException(UserNotExistException ex){
        Map<String,Object> result = new HashMap<>();
        result.put("id",ex.getId());
        result.put("message",ex.getMessage());
        return result;
    }
//这里为了简单演示,就暂时不用ResultBean类来作为返回结果集
}

利用postman工具,可以看到返回的结果比之前清晰了许多。

{
    "id": "1",
    "message": "用户不存在"
}

至此,已完成在Spring Boot中创建统一的异常处理,实际实现还是依靠Spring MVC的注解。