文章

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


前言

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

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

异常处理设计目标和要点

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

异常的分类

将异常进行分类,以便更清晰地表达异常含义,同时保持与前端 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