良好的异常处理设计:系统稳定性的基础保障
前言
异常一定会发生,并且异常处理是任何软件系统至关重要的一部分,良好的异常处理设计是系统稳定性的基础保障。
面向异常信息的一般有两类人群,一是用户(系统使用者),二是开发人员,先来展示下几个具体的痛点以及对应的设计思路:
下面重点讲述下如何设计一个优雅的异常处理机制。
异常的分类
将异常进行分类,以便更清晰地表达异常含义,同时保持与前端 HTTP 状态码的良好兼容。
BaseException :所有自定义异常的基类,便于统一管理和扩展。
BizException :表示业务逻辑相关的异常,例如:非法参数输入、商品库存不足等
SysException : 表示系统运行相关的异常,例如:数据库连接失败、获取锁失败等
ClientException :表示外部系统调用失败异常,例如:调用第三方支付接口失败、微服务内接口调用报错等
AuthException:表示认证和鉴权相关的异常,例如:Token 过期、无效 Token,用户无权访问资源等
设计要点与扩展性:
HttpStatus 状态码绑定
每种异常类型预定义一个适用的 HTTP 状态码,方便服务端直接转换为前端可识别的响应格式。例如:401 认证失败错误码时,前端需要进行登出处理。
错误码设计 使用
errCode
字段标识具体的错误场景,建议遵循一定的编码规则,如:纯字母/模块标识 + 错误编码(如USER_0001
表示用户模块的用户不存在,或者直接NOT_FOUND
表示数据不存在)。统一异常处理
便于进行统一的异常处理逻辑,例如:Spring Boot 中的
@ControllerAdvice
和@ExceptionHandler
扩展性 如果未来有新的异常场景,可以继续继承
BaseException
创建字类。
部分代码实现:
/**
* 异常基类
*/
@Getter
@Setter
public abstract class BaseException extends RuntimeException {
private int httpStatus;
private String errCode;
private String errMessage;
public BaseException(int httpStatus, String errorCode, String errMessage) {
super(errMessage);
this.httpStatus = httpStatus;
this.errCode = errorCode;
this.errMessage = errMessage;
}
public BaseException(int httpStatus, String errorCode, String errMessage, Throwable cause) {
super(errMessage, cause);
this.httpStatus = httpStatus;
this.errCode = errorCode;
this.errMessage = errMessage;
}
}
/**
* 业务异常,例如:400 和 404 错误
*/
public class BizException extends BaseException {
private static final int BIZ_EXCEPTION_HTTP_STATUS = 400;
private static final String DEFAULT_ERR_CODE = "BIZ_ERROR";
public BizException(String errCode, String errMessage) {
super(BIZ_EXCEPTION_HTTP_STATUS, errCode, errMessage);
}
public BizException(int httpStatus, String errorCode, String errorMessage) {
super(httpStatus, errorCode, errorMessage); // 可指定 404 等其他状态
}
public BizException(String errorCode, String errorMessage, Throwable cause) {
super(BIZ_EXCEPTION_HTTP_STATUS, errorCode, errorMessage, cause);
}
public BizException(String errorMessage) {
super(BIZ_EXCEPTION_HTTP_STATUS, DEFAULT_ERR_CODE, errorMessage);
}
}
异常错误码设计
轻量级错误码设计方案:
纯字母错误码:使用纯字母进行异常错误的描述,比如:TOKEN_INVALID、AUTHORIZE_FORBIDDEN、NOT_FOUND 等
项目编码+ 模块编码 + 错误编码:比如:
PDM_USER_001
或者更细的力度。同时需要维护一个错误码和错误描述对应的文档。阿里巴巴 Java 开发手册错误码:A0100 表示用户注册错误,错误码规则如下
五位组成
A代表用户端错误
B代表当前系统执行异常
C代表第三方服务异常
若无法确定具体错误,选择宏观错误
大的错误类间的步长间距预留100
一些参考实例:
1)谷歌 API 错误码定义(采用的是纯字母错误码)
2)阿里错误码定义(采用的是错误类型+ 4 位数字错误编码)
异常统一处理
以下是 Java + SpringBoot 框架下的异常统一处理的方案。通常我们在开发 Web 应用的时候,会涉及到三类组件,分别是:Controller
、Filter
和 HandlerInterceptor
。
执行顺序:Filter > HandlerInterceptor > Controller
1)Filter
最外层的组件,处理请求的最早阶段。
负责请求的预处理(如认证、日志记录)以及响应的后处理。
对应 Servlet 的过滤器机制。
2)HandlerInterceptor
位于 Spring MVC 的请求分发流程中。
适合对请求进行细粒度的检查(如权限验证、业务逻辑校验)。
preHandle
在 Controller 之前执行,postHandle
和afterCompletion
在 Controller 或异常处理器之后执行。
3)Controller
核心业务逻辑处理。
ControllerAdvice
和@ExceptionHandler
处理 Controller 方法抛出的异常。
全局异常处理实现
首先定义一个 ExceptionResponse 类:
public class Response {
private boolean success;
private String errCode;
private String errMessage;
private int httpStatus;
public static Response buildFailure(int httpStatus, String errCode, String errMessage) {
Response response = new Response();
response.setHttpStatus(httpStatus);
response.setSuccess(false);
response.setErrCode(errCode);
response.setErrMessage(errMessage);
return response;
}
}
然后通过 Exception 或 HttpResponse来构建 Response:
@Slf4j
public class ExceptionResponseUtil {
public static Response buildResponseFromException(Throwable e) {
int httpStatus = HttpStatus.INTERNAL_SERVER_ERROR.value();
String errCode = "UNKNOWN_ERROR";
String errMessage = e.getMessage();
if (e instanceof BaseException baseException) {
httpStatus = baseException.getHttpStatus();
errCode = baseException.getErrCode();
}
log.error("Unified Catch Exception : [{}-{}] {}", httpStatus, errCode, errMessage, e);
return Response.buildFailure(httpStatus, errCode, errMessage);
}
@SneakyThrows
public static void handleResponseFromHttpRequest(
HttpServletRequest request,
HttpServletResponse response,
Throwable e) {
Response respEntity = buildResponseFromException(e);
response.setStatus(respEntity.getHttpStatus());
ServletUtil.printResponse(request, response, respEntity);
}
}
最后在 Filter 或者 ControllerAdvice 中统一处理异常:
@Slf4j
@ResponseBody
@ControllerAdvice
public class HttpExceptionAdvice {
@ExceptionHandler(Throwable.class)
public ResponseEntity<Object> handleException(Throwable e) {
Response response = ExceptionResponseUtil.buildResponseFromException(e);
return new ResponseEntity<>(response, HttpStatus.valueOf(response.getHttpStatus()));
}
}
@Slf4j
public class HttpFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("http filter init");
}
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
try {
// 执行具体的 filter 逻辑
filterChain.doFilter(servletRequest, servletResponse);
} catch (Throwable e) {
handleResponseFromHttpRequest(request, response, e);
} finally {
MDCUtil.clean();
}
}
@Override
public void destroy() {
log.info("http filter destroy");
}
}
小结
通过定义规范的错误码,统一的前端响应体,以及异常日志打印,能够有效的减少代码的复杂度,有利于代码的维护,并且能够快速定位到 Bug,大大提高开发效率。
参考文档
欢迎关注我的公众号“Eric技术圈”,原创技术文章第一时间推送。