Java异常处理维护性好坏取决于分类、传播与上下文封装是否统一;应定义具业务语义的自定义异常继承BusinessException,强制错误码和提示消息,Controller层仅用@ControllerAdvice做响应翻译,DAO层需将SQL异常映射为业务异常并保留cause,日志须含MDC上下文及关键参数。

Java里异常处理机制好不好维护,关键不在“捕获”或“抛出”的动作本身,而是异常的分类、传播路径和上下文封装是否一致。不统一的异常策略会让日志看不出问题根源,让调用方无法合理决策恢复方式,最终导致 try-catch 块越写越多、越套越深。
用自定义业务异常替代 RuntimeException 泛滥
直接 throw new RuntimeException 或其子类(如 IllegalArgumentException)看似省事,但会模糊业务语义。下游模块无法区分“参数错”和“库存不足”,也无法针对性重试或降级。
- 为每类业务失败场景建一个明确命名的异常,例如
InsufficientStockException、PaymentTimeoutException - 所有自定义异常继承同一个基类(如
BusinessException),便于全局统一处理 - 构造函数强制传入错误码(
String errorCode)和用户提示消息(String userMessage),避免日志中只有堆栈没有业务标识
示例:
public class InsufficientStockException extends BusinessException {
public InsufficientStockException(String skuId) {
super("STOCK_INSUFFICIENT", "商品 " + skuId + " 库存不足");
}
}
Controller 层只做异常翻译,不处理业务逻辑
Spring MVC 的 @ControllerAdvice 是集中翻译异常的理想位置。这里不该做重试、补偿或数据库操作,只负责把异常转成 HTTP 状态码和 JSON 响应体。
- 用
@ExceptionHandler(BusinessException.class)捕获所有业务异常,返回 400 或 422 - 对
Exception或Throwable的兜底处理必须记录完整堆栈,并返回 500 —— 但要确保它只在真正未预期时触发 - 避免在 Controller 方法内写
try-catch,除非你要立即转换异常类型(比如把 DAO 异常包装成业务异常)
DAO 层异常不向上裸露 SQLException
MyBatis 或 JPA 抛出的原生 JDBC 异常(如 SQLTimeoutException、SQLIntegrityConstraintViolationException)携带数据库方言细节,上层模块无法也不该理解。
立即学习“Java免费学习笔记(深入)”;
- 在 DAO 实现类或
@Mapper的@SelectProvider等处,用try-catch捕获 SQL 相关异常 - 根据错误特征映射为业务异常:主键冲突 →
DuplicateKeyException;外键缺失 →ReferentialIntegrityException - 保留原始异常作为 cause(
new BusinessException(...).initCause(e)),方便排查时追溯到底层原因
日志中必须包含异常上下文,而非仅靠堆栈
一个 NullPointerException 在日志里出现十次,如果每次都没有请求 ID、用户 ID、关键参数值,就等于没日志。
- 在全局异常处理器中,用 MDC 注入
traceId、userId、requestId等字段 - 自定义异常的
toString()或额外提供toLogString()方法,拼接业务关键字段(如订单号、SKU、金额) - 禁止在 catch 块里只写
log.error("xxx failed"),必须带e参数:log.error("update order status failed, orderId={}", orderId, e)
维护性差的异常设计,往往不是因为没写 try-catch,而是每次加新逻辑时都绕过已有规范——比如临时补个 new RuntimeException("TODO"),或者在 service 方法里吞掉异常还返回 null。这类“捷径”积累多了,整个链路就失去可诊断性。










