包装器模式通过将受检异常封装为RuntimeException子类,在保持原始异常信息和业务语义的前提下解除强制处理义务。它用于Lambda、Stream、DAO等场景,需继承RuntimeException、提供cause构造器、携带errorCode等业务字段,并在技术栈向业务域过渡时执行,同时集成traceId、日志追踪与告警钩子。

受检异常本身不能被“消除”或“转换类型”,但可以通过包装器(Wrapper)模式,用运行时异常包裹它,在保持语义清晰、上下文完整的同时,解除调用方的强制处理义务。
为什么需要包装器模式
Java 要求所有受检异常必须显式捕获或声明 throws,这在函数式编程(如 Lambda)、流式操作(Stream)、回调接口等场景中会直接导致编译失败。包装器不是绕过检查机制,而是把技术异常升维为业务可理解、可分类、可追踪的错误信号。
- Lambda 表达式无法抛出 IOException,但可以抛出 RuntimeException 的子类
- Stream.map() 只接受 Function,不接受会抛异常的 ThrowingFunction —— 包装后就能接入
- DAO 层的 SQLException 不该暴露给 Service,应统一转为 PersistenceException 或 BusinessException
怎么设计一个合格的异常包装器
关键不在“包”,而在“包得有信息、有层次、有用途”。一个合格的包装器需满足三个基本条件:
- 继承 RuntimeException:避免强制上层 try-catch,同时保留 Throwable 的全部能力
- 提供 cause 构造器:new MyBusinessException("订单创建失败", e) 必须能通过 getCause() 拿回原始 SQLException 或 IOException
- 携带业务字段:比如 errorCode("ORDER_CREATE_TIMEOUT")、httpStatus(408)、userMessage("下单超时,请重试")
不推荐直接 new RuntimeException(e),那只是技术兜底,不是业务建模。
典型包装方式与使用位置
包装动作不是越早越好,也不是越晚越好,而应在“异常首次脱离底层技术栈、进入业务语义域”的边界执行:
- DAO 层:捕获 SQLException → 包装为 DataAccessException 或自定义 PersistenceException
- 工具方法内部:Files.readString(path) 抛 IOException → 工具类返回 String,内部用 UncheckedIOException 包装
- Lambda 适配层:用 ThrowingFunction + wrap() 工具方法,将 Files::readAllLines 转为 Function<Path, List<String>>
- HTTP 客户端调用处:RestTemplate.getForObject 抛 RestClientException → 包装为 ExternalServiceException,并附带服务名和请求 ID
配套可观测性不能少
包装不是终点,而是可观测链路的起点。每次包装都应触发以下动作:
- 生成唯一 traceId 并绑定到 MDC,确保日志、指标、链路追踪三者一致
- 自动提取 StackTraceElement,记录异常发生的具体类、方法、行号
- 在日志中输出原始异常类型(如 "Caused by: java.sql.SQLTimeoutException"),而非只打包装后的类名
- 开放 onErrorLogged 钩子,供监控系统注入告警逻辑(例如 errorCode 为 "DB_CONN_LOST" 时触发短信通知)

















