系列文章目录
上一讲,我们讲了 DDD 领域驱动设计在 SpringBoot 中的实践,接下来,说一下我是如何处理异常的。
请看今天的第3讲:异常处理和错误码管理。
在业务逻辑中,处理异常有两种方式:
选择哪种需要根据场景而定,不管如何选择,只要团队达成共识,统一规范就可以。
我在 YanX 项目中使用的是混合方式:后台逻辑使用抛出异常方式;对于用户或接口,使用返回错误码方式。
创建一个统一的业务异常基类 BaseException ,继承运行时异常 RuntimeException ,包含两个属性:code、messgae ,和一些常用的构造方法。
这里面的属性 code 的作用就是储存错误码,以便于在返回前台时将错误码返回给用户。
@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;
}
}
抛出异常:
public void demo() {
throw new BaseException(999, "系统错误");
}
上面的自定义异常看起来很平平无奇,但是怎么将错误码和错误信息管理起来,使代码更优雅简单,是我们接下来要解决的问题。
我使用了 Enum 枚举,先创建一个接口,供枚举来实现,其中包含两个方法:
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();
}
下面创建一个枚举类,实现上面的接口:
@Getter
@AllArgsConstructor
public enum SystemError implements IExceptionEnum {
/**
* 定义系统异常
*/
E990("自定义异常"),
E996("权限异常"),
E997("票据异常"),
E998("请求异常"),
E999("系统错误");
private String msg;
}
观察上面的错误码枚举类,我们发现,枚举值为字母+错误码,属性 msg 为错误信息。
这样就把错误码和异常信息统一管理了起来,抛出异常可优化为:
public void demo() {
throw new BaseException(SystemError.E990.toCode(), SystemError.E990.getMsg());
}
然而这样依然不够优雅,代码量比之前还要长。要是直传枚举值一个参数就好了,那么我们继续优化。
创建一个异常类 BusinessException ,继承 BaseException (保证 BaseException 的简单性,创建一个子类,用来接收枚举值),如下:
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);
}
}
我们主要看下最后两个构造函数,使用它们,我们抛出异常的代码可优化为:
public void demo() {
throw new BusinessException(SystemError.E990);
}
如果想要保留原异常信息,还可以使用:
public void demo() {
try {
//业务代码
} catch (Exception e) {
throw new BusinessException(e, SystemError.E990);
}
}
下面我们使用 @ControllerAdvice 和 @ExceptionHandler 注解做一下统一异常处理:
代码如下:
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 系统异常
*
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public BaseException handler(Exception e) {
BaseException be = new BusinessException(e);
this.logException(be);
return be;
}
/**
* 打印异常信息
*
* @param be
*/
private void logException(BaseException be) {
Throwable cause = be.getCause();
if (cause == null) {
cause = be;
}
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();
}
}
本讲介绍了我对异常的处理方式,原理很简单,主要注重代码的可读性和优雅性,希望能够给大家提供一个思路。项目代码已托管到 Gitee(搜索 yanx ),大家可自行下载学习,如果有什么问题请在下方留言。