← 返回题目列表

什么是异常链?如何自定义异常?异常处理有哪些最佳实践?

中等 第 23 / 32 题 更新于 2026/07/27
异常链自定义异常异常处理cause

简化版

异常链(Exception Chaining) 是「捕获一个异常后,抛出一个新异常,同时把原异常作为『原因(cause)』附上」——这样既能抛出更贴合业务的异常,又不丢失底层异常的堆栈信息,排查时能看到完整的「因果链」。实现靠 new BusinessException("下单失败", e)(把原异常 e 作为构造参数)或 initCause(e)自定义异常通常继承 RuntimeException(非受检,更常用)或 Exception(受检,强制处理),提供带 message 和 cause 的构造器。异常处理最佳实践:① 别吞异常(空 catch);② 抛异常要带上下文和原始 cause(保留异常链);③ 别用异常控制正常流程(异常开销大);④ 精确捕获(别一律 catch Exception);⑤ 早抛出、晚捕获(在能处理的层统一处理);⑥ 资源用 try-with-resources。

详细版

异常链:保留原因

// ✗ 差:丢失原始异常,只知道"下单失败",不知道底层为什么失败
try {
    dao.insert(order);
} catch (SQLException e) {
    throw new BusinessException("下单失败");  // 原始的 SQLException 堆栈丢了!
}

// ✓ 好:把原异常作为 cause 附上,保留完整因果链
try {
    dao.insert(order);
} catch (SQLException e) {
    throw new BusinessException("下单失败", e);  // e 作为 cause,堆栈保留
    // 日志里会打印:BusinessException: 下单失败
    //              Caused by: SQLException: ...(原始异常的完整堆栈)
}

自定义异常

// 继承 RuntimeException(非受检,更常用)
public class BusinessException extends RuntimeException {
    private final int code;   // 可加业务错误码

    public BusinessException(String message) { super(message); }
    public BusinessException(String message, Throwable cause) {   // ★ 带 cause 的构造器(异常链)
        super(message, cause);
    }
    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }
    public int getCode() { return code; }
}

异常处理最佳实践清单

实践反例正确做法
别吞异常catch (Exception e) {} 空 catch至少 log,或抛出
保留异常链throw new Xxx("msg") 丢 causethrow new Xxx("msg", e)
别用异常控流程用异常判断循环结束用正常的条件判断
精确捕获一律 catch Exceptioncatch 具体异常类型
别只打 messagelog.error(e.getMessage())log.error("msg", e) 打完整堆栈
资源释放手写 finally 关资源try-with-resources

⚠️ 异常链最容易被破坏的地方是「重新抛出异常时忘了传 cause」——catch (SQLException e) { throw new BusinessException("失败"); } 这样写,原始的 SQLException 堆栈就彻底丢失了,线上排查时只看到「失败」却不知道底层是「数据库连接超时」还是「唯一键冲突」。正确做法是始终把捕获的异常作为 cause 传给新异常new BusinessException("失败", e)),保留完整的「Caused by」链条。

完整版教学

一、异常链解决什么:既转换又不丢信息

实际开发中经常需要「捕获底层异常,转换成业务异常再抛出」——比如 DAO 层抛 SQLException,你想在 Service 层转成 BusinessException(更贴合业务、上层好处理)。但转换时有个矛盾:

矛盾:既想"抛出新的业务异常"(对上层友好),又想"保留原始异常信息"(便于排查)
  只抛新异常:上层看到 BusinessException("下单失败") —— 但不知道底层为什么失败
  只抛原异常:上层看到 SQLException —— 但暴露了底层细节、不够业务化

异常链的解法:抛新异常 + 把原异常作为"cause(原因)"附上
  throw new BusinessException("下单失败", sqlException)
  → 上层看到业务异常 BusinessException(友好)
  → 但异常里带着原始的 SQLException(排查时能看到完整因果)

异常链的价值是「两全其美」——既能抛出「合适层次的异常」(业务化、屏蔽底层细节),又「不丢失原始的堆栈信息」(排查时能追溯到根因)。它通过「新异常持有 cause(原异常)」的方式,把「多层异常」串成一条「因果链」。日志打印时会显示 Caused by: 一层层追溯到最底层的根异常。理解「异常链 = 转换异常时保留原因、两全其美」,就理解了它为什么是异常处理的核心实践——没有它,异常转换就会丢失根因、排查困难。

二、异常链的实现:cause 参数

异常链的实现很简单——Throwable 有一个 cause 字段,通过带 cause 的构造器或 initCause() 设置

// 方式1:带 cause 的构造器(推荐)
throw new BusinessException("下单失败", sqlException);   // sqlException 作为 cause

// 方式2:initCause(当异常没有带 cause 的构造器时)
BusinessException be = new BusinessException("下单失败");
be.initCause(sqlException);
throw be;

