三层架构中自定义异常通过自然上抛、分层封装和统一拦截实现:各层定义专属异常类,不捕获无明确处理意图的异常,Controller层依赖全局处理器转化响应,异常需携带上下文信息。

自定义异常在三层架构中不是“手动传递”,而是通过自然上抛 + 分层封装 + 统一拦截实现的。关键不在于怎么“传”,而在于每一层该定义什么、抛什么、拦什么。
每层定义专属异常,明确职责边界
DAO 层、Service 层、Controller 层应各自定义继承体系清晰的异常类,比如:
-
DAO 层:定义
DaoException(继承RuntimeException),封装数据库连接失败、SQL 语法错误、主键冲突等底层问题; -
Service 层:定义
BusinessException(也继承RuntimeException),如UserAlreadyExistsException、InsufficientBalanceException,表达业务规则不满足; -
Controller 层:一般不定义新异常,而是接收下层异常,并交由全局处理器统一响应;也可定义极少数与 HTTP 协议强相关的异常(如
InvalidRequestException)。
所有自定义异常建议共用一个根异常(如 BaseException),便于后续统一捕获和识别。
异常不捕获,除非有明确处理意图
各层对异常的默认策略是不吞掉、不静默、不空 catch:
Java项目代码review工具。分析Git变更+完整调用链路上下文,推断业务需求,进行多维度评分和分类汇总,生成完整PRD文档。包含细粒度Java代码审查清单(Null安全、异常处理、Streams、并发、equals/hashCode、资源管理、API设计、性能、MyBatis/ORM、事务边界、SQL/DD...
立即学习“Java免费学习笔记(深入)”;
- DAO 层遇到 SQLException,直接包装为
DaoException抛出,不 try-catch 记录后返回 null; - Service 层调用 DAO 出错,可选择原样上抛,或根据上下文补充业务语义(例如:“查询用户失败” → “用户不存在或系统异常”),再以
BusinessException形式抛出; - Service 层自己触发的业务校验失败(如用户名已注册),应主动
throw new UserAlreadyExistsException(),而不是用 if-else 返回错误码。
只有当某一层能完全消化并闭环处理**该异常时才捕获**——例如 Service 层调用第三方支付接口失败,降级走本地记账,则可 catch 并转为内部成功流程;否则一律上抛。
Controller 层只做拦截和转化,不做业务判断
Controller 本身不写 try-catch 处理具体异常类型,而是依赖全局异常处理器(如 @RestControllerAdvice):
- 捕获
BusinessException→ 记WARN日志,提取错误码和提示语,返回标准 JSON(如{"code":1001,"msg":"用户名已存在"}); - 捕获
DaoException或NullPointerException等系统异常 → 记ERROR日志(含 URL、参数、用户 ID),返回通用提示(如{"code":5000,"msg":"服务暂时不可用"}); - 避免在 Controller 方法里对不同异常分别写多个 catch 块——这会破坏分层抽象,也让异常流失去统一治理能力。
异常信息要携带上下文,不能只留空消息
自定义异常构造时,应支持传入业务标识、错误码、原始异常等:
- 推荐构造函数:
BusinessException(int code, String message, Throwable cause); - DAO 异常可附带 SQL 片段、执行耗时;
- Service 异常可绑定订单号、用户 ID,方便日志追踪;
- 所有异常最终在全局处理器中被结构化输出,前端无需解析堆栈,运维可通过错误码快速定位层级和场景。

















