用 RuntimeException 包装异常是为了将检查型异常转为非检查型,避免破坏封装性并控制信息粒度;必须保留原始异常作为 cause 并提供业务上下文消息,禁用 addSuppressed() 主动包装主异常,自定义异常应继承 RuntimeException、提供 errorCode 字段且不序列化。

为什么要用 throw new RuntimeException(e) 包装异常?
直接抛出底层异常(比如 IOException、SQLException)会让上层调用方被迫处理与业务无关的检查型异常,破坏封装性。更严重的是,原始异常堆栈可能暴露敏感路径或内部实现细节。用 RuntimeException 包装,本质是做一层语义转换:把“系统级失败”转为“业务不可达”,同时控制信息粒度。
常见错误是只写 new RuntimeException(e) 而不传原始消息,导致日志里只有 java.lang.RuntimeException,没有上下文;或者直接丢弃 e,写成 new RuntimeException("failed"),彻底丢失根因。
- 必须保留原始异常作为 cause:
new RuntimeException("DB query failed", e) - 消息中要包含关键业务标识,比如操作对象、ID:
"Failed to load user with id=" + userId - 避免在消息里拼接敏感数据(如密码、token),哪怕是在开发环境
Cause 和 Suppressed 的区别在哪?
cause 是异常链的主干,代表“因为什么而失败”,每个异常最多一个;suppressed 是 Java 7 引入的机制,用于记录 try-with-resources 自动关闭时发生的额外异常,可有多个。包装异常时,绝大多数场景只应设置 cause。
误用 addSuppressed() 包装主异常会导致诊断混乱:日志工具(如 Logback)默认只打印 cause 链,suppressed 往往被忽略;监控系统也极少采集 suppressed 异常。
立即学习“Java免费学习笔记(深入)”;
- 正确做法:用构造函数传入
cause,例如new ServiceException("Validation failed", e) - 仅在资源清理失败需并行上报时才手动调用
addSuppressed() - 自定义异常类应显式提供带
Throwable参数的构造函数,并调用super(message, cause)
自定义异常要不要继承 RuntimeException?
取决于是否希望调用方强制处理。面向 API 或服务层,推荐继承 RuntimeException(如 ServiceException、ValidationException),让业务代码专注逻辑而非异常模板;但 DAO 层若仍需向上透传 JDBC 异常,可保留检查型异常(如 DataSourceException extends Exception),再由 service 层统一包装。
容易踩的坑是混用:同一模块里既有 throws IOException 又有 throw new ServiceException(e),造成异常处理策略断裂。更隐蔽的问题是自定义异常没重写 toString() 或没提供 errorCode 字段,导致统一错误响应时无法提取结构化信息。
- 所有自定义运行时异常必须提供
String message, Throwable cause构造函数 - 建议增加
int errorCode或String code字段,用于前端识别错误类型 - 避免让自定义异常实现
Serializable,除非明确需要跨 JVM 传输(如 RMI)
Spring 中 @ExceptionHandler 怎么配合包装异常?
包装只是第一步,真正起作用的是统一拦截和转化。Spring 的 @ExceptionHandler 能捕获你包装后的异常,但前提是它能匹配到类型——所以自定义异常必须是具体类,不能只靠 RuntimeException 泛泛而谈。
典型错误是写成 @ExceptionHandler(RuntimeException.class),结果把 NullPointerException 也一并吞掉,掩盖了本该快速暴露的编程错误。另一个问题是没设置 @ResponseStatus,导致 HTTP 状态码始终是 500,无法区分客户端错误(4xx)和服务端错误(5xx)。
@ExceptionHandler(ServiceException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ErrorResponse handleServiceException(ServiceException e) {
return new ErrorResponse(e.getErrorCode(), e.getMessage());
}
- 按具体异常类型注册 handler,优先级高于泛化类型
- 在 handler 内不要重新包装异常,否则会破坏原有 cause 链
- 如果需要补充上下文(如 traceId),用 MDC 注入,而不是拼到异常消息里
new RuntimeException() 就完事。最常被忽略的是:包装后是否还保有足够诊断信息、是否与全局错误处理机制对齐、以及自定义异常是否真的能被准确识别和分类。这三个点没对齐,包装就只是把问题藏得更深了一点。










