用 RuntimeException 包装异常是为了将检查型异常转为非检查型,避免破坏封装性并控制信息粒度;必须保留原始异常作为 cause 并提供业务上下文消息,禁用 addSuppressed() 主动包装主异常,自定义异常应继承 RuntimeException、提供 errorCode 字段且不序列化。
throw new RuntimeException(e) 包装异常?直接抛出底层异常(比如 IOException、SQLException)会让上层调用方被迫处理与业务无关的检查型异常,破坏封装性。更严重的是,原始异常堆栈可能暴露敏感路径或内部实现细节。用 RuntimeException 包装,本质是做一层语义转换:把“系统级失败”转为“业务不可达”,同时控制信息粒度。
常见错误是只写 new RuntimeException(e) 而不传原始消息,导致日志里只有 java.lang.RuntimeException,没有上下文;或者直接丢弃 e,写成 new RuntimeException("failed"),彻底丢失根因。
new RuntimeException("DB query failed", e)
"Failed to load user with id=" + userId
Cause 和 Suppressed 的区别在哪?cause 是异常链的主干,代表“因为什么而失败”,每个异常最多一个;suppressed 是 Java 7 引入的机制,用于记录 try-with-resources 自动关闭时发生的额外异常,可有多个。包装异常时,绝大多数场景只应设置 cause。
误用 addSuppressed() 包装主异常会导致诊断混乱:日志工具(如 Logback)默认只打印 cause 链,suppressed 往往被忽略;监控系统也极少采集 suppressed 异常。
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)@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());
}
new RuntimeException() 就完事。最常被忽略的是:包装后是否还保有足够诊断信息、是否与全局错误处理机制对齐、以及自定义异常是否真的能被准确识别和分类。这三个点没对齐,包装就只是把问题藏得更深了一点。