真正优雅的做法是用责任链模式解耦异常处理,让不同处理器按类型分发处理,结合@ControllerAdvice动态扩展,保留原始异常链并前置拦截降低try-catch使用频率。

直接在业务代码里堆砌多层 try-catch,会让逻辑被异常处理淹没,可读性差、维护成本高。真正优雅的做法,是把“谁来处理异常”和“怎么处理”解耦——责任链模式正是为此而生:它让不同类型的异常按需流转,由最合适的处理器接手,而不是靠 if-else 或嵌套 catch 硬扛。
用责任链替代嵌套 catch,让异常各归其位
传统写法中,一个方法里可能同时要处理参数校验失败、远程调用超时、数据库连接中断、JSON 解析错误……全塞进同一个 try 块,再配一堆并列或嵌套的 catch,极易失控。责任链模式则把每类异常交给专门的处理器:
- 参数异常处理器 → 拦截 IllegalArgumentException、MissingParamException,返回 400 和提示文案
- 远程调用异常处理器 → 识别 FeignException、TimeoutException,触发降级或重试
- 数据访问异常处理器 → 捕获 SQLException、JPA 异常,转为统一 ServiceException 并记录 traceId
- 兜底处理器 → 处理未被前面环节捕获的 RuntimeException,返回 500 并告警
每个处理器只关心自己能处理的异常类型,不匹配就往下传——就像审批流程里,主管批不了自动转给总监,无需在每个环节重复判断“这事该不该我管”。
Spring 中落地:@ControllerAdvice + 自定义 Handler 链
SpringBoot 本身已内置责任链思想(比如多个 @ExceptionHandler 方法按异常继承顺序执行),但默认是静态注册。若需动态插拔或细粒度控制,可手动构建轻量链:
立即学习“Java免费学习笔记(深入)”;
- 定义统一接口:
interface ExceptionHandler { boolean supports(Throwable t); Response handle(Throwable t); } - 实现具体处理器(如
ValidationExceptionHandler、DbExceptionHandler),各自实现supports()判断是否匹配 - 在全局异常拦截器中遍历处理器链:
handlers.stream().filter(h -> h.supports(e)).findFirst().map(h -> h.handle(e)).orElseThrow(...)
这样既复用 Spring 的异常传播机制,又保留了链式扩展能力——加新异常类型?只需新增一个处理器类,注册进链,业务代码零改动。
结合异常链,避免信息丢失
责任链不是把异常“吞掉”,而是传递+增强。每个处理器在封装新异常时,务必保留原始 cause:
- 不要写
throw new BusinessException("操作失败")—— 丢掉了根因 - 应写
throw new BusinessException("库存不足", e)—— 构造时传入原始异常 - 日志打印时用
log.error("下单失败", e),确保完整堆栈可追溯
配合全局日志框架(如 Logback 的 %ex),原始异常的每一层调用栈都会被保留,排查时一眼看到是 Dubbo 超时还是 Redis 连接拒绝,而不是只看到一层包装后的模糊提示。
提前拦截,从源头减少 try-catch 出现场景
责任链解决的是“异常发生后怎么分发”,但更优策略是“让不该发生的尽量不发生”。这需要前置防御:
- 用 JSR-303 注解(@NotNull、@Min)做 Controller 层参数校验,失败直接 400,不进 Service
- DAO 层查询前先判空或校验 ID 格式,避免 NPE 或 NumberFormatException 进入业务主干
- 远程调用前检查服务健康状态(如 Sentinel 熔断开关),失败走降级,不触发网络异常
这些不是绕过异常处理,而是把低层级、确定性的失败提前拦截,让 try-catch 只聚焦于真正不可预知的运行时风险(如网络瞬断、磁盘满),大幅压缩其使用范围。


