// Throwable 的构造器(所有异常都继承自它):
// Throwable(String message, Throwable cause)  ← 支持 cause
// Throwable(Throwable cause)

打印时的效果:

BusinessException: 下单失败
    at com.xx.OrderService.create(OrderService.java:25)
    ...
Caused by: java.sql.SQLException: Connection timeout   ← 原始根因,完整堆栈
    at com.xx.OrderDao.insert(OrderDao.java:40)
    ...

关键机制:Caused by: 就是异常链的体现——JVM 打印异常时会递归打印 cause,形成「新异常 → Caused by 原异常 → Caused by 更底层异常」的链条,一直追到根因。所以只要「抛新异常时传了 cause」,排查时就能顺着 Caused by 找到最底层的真正原因。理解「异常链靠 cause 字段实现、打印时显示 Caused by」,就掌握了异常链的技术细节——核心就是「别忘了传 cause」。

三、自定义异常:继承哪个、加什么

自定义异常是把「业务错误」表达成一个专门的异常类,关键是继承哪个基类提供哪些构造器

public class BusinessException extends RuntimeException {   // 继承 RuntimeException
    private final int code;   // 业务错误码(可选)

    public BusinessException(String message) { super(message); }
    public BusinessException(String message, Throwable cause) { super(message, cause); }  // ★必须
    public BusinessException(int code, String message) { super(message); this.code = code; }
    public int getCode() { return code; }
}

设计要点:① 继承 RuntimeException 还是 Exception——大多数业务异常继承 RuntimeException(非受检,不强制 try-catch,用起来灵活,Spring 事务默认对 RuntimeException 回滚);只有「调用方必须处理」的才继承 Exception(受检,强制处理)。② 必须提供带 cause 的构造器(String, Throwable))——否则无法建异常链。③ 可加业务字段(如错误码 code、错误详情),配合全局异常处理返回结构化错误。理解「自定义异常一般继承 RuntimeException、必带 cause 构造器、可加错误码」,就能设计出好用的业务异常——它是全局异常处理、统一错误响应的基础(前面 Spring 的 @ControllerAdvice 就是捕获这些自定义异常)。

四、最佳实践一:别吞异常、别只打 message

异常处理最常见的两个错误——吞异常只打 message

// ✗ 反模式1:空 catch 吞异常——出了问题完全查不到
try { risky(); } catch (Exception e) { }   // 异常被吃掉,无声无息

// ✗ 反模式2:只打 message,丢了堆栈——不知道哪行出的错
catch (Exception e) { log.error("出错: " + e.getMessage()); }  // 没堆栈,定位难

// ✓ 正确:带上下文 + 完整异常(堆栈)
catch (Exception e) { log.error("处理订单失败, orderId={}", orderId, e); }
//                                                              ↑ 异常对象作最后参数才打堆栈

吞异常(空 catch)是头号反模式——它让异常「无声无息地消失」,程序继续跑但状态可能已经错了,出问题时完全无法追查。只打 e.getMessage() 也是常见错误——message 只是一句话(如「null」),没有堆栈,你不知道是哪行、哪个调用链出的错。正确做法是 log.error("上下文信息", e)(把异常对象作为最后一个参数传给日志方法,才会打印完整堆栈)。理解「绝不空 catch 吞异常、日志要带完整异常堆栈和上下文」,就避开了异常处理最坑的两个错误——线上事故里「异常被吞了查不到」是最让人抓狂的一类。

五、最佳实践二:别用异常控制流程

一个重要的性能和设计原则——异常只用于「异常情况」,不用于「正常的控制流程」

// ✗ 反模式:用异常控制正常流程
try {
    while (true) {
        process(iterator.next());   // 用 NoSuchElementException 判断循环结束
    }
} catch (NoSuchElementException e) { }   // 把"正常结束"当异常处理

// ✓ 正确:用正常的条件判断
while (iterator.hasNext()) {
    process(iterator.next());
}

原因:异常的创建开销很大——new 一个异常时要填充堆栈信息(fillInStackTrace),这个操作要遍历整个调用栈、代价高(比正常的方法调用慢几十上百倍)。如果用异常控制正常流程(如循环里频繁抛异常判断结束),会造成严重的性能问题。而且用异常控流程也让代码难读(正常逻辑藏在 catch 里)。所以「异常是给『意外/错误』用的,不是给『正常分支』用的」——正常的判断用 if、循环结束用条件,别用 try-catch 兜。理解「异常创建开销大(填充堆栈)、别用异常控正常流程」,就懂了这个性能+设计的双重原则。(题外:如果确实要频繁抛异常且不需要堆栈,可重写 fillInStackTrace 返回自身来省开销,但这是特殊优化。)

六、最佳实践三:精确捕获、分层处理

还有两条重要实践——精确捕获分层处理

