把底层异常包装成业务异常再往上抛,是几乎每个 Java 项目都在用的模式。这篇真实验证 cause 链的两种设置方式,以及一个容易被忽视的约束:initCause() 真实只能调用一次。
上一篇:switch 穿透真实复现
最常见的异常包装方式:catch住底层异常,通过构造器把它作为cause传给新抛出的业务异常。
ExceptionChainDemo.java try { lowLevel(); // 抛出 IllegalStateException("底层:数据库连接失败") } catch (IllegalStateException e) { throw new RuntimeException("中层:业务操作失败", e); }
真实捕获的异常: 中层:业务操作失败 真实的 cause: java.lang.IllegalStateException: 底层:数据库连接失败 cause 是否真实是原始异常类型: true cause.getCause() 真实是: null(底层异常没有再包装谁,所以是null)
真实结果验证了最基本的用法:RuntimeException(String, Throwable) 这个构造器会把第二个参数真实存进这个异常对象的 cause 字段,调用方后续用 getCause() 真实能拿回最初那个 IllegalStateException 对象(不是重新构造的副本,是同一个引用)。原始异常自己没有包装任何东西,所以它的 getCause() 真实是 null——这是异常链的起点。
如果用的异常类型没有带cause参数的构造器(比如一些老代码里常见的自定义异常),可以事后调用 initCause()。
initCause() 方式的 cause 真实是: java.lang.IllegalStateException: 底层:数据库连接失败
真实结果证明 initCause() 达到的效果和构造器传入完全一样——事实上,带 cause 参数的构造器内部真实就是调用了 initCause(),只是省去了手动调用这一步。
容易被忽视的约束:一旦 cause 被设置过(不管是构造器还是initCause()),再次调用initCause()会真实报错,不是静默覆盖。
ExceptionChainDemo.java RuntimeException e1 = new RuntimeException("test"); e1.initCause(new IllegalStateException("cause1")); e1.initCause(new IllegalStateException("cause2")); // 第二次调用
真实验证 initCause() 第二次调用抛出异常: Can't overwrite cause with java.lang.IllegalStateException: cause2
最后验证平时在日志里看到的"Caused by:"到底从哪来。
java.lang.RuntimeException: 中层:业务操作失败 at ExceptionChainDemo.midLevel(ExceptionChainDemo.java:10) at ExceptionChainDemo.main(ExceptionChainDemo.java:54) 堆栈里真实包含 'Caused by:' 标记: true
真实验证了 Throwable.printStackTrace() 不只是打印当前这个异常自己的堆栈——它会真实递归遍历整条 cause 链,把每一层异常的堆栈都打印出来,层与层之间用 "Caused by:" 标记分隔。这也是为什么排查线上问题时,应该重点看日志里最后一段 Caused by:(往往是最底层、最接近真实故障根源的异常),而不是只看最外层那个业务异常的消息。
每一行都是真实运行这段代码得到的结果或异常。