DAO层应抛受检异常(如SQLException、IOException),因其反映底层资源不确定性;Service层须捕获并转为自定义运行时异常,以解耦技术细节、明确业务语义、避免API污染。

DAO 层通常直接面对底层资源(数据库、文件、网络),容易触发外部不确定性,比如连接中断、SQL 语法错误、IO 失败。这类问题天然属于受检异常的适用场景——调用方(通常是 Service 层)合理预期它可能发生,且有能力做重试、降级或转译为用户友好的提示。
DAO 层该不该抛受检异常?
应该,但仅限原始技术异常本身。例如:
- SQLException:数据库执行失败,如连接超时、唯一键冲突、锁等待超时
- IOException:读写文件、序列化/反序列化失败
- ClassNotFoundException:动态加载类型失败(较少见,但仍有)
这些异常由 JDK 或驱动原生抛出,DAO 层不建议自行 catch 后吞掉或转成 RuntimeException——那样会丢失原始上下文,不利于排查是网络抖动、SQL 写错,还是权限配置问题。
Service 层为什么要转换成运行时异常?
业务逻辑层关注“做什么”,不负责“怎么连数据库”。让 Service 方法签名里堆满 throws SQLException, IOException 会带来三个实际问题:
- API 污染:每个 service 方法都要声明一堆底层异常,调用方被迫处理与业务无关的细节
- 职责错位:事务边界在 Service,但 SQLException 的恢复策略(如重试)往往需结合业务语义判断,DAO 层无法独立决策
- 异常语义模糊:同一个 SQLException 可能对应“用户重复注册”(应提示)或“系统不可用”(应降级),DAO 层无业务上下文做区分
所以主流做法是:Service 层捕获 DAO 抛出的受检异常,封装为自定义的运行时异常,例如 DuplicateUserException、SystemUnavailableException,再向上抛出。这样既保留可识别的业务含义,又免去调用方强制处理负担。
哪些情况可以保留在 DAO 层不转?
极少数例外,当异常本身就是明确的业务信号,且上层必须显式响应时,可考虑不封装:
- 查询返回空结果但业务要求必须存在(如
Optional.empty()+ 自定义受检异常UserNotFoundException) - 分布式锁获取失败,需要上层决定是排队还是立即失败(此时用
LockAcquireTimeoutException extends Exception)
但这类设计要谨慎——它打破了“DAO 只管数据存取”的分层契约,需团队共识并写入接口文档。
一个典型的转换示例
DAO 方法:
public User findById(Long id) throws SQLException {<br> // 执行 JDBC 查询,可能抛 SQLException<br>}
Service 方法:
public User getUserById(Long id) {<br> try {<br> return userDao.findById(id);<br> } catch (SQLException e) {<br> if (isDuplicateKey(e)) {<br> throw new DuplicateUserException("用户ID已存在", e);<br> } else if (isConnectionLost(e)) {<br> throw new SystemUnavailableException("当前服务暂不可用", e);<br> } else {<br> throw new DataAccessException("查询用户失败", e);<br> }<br> }<br>}
其中 DuplicateUserException 和 SystemUnavailableException 都继承 RuntimeException,DataAccessException 是 Spring 提供的典型运行时包装类。

















