DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
晚安云;香港 CN2 ¥15/月
搬瓦工;先看线路,再选 VPS;CN2 GIA · 对应线路套餐;查看线路 →

Spring 错误处理线程满载,检查 cause 是否形成环

异常链 A 到 B 又回到 A 时,旧 getRootCause 和断连识别可一直循环。先在加固边界识别环,避免错误处理器自身挂住。

错误处理器调用 NestedExceptionUtils.getRootCause(),或者用 DisconnectedClientHelper.isClientDisconnectedException() 判断客户端断开,线程却一直占用 CPU,没有返回。除正常的深层包装外,还应检查 cause 链是否形成了环。

具体边界是 A 的 cause 指向 B,B 又指回 A。Java 禁止异常直接把自身设为 cause,但不因此禁止两个对象通过后续 initCause 互相连接。普通 new Exception(message, cause) 的单向包装通常不会自然形成这种环;这里讨论的是刻意构造的加固样本,不是已经确认的生产事故。

只比较上一个节点,挡不住两个节点的环

Spring 6.2.19 中的 getRootCause 在 cause 为 null 或等于上一节点时结束。正式 7.0.10 仍可核对到同类条件,它能处理直接自环,却不能结束 A 到 B 到 A 的遍历。

断连识别还可能在自身扫描后调用最具体原因的查找。因此只修改外层日志格式,或者捕获 RuntimeException,不能终止一个没有抛异常的循环。

在兜底处理器进入遍历前检查图形

如果应用的兜底错误处理器接受任意 Throwable,可以在调用上述工具前,用对象身份集合和明确深度预算检查 cause。下面方法可放在 Java 工具类中:

import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;

static boolean safeCauseChain(Throwable error, int maxDepth) {
    if (maxDepth <= 0) {
        throw new IllegalArgumentException("positive depth required");
    }
    Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
    Throwable current = error;
    for (int depth = 0; current != null; depth++) {
        if (depth >= maxDepth || !seen.add(current)) {
            return false;
        }
        current = current.getCause();
    }
    return true;
}

返回 false 表示链有环或超过预算,不表示客户端必然断开。调用方应走原来的失败响应与受限诊断路径,避免继续调用会重新遍历整条 cause 的工具;也不能把它当作可忽略异常而返回业务成功。深度预算按项目的正常包装范围确定,代码没有给出跨项目通用值。

这里使用 IdentityHashMap,比较的是同一个异常对象是否再次出现,不依赖异常子类的 equals 或 hashCode。对于自行重写 getCause 且调用本身会阻塞的特殊异常,这份检查也不提供超时保证;应另外约束异常实现来源。

修复循环的生成位置,保留最初失败

环通常还意味着异常构造或复用逻辑有问题。检查清理失败是否被反向接成 cause、是否复用可变异常对象,以及序列化重建错误图的代码。多个独立失败可按既有规则使用 suppressed 异常,不要强行让两个 cause 互相指向。

在隔离样本中覆盖正常链、无 cause、两节点环和超深链,确认处理器能够结束,而且正常业务异常仍按原策略分类。保存原处理器配置,撤回局部防护时先确认异常构造路径已经修正;不要通过关闭错误处理或忽略全部 IOException 来降低 CPU 使用。