受检异常应限于边界层(如DAO),业务层需捕获并包装为语义化运行时异常,避免污染接口;仅当调用方需差异化处理时才保留,如文件上传或金融SDK等契约明确场景。

受检异常(Checked Exception)本身不是业务逻辑的一部分,它的存在意义在于强制开发者面对可预见的外部风险——比如文件读取失败、网络超时、数据库连接中断。解耦的关键,不在于回避它,而在于不让它污染业务主干流程。
受检异常该出现在哪一层
它天然属于与外部资源交互的边界层:比如 DAO 层调用 JDBC 时抛出 SQLException,或工具类读取配置文件时抛出 IOException。这些异常反映的是技术环境的不确定性,而非业务规则是否成立。业务 Service 层不应直接 throws IOException,而应将其转换为更语义化的表达。
- DAO 层可抛出受检异常(如 SQLException),因为它是数据访问契约的一部分
- Service 层应捕获并包装——用自定义业务异常(如 OrderCreationFailedException)替代原始受检异常
- Controller 层只面向前端暴露统一格式的错误响应,不泄露底层受检异常类型
为什么不能让业务方法签名堆满 throws
当一个 service 方法写成 public Order createOrder(...) throws IOException, SQLException, InterruptedException,就等于把技术细节强行注入业务接口。调用方被迫处理一堆与“创建订单”无关的异常,既违背单一职责,也掩盖真实业务意图。
- 受检异常一旦跨层传播,会层层污染方法签名,导致 API 可读性下降
- 多数业务场景中,IOException 发生时并不需要用户重试“提交订单”,而是应提示“系统繁忙,请稍后”
- 真正需要恢复的场景极少(例如重试一次数据库连接),大多数时候只需记录日志 + 返回友好提示
解耦的实操方式:包装与转换
保留受检异常的预警价值,但剥离其技术属性。核心动作是“捕获 → 包装 → 抛出自定义运行时异常”。
- 在 DAO 或工具方法中捕获 IOException,构造带上下文信息的新异常:
throw new DataAccessException("读取用户配置失败", e); - 自定义异常继承 RuntimeException,避免强制上层声明 throws
- 保留原始异常作为 cause,确保栈轨迹完整,便于排查根源
- Spring 环境下配合 @ControllerAdvice 全局捕获,统一转为 JSON 错误响应
什么时候仍需保留受检异常
并非所有受检异常都要消灭。当调用方确实有能力且有必要差异化处理时,才值得保留。
- 文件上传工具类提供
saveToFile(...)方法,调用方可能想区分“磁盘满”和“权限不足”,此时保留 IOException 并细化子类更合理 - 银行转账 SDK 对外提供接口,明确要求调用方处理 InsufficientBalanceException(虽为运行时异常)和 NetworkTimeoutException(受检),因两者恢复策略完全不同
- 关键中间件客户端(如 RocketMQProducer)仍使用受检异常约束调用者做重试或降级,属于契约设计的一部分

















