本文用到全部源码,见文末

为什么要有全局异常处理

一个软件系统总是会遇到各种异常,包括人为抛出的业务异常,以及bug导致的异常。那怎么对这些异常进行处理呢?

最简单的做法是在controller层用try/catch捕获这些异常,然后进行对应的处理.显而易见这种处理方法存在很大的问题,主要是:

  1. 代码冗余controller中会存在大量的try/catch代码,这些代码可能内容基本都是一样的,属于垃圾代码

  2. 不便于修改:假设需要对某种错误类型进行特殊处理,那么需要修改所有的接口,很麻烦,也容易出错

Spring统一异常处理

那么有没有一种方法能够统一处理呢?当然是有的,spring中的AOP就是用来做这样的统一处理的。当然不需要我们来实现这个AOP逻辑。spring已经帮我们实现了。通过@ControllerAdvice(从英文名称就能看出来这个注解用于处理controller的各种事件通知)注解,可以配合@ExceptionHandler@InitBinder@ModelAttribute等注解配套使用.既然是异常处理,那么我们需要用到的就是@ExceptionHandler.

最简单的用法如下:

建立一个ExceptionHandle类即可(@RestControllerAdvice就是@ControllerAdvice@ResponseBody的组合注解):

/**
 * @author fanxb
 * @date 2021-09-24-下午4:37
 */
@RestControllerAdvice
@Slf4j
public class ExceptionHandle {
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        log.error("捕获到错误:{}", e.getMessage(), e);
        return new Result(0, e.getMessage(), null);
    }
}

建立上述类后,controller层抛出的异常全部会进入到handleException方法中进行统一处理。

注意,只有controller抛出的异常会到这里来,过滤器、拦截器的异常是不行的,因为这里的AOP切点是controller

如何优雅的进行错误处理

上一节说明了如何对异常进行捕获,那么在真实的项目中是如何进行处理的呢?这里介绍一种比较优雅的处理方式。

定义统一返回类

统一接口返回的数据格式,便于前后端交互,同时也方便进行一些统一的操作。

/**
 * 下面三个注解是lombok的减负注解,减少一些结构性的编码 
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result implements Serializable {
    private static final long serialVersionUID = 451834802206432869L;
    /**
     * 1:成功,其他失败
     */
    private int code;
    /**
     * message提示内容,当接口异常时提示异常信息
     */
    private String message;
    /**
     * 携带真正需要返回的数据
     */
    private Object data;

    public static Result success(Object obj) {
        return new Result(1, null, obj);
    }
}

定义异常类

首先定义一个基础异常类,继承RuntimeException

@Getter
public class BaseException extends RuntimeException {
    private static final long serialVersionUID = -3149747381632839680L;
    /**
     * 基本错误code为0
     */
    private static final int BASE_CODE = 0;
    private final int code;
    private final String message;

    public BaseException() {
        this(null, null, null);
    }

    public BaseException(String message) {
        this(message, null, null);
    }

    public BaseException(Exception e) {
        this(null, null, e);
    }

    public BaseException(String message, Exception e) {
        this(message, null, e);
    }

    protected BaseException(String message, Integer code, Exception e) {
        super(e);
        this.message = message == null ? "" : message;
        this.code = code == null ? BASE_CODE : code;
    }

    @Override
    public String getMessage() {
        if (this.message != null && this.message.length() > 0) {
            return this.message;
        }
        return super.getMessage();
    }
}

可以看到其中有个构造方法是protect,是为了避免开发图省事直接传入错误码code(强制建立新的业务异常类)

然后如果有自定义的异常,需要建立新的异常类继承BaseException,比如定义一个CustomBusinessException

public class CustomBusinessException extends BaseException {
    private static final long serialVersionUID = 1564935267302330109L;

    /**
     * 自定义业务异常错误码
     */
    private static final int ERROR_CODE = -1;

    public CustomBusinessException() {
        super("自定义义务异常", ERROR_CODE, null);
    }

    public CustomBusinessException(String message, Exception e) {
        super(message, ERROR_CODE, e);
    }
}

之所以用这种新建业务异常类的方式来定义错误码,一方面是为了提高代码可读性,另一方面也是为了方便对异常进行分类处理。

另外也可采用另外一种方式,将错误码定义为枚举类型,构造函数中传入枚举。

编写统一异常处理类

@RestControllerAdvice
@Slf4j
public class ExceptionHandle {
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        BaseException be;
        if (e instanceof BaseException) {
            be = (BaseException) e;
            //手动抛出的异常,仅记录message
            log.info("baseException:{}", be.getMessage());
            if (be instanceof CustomBusinessException) {
                //可在这里进行一些针对特定异常的处理逻辑
                log.info("customBusinessException:{}", be.getMessage());
            }
        } else {
            //其它异常,非自动抛出的,无需给前端返回具体错误内容(用户不需要看见空指针之类的异常信息)
            log.error("other exception:{}", e.getMessage(), e);
            be = new BaseException("系统异常,请稍后重试", e);
        }
        return new Result(be.getCode(), be.getMessage(), null);
    }
}

可以将各种统一处理逻辑定义在这里。本类中是使用一个方法来对所有的异常进行处理,在这个方法中再对异常类型进行区分。还有另外一种写法是定义多个handle方法,每个handle处理一种异常。

另外在此还可以统一处理参数校验的异常,无需在接口代码中手动判断,下一篇中专门说明

如何使用

通常代码会分为controller,service,dao三层,业务代码会写在service层中,因此一般是在service层中抛出异常。当然这三层中抛出的未捕获异常都能被Exceptionhandle捕获。

本文所用到代码见:github