文章

良好的异常处理设计:系统稳定性的基础保障

见字如面,与大家分享实践中的经验与思考。

异常一定会发生,并且异常处理是任何软件系统至关重要的一部分,良好的异常处理设计是系统稳定性的基础保障。

面向异常信息的一般有两类人群,一是用户(系统使用者),二是开发人员,先来展示下几个具体的痛点以及对应的设计思路:

异常处理设计目标和要点

下面重点讲述下如何设计一个优雅的异常处理机制。

异常的分类

将异常进行分类,以便更清晰地表达异常含义,同时保持与前端 HTTP 状态码的良好兼容。

异常分类

  • BaseException :所有自定义异常的基类,便于统一管理和扩展。

  • BizException :表示业务逻辑相关的异常,例如:非法参数输入、商品库存不足等

  • SysException : 表示系统运行相关的异常,例如:数据库连接失败、获取锁失败等

  • ClientException :表示外部系统调用失败异常,例如:调用第三方支付接口失败、微服务内接口调用报错等

  • AuthException:表示认证和鉴权相关的异常,例如:Token 过期、无效 Token,用户无权访问资源等

设计要点与扩展性

  1. HttpStatus 状态码绑定

    每种异常类型预定义一个适用的 HTTP 状态码,方便服务端直接转换为前端可识别的响应格式。例如:401 认证失败错误码时,前端需要进行登出处理。

  2. 错误码设计 使用 errCode 字段标识具体的错误场景,建议遵循一定的编码规则,如:纯字母/模块标识 + 错误编码(如 USER_0001 表示用户模块的用户不存在,或者直接NOT_FOUND 表示数据不存在)。

  3. 统一异常处理

    便于进行统一的异常处理逻辑,例如:Spring Boot 中的 @ControllerAdvice@ExceptionHandler

  4. 扩展性 如果未来有新的异常场景,可以继续继承 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 表示用户注册错误,错误码规则如下

    1. 五位组成

    2. A代表用户端错误

    3. B代表当前系统执行异常

    4. C代表第三方服务异常

    5. 若无法确定具体错误,选择宏观错误

    6. 大的错误类间的步长间距预留100

一些参考实例:

1)谷歌 API 错误码定义(采用的是纯字母错误码)

Google API 错误码定义

2)阿里错误码定义(采用的是错误类型+ 4 位数字错误编码)

阿里错误码定义

异常统一处理

以下是 Java + SpringBoot 框架下的异常统一处理的方案。通常我们在开发 Web 应用的时候,会涉及到三类组件,分别是:ControllerFilterHandlerInterceptor

执行顺序:Filter > HandlerInterceptor > Controller

1)Filter

  • 最外层的组件,处理请求的最早阶段。

  • 负责请求的预处理(如认证、日志记录)以及响应的后处理。

  • 对应 Servlet 的过滤器机制。

2)HandlerInterceptor

  • 位于 Spring MVC 的请求分发流程中。

  • 适合对请求进行细粒度的检查(如权限验证、业务逻辑校验)。

  • preHandle 在 Controller 之前执行,postHandleafterCompletion 在 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,大大提高开发效率。

参考文档

Goolea API 错误码

阿里 Java 开发手册(黄山版)


欢迎关注我的公众号“Eric技术圈”,原创技术文章第一时间推送。

License:  CC BY 4.0