精确捕获(catch 具体异常,别一律 catch Exception):
  ✗ catch (Exception e)  ← 太宽,把不该捕获的也捕获了(如 RuntimeException 编程错误)
  ✓ catch (IOException e) / catch (SQLException e)  ← 精确,只处理该处理的
  → 精确捕获让你能针对不同异常做不同处理,也不会误吞编程 bug

分层处理(早抛出、晚捕获——在能处理的层统一处理):
  底层(DAO):抛出/转换异常,不自己处理(它不知道怎么处理业务错误)
  中层(Service):转换成业务异常(带 cause),或让它继续往上抛
  上层(Controller/全局异常处理器):统一捕获、返回友好错误
  → 别在每一层都 try-catch(到处 catch 让代码乱、且底层无法决定怎么处理)

精确捕获catch 具体异常类型(IOExceptionSQLException)而非笼统的 Exception——这样能针对不同异常做不同处理,也避免误吞了本该暴露的编程错误(如 NPE)。分层处理(早抛晚捕):异常应该「在能真正处理它的层」被捕获——底层只管抛/转换(它不知道业务该怎么办),到上层(Controller 或全局异常处理器)统一捕获处理(返回错误页/JSON)。别在每一层都 try-catch(既啰嗦又让底层做了不该它做的决定)。这和前面 Spring 的 @ControllerAdvice 全局异常处理是一脉相承的——底层抛、上层统一处理。理解「精确捕获 + 早抛晚捕分层处理」,就掌握了异常处理的架构层面实践。

记忆钩子:「异常链=转换异常时把原异常作 cause 附上(new Xxx(‘msg’, e)),保留完整 Caused by 因果链,别丢 cause;自定义异常一般继承 RuntimeException、必带 (String,Throwable) 构造器、可加错误码;最佳实践:别空 catch 吞异常、日志用 log.error(‘msg’, e) 带堆栈、别用异常控正常流程(创建开销大要填充堆栈)、精确捕获、早抛晚捕分层处理、资源用 try-with-resources」

七、常见误区与追问

  • 误区:转换异常时只 throw new Xxx("msg") 就行。 会丢失原始异常堆栈——必须把捕获的异常作为 cause 传入 throw new Xxx("msg", e),保留异常链(Caused by),否则排查时找不到根因。
  • 误区:空 catch 或只打 e.getMessage() 没关系。 空 catch 吞异常导致问题无声消失、无法追查;只打 message 丢了堆栈定位困难;应 log.error("上下文", e) 打完整堆栈。
  • 误区:用异常控制正常流程无所谓。 异常创建要填充堆栈(fillInStackTrace)开销大(比正常调用慢几十上百倍),频繁抛异常控流程有严重性能问题,且代码难读;正常判断用 if。
  • 误区:自定义异常都该继承 Exception(受检)。 大多数业务异常继承 RuntimeException(非受检、灵活、Spring 事务默认回滚);只有「调用方必须处理」的才用受检 Exception。
  • 追问:怎么保留异常链? 抛新异常时把捕获的原异常作为 cause 传入——用带 cause 的构造器 new Xxx("msg", e)initCause(e);打印时 Caused by: 会一层层追溯到根因。
  • 追问:为什么不能用异常控制正常流程? 异常创建时要填充堆栈信息(fillInStackTrace),遍历调用栈开销大,频繁抛出性能差;异常应只用于「意外/错误」,正常分支用条件判断。
  • 追问:自定义异常应该继承 RuntimeException 还是 Exception? 大多数继承 RuntimeException(非受检,不强制 try-catch,用起来灵活,配合全局异常处理,Spring 事务默认对它回滚);只有「必须让调用方处理」的少数场景用受检 Exception。

八、加强记忆

异常链(Exception Chaining) 是「转换异常时把原异常作为 cause(原因) 附上」——throw new BusinessException("下单失败", e),既抛出合适层次的业务异常(友好、屏蔽底层),又保留原始异常的完整堆栈(排查能追溯根因),打印时显示 Caused by: 一层层追到底。最容易犯的错是重新抛异常时忘了传 causethrow new Xxx("msg")),会彻底丢失根因。自定义异常一般继承 RuntimeException(非受检、灵活、Spring 事务默认回滚,少数「必须处理」的才用受检 Exception),必须提供带 (String, Throwable) 的构造器(否则建不了异常链),可加业务错误码配合全局异常处理。异常处理最佳实践:① 绝不空 catch 吞异常、② 日志用 log.error("上下文", e) 打完整堆栈(别只打 message)、③ 别用异常控制正常流程(创建异常要填充堆栈 fillInStackTrace、开销大,正常判断用 if)、④ 精确捕获具体异常(别一律 catch Exception)、⑤ 早抛晚捕分层处理(底层抛/转换、上层统一捕获,别每层都 try-catch)、⑥ 资源用 try-with-resources。一句话「异常链传 cause 保因果、自定义继承 RuntimeException 带 cause 构造器、别吞异常别只打 message、别用异常控流程、精确捕获早抛晚捕」。