Java中的自定义异常类设计与应用

自定义异常类必须继承Exception或RuntimeException,提供三个标准构造函数,不重写getMessage()和getCause(),命名体现业务语义而非技术细节,并配合@ControllerAdvice统一处理。

自定义异常类必须继承 ExceptionRuntimeException

Java 中抛出自定义异常的前提是它得是 Throwable 的子类。绝大多数业务异常应继承 Exception(编译期检查异常),强制调用方处理;若属于程序逻辑错误(如参数非法、状态不一致),可继承 RuntimeException(运行时异常),避免处处 try-catch 或声明 throws

常见错误是直接继承 Throwable 或空参构造不调用父类构造,导致堆栈信息丢失或序列化失败。

  • 继承 Exception:适合需显式处理的业务场景,如支付超时、库存不足
  • 继承 RuntimeException:适合开发阶段应避免发生的错误,如 IllegalArgumentException 的语义延伸
  • 务必提供至少三个标准构造函数:MyException()MyException(String message)MyException(String message, Throwable cause)

getMessage()getCause() 不要被覆盖成空逻辑

自定义异常类中,除非有明确封装需求(如脱敏敏感字段),否则不要重写 getMessage()getCause()。JVM 日志、IDE 调试、监控系统都依赖这些方法返回原始信息。

例如,以下写法会掩盖真实错误原因:

public class PaymentException extends Exception {
    public PaymentException(String message) {
        super(message);
    }
    @Override
    public String getMessage() {
        return "系统繁忙,请稍后再试"; // ❌ 隐藏原始 message,调试困难
    }
}

正确做法是保留原始信息,必要时在日志中补充上下文:

  • super(message, cause) 传递原始异常链
  • catch 块中用 log.error("支付失败,订单ID: {}", orderId, e) 补充业务字段
  • 避免在异常类内部做字符串拼接或格式化,交给日志层或调用方

异常类名要体现“什么错了”,而不是“怎么错的”

命名反映语义层级,而非技术路径。比如 InsufficientBalanceExceptionAccountBalanceCheckFailedException 更清晰;OrderAlreadyShippedExceptionUpdateOrderStatusException 更准确。

容易踩的坑:

  • 带动词或方法名(如 ValidateUserException)—— 这是动作,不是状态
  • 含技术实现细节(如 JDB

    CConnectionTimeoutException
    )—— 应抽象为业务语义,如 DatabaseUnavailableException
  • 用缩写或模糊词(如 InvldParamExBusinessError)—— 无法快速定位问题域

Spring 项目中配合 @ControllerAdvice 统一处理更实用

单独定义异常类意义有限,关键在于如何与框架协同。Spring MVC 中,自定义异常配合全局异常处理器能统一响应格式、HTTP 状态码和日志记录。

典型结构:

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(InsufficientStockException.class)
    @ResponseStatus(HttpStatus.PRECONDITION_FAILED)
    @ResponseBody
    public ApiResponse handleInsufficientStock(InsufficientStockException e) {
        return ApiResponse.fail("库存不足", "STOCK_SHORTAGE");
    }
}

注意点:

  • 确保异常类在 @ExceptionHandler 方法签名中精确匹配(不建议捕获 Exception 后再 instanceof 判断)
  • 避免在 @ExceptionHandler 中抛出新异常,否则可能绕过统一处理逻辑
  • 如果使用 Spring Boot + ResponseEntityExceptionHandler,优先复用其默认行为,再针对性扩展

真正难的不是写一个异常类,而是让整个团队对每个异常的触发条件、恢复方式和日志埋点达成一致。名字、构造方式、是否检查、在哪捕获——这些决策一旦分散,就会变成排查时的噪音源。