将受检异常包装为运行时异常是解耦分层、收拢错误语义、提升演进自由度的关键动作;需在dao与service边界统一转化,按业务场景封装具体异常类型,并配套日志、监控与全局处理器。

重构传统强耦合旧版框架时,将受检异常包装为运行时异常不是“绕过检查”,而是解耦分层、收拢错误语义、提升演进自由度的关键动作。重点不在“怎么抛”,而在“在哪抛、为什么抛、抛成什么样”。
在DAO与Service边界做一次干净的异常剥离
旧框架常让SQLException、IOException一路透传到Controller,导致业务方法签名堆满throws声明。重构第一步是划定“异常转化区”:
- DAO层(或MyBatis Mapper接口实现类)捕获所有底层IO/SQL异常,统一转为语义明确的运行时异常,如DataAccessException(Spring已提供)或自定义PersistenceException
- Service层不再声明throws SQLException等,只处理业务逻辑异常;若需重试或降级,也基于运行时异常类型做判断,而非原始受检异常类名
- 禁止在Service内部再catch并重新throw原始受检异常——这会把技术细节重新暴露给业务逻辑
用带业务含义的运行时异常替代泛型RuntimeException
直接throw new RuntimeException(e)只是技术上可行,但会丢失可维护性。应按失败场景归类封装:
- 参数解析失败(如JSON反序列化、日期格式错误)→ IllegalArgumentException子类,如InvalidRequestException
- 外部服务不可用或超时 → ExternalServiceException,含服务名、超时阈值等上下文字段
- 状态不一致(如订单已支付却尝试取消)→ IllegalStateException子类,如IllegalOrderStateException
- 所有自定义异常必须提供Throwable cause构造器,并在日志中记录原始异常类名和消息
为流式操作和函数式接口提供安全适配层
旧框架若开始引入Stream、CompletableFuture等现代API,受检异常会立刻卡住lambda表达式。此时需轻量封装:
- 对Files.readAllLines()、BufferedReader.lines()等IO方法,优先使用Java 8+内置的UncheckedIOException包装
- 对自定义IO逻辑,提供工具方法如IOFunctions.unchecked(Files::readAllLines),内部完成try-catch + UncheckedIOException构造
- 避免在每个lambda里重复写try/catch;工具方法应返回Function
>这类无异常声明的接口
配合全局异常处理器与可观测性补全链路
包装为运行时异常后,不能降低问题定位能力。需同步落地支撑机制:
- 统一@ControllerAdvice或@ExceptionHandler拦截所有BusinessException及其子类,返回标准错误码+提示,不暴露堆栈给前端
- 在包装异常前打WARN日志,包含:模块名、操作名、关键入参摘要、原始异常类名(如“UserService.updateUser: userId=123, cause=java.sql.SQLTimeoutException”)
- 监控系统按异常类型(而非message)聚合告警,例如ExternalServiceException每分钟超5次即触发










