异常处理实践 - 抛异常+错误码 - 2022-12-28已发

哈喽,大家好,我是指北君。

今天和大家分享一下异常处理和错误码管理。


前言

在业务逻辑中,处理异常有两种方式:

  • 抛出异常:可以使得代码更清晰,可读性更好,更符合面向对象。
  • 返回错误码:优点是性能更好,但是不宜维护。

选择哪种需要根据场景而定,不管如何选择,只要团队达成共识,统一规范就可以。

下面介绍一下我使用的处理异常的方式。

自定义异常

创建一个统一的业务异常基类 BaseException ,继承运行时异常 RuntimeException ,包含两个属性:code、message ,和一些常用的构造方法。

这里面的属性 code 的作用就是储存错误码,以便于在返回前台时将错误码返回给用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Getter
public class BaseException extends RuntimeException {
    /**
     * 错误码
     */
    private Integer code;

    /**
     * 错误消息
     */
    private String message;

    public BaseException(Throwable cause) {
        this(null, cause.getMessage(), cause);
    }

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

    public BaseException(Integer code, Throwable cause) {
        this(code, cause.getMessage(), cause);
    }

    public BaseException(Integer code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
        this.message = message;
    }
}

抛出异常:

1
2
3
public void demo() {
    throw new BaseException(999, "系统错误");
}

错误码管理

上面的自定义异常看起来平平无奇,但是怎么将错误码和错误信息管理起来,使代码更优雅简单,是我们接下来要解决的问题。

我使用了 Enum 枚举,先创建一个接口,供枚举来实现,其中包含两个方法:

  • toCode():将枚举值转为整形错误码,默认已实现;
  • getMsg():获取枚举中的异常信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface IExceptionEnum {
    /**
     * 获取异常编码
     *
     * @return
     */
    default int toCode() {
        StringBuilder num = new StringBuilder();
        for (char c : this.toString().toCharArray()) {
            if (CharUtil.isNumber(c)) {
                num.append(c);
            }
        }
        return Convert.toInt(num, -1);
    }
    /**
     * 获取异常信息
     *
     * @return
     */
    String getMsg();
}

下面创建一个枚举类,实现上面的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Getter
@AllArgsConstructor
public enum SystemError implements IExceptionEnum {
    /**
     * 定义系统异常
     */
    E990("自定义异常"),
    E996("权限异常"),
    E997("票据异常"),
    E998("请求异常"),
    E999("系统错误");

    private String msg;
}

观察上面的错误码枚举类,我们发现,枚举值为字母+错误码,属性 msg 为错误信息。

这样就把错误码异常信息统一管理了起来,抛出异常可优化为:

1
2
3
public void demo() {
    throw new BaseException(SystemError.E990.toCode(), SystemError.E990.getMsg());
}

然而这样依然不够优雅,代码量比之前还要长。要是能够只传枚举值一个参数就好了,那么我们继续优化。

创建一个异常类 BusinessException ,继承 BaseException (保证 BaseException 的简单性,创建一个子类,用来接收枚举值),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class BusinessException extends BaseException implements Serializable {
    private static final long serialVersionUID = 1L;

    public BusinessException() {
        this(SystemError.E990.getMsg());
    }

    public BusinessException(String msg, Object... objects) {
        super(SystemError.E990.toCode(), String_.format(msg, objects));
    }

    public BusinessException(Throwable cause) {
        super(cause instanceof BaseException ? ((BaseException) cause).getCode() : SystemError.E999.toCode(), cause);
    }

    public BusinessException(IExceptionEnum exceptionEnum, Object... objects) {
        this(null, exceptionEnum, objects);
    }

    public BusinessException(Throwable cause, IExceptionEnum exceptionEnum, Object... objects) {
        super(exceptionEnum.toCode(), String_.format(exceptionEnum.getMsg(), objects), cause);
    }
}

我们主要看下最后两个构造函数,使用它们,我们抛出异常的代码可优化为:

1
2
3
public void demo() {
    throw new BusinessException(SystemError.E990);
}

如果想要保留原异常信息,还可以使用:

1
2
3
4
5
6
7
public void demo() {
    try {
        //业务代码
    } catch (Exception e) {
        throw new BusinessException(e, SystemError.E990);
    }
}

以上,我们完成了自定义异常和错误码的封装:在每个业务模块中新建一个异常枚举类,用来统一管理异常;使用时抛出一个 BusinessException 即可。

统一异常处理

最后,我们再使用 @ControllerAdvice 和 @ExceptionHandler 注解做一下统一异常处理:

  • 业务异常打印到日志中
  • 系统异常封装为 BaseException 进行返回,同样打印日志;
  • 这里也可以做其他操作,比如短信提醒等。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 系统异常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public BaseException handler(Exception e) {
        BaseException be = new BusinessException(e);
        this.logException(be);
        return be;
    }

    /**
     * 业务异常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(value = BaseException.class)
    @ResponseBody
    public <E extends BaseException> BaseException baseExceptionHandler(E e) {
        this.logException(e);
        return handler(e);
    }

    /**
     * 打印异常信息
     *
     * @param be
     */
    private void logException(BaseException be) {
        Throwable cause = be.getCause();
        log.error("E{} : {} -> {}\n{}", be.getCode(), be.getMessage(), cause.getClass().getName(), getThrowMsg(cause));
    }

    private String getThrowMsg(Throwable cause) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < cause.getStackTrace().length; i++) {
            String str = cause.getStackTrace()[i].toString();
            //只显示前20行
            if (i < 20) {
                sb.append(str).append("\r\n");
            } else {
                break;
            }
        }
        return sb.toString();
    }
}

写在最后

时隔2个月,某鱼群再次限时开放了。

Java技术指北读者交流群,(聊天学习摸鱼,白嫖技术课程为主),又不定时开放了,有一群有趣有料的小伙伴在等你哦!进群方式:公众号后台回复 666

Java Geek Tech wechat
欢迎订阅 Java 技术指北,这里分享关于 Java 的一切。