异常堆栈别靠猜:五个高频 Exception 的定位清单与排查顺序
写代码最怕的不是报错,而是报错之后对着堆栈发呆。尤其是线上环境,日志打出来一大片,真正有用的信息往往被淹没在中间。这篇帖子不聊框架原理,就聊实际排查时怎么从异常信息里快速找出问题源头。
1. NullPointerException:先看行号,再看调用链
NPE 是最常见的,但也是最容易被误判的。很多人一看到 NPE 就认为是某个对象没初始化,其实很多时候是链式调用的中间某一步返回了 null。定位思路:先看堆栈顶部的行号,定位到具体代码行;然后从这一行往前推,看哪个方法调用可能返回 null。如果是 MyBatis 或 JPA 查询结果直接拿来用,优先检查查询条件是否可能查不到数据。
2. ClassCastException:别只盯着类型转换那行
这种异常经常出现在泛型擦除、JSON 反序列化、或者从 Map 里取值强转的场景。堆栈会指向强转的那行,但真正的根源往往在数据来源。比如从 Redis 取对象时用了错误的序列化方式,或者接口返回的 JSON 字段类型和实体类不一致。排查顺序:先确认数据是从哪里来的(缓存、接口、数据库),再检查对应字段的类型定义,最后才看强转代码本身。
3. IndexOutOfBoundsException:数组越界和集合越界要分开查
数组越界通常是循环边界算错了,集合越界则多半是 remove 或 get 时用了错误的索引。一个容易忽略的点:subList 返回的是视图,对它进行结构性修改会影响原列表,这时候索引很容易错位。快速定位方法:在异常行打断点,查看当前集合的实际 size 和传入的 index,对比一下就能看出是边界条件写错了还是数据本身不符合预期。
4. SQLException:不要只看 message,要看 SQLState 和错误码
SQL 报错信息里除了人类可读的 message,还有两个关键字段:SQLState(五位数)和 vendor code(数据库厂商错误码)。比如 MySQL 的 1062 是唯一键冲突,1451 是外键约束失败。遇到 SQLException 先别急着复制 message 去搜,先把这两个值记下来,直接查对应数据库的官方错误码表,定位速度会快很多。另外,堆栈里通常会有 SQL 语句和参数列表,检查参数是否为空或类型不匹配。
5. ConcurrentModificationException:遍历时修改集合的典型症状
这个异常在单线程环境下也会出现,不一定是并发问题。最常见的是在 foreach 循环里直接调用 list.remove()。定位思路:看异常堆栈指向的代码行,如果是 for-each 语法,改成 Iterator 显式遍历,然后调用 iterator.remove()。如果修改操作在另一个线程里,那就要检查共享集合有没有做同步控制。一个实用的排查技巧:在异常发生前打印集合的 hashCode,如果两次打印不一致,说明集合被重新赋值了,而不是原地修改。
实战排查顺序总结
拿到一段完整堆栈时,我习惯按这个顺序来:
① 看异常类型,判断是哪一类问题;
② 看堆栈最顶部三行,定位到具体类和方法;
③ 看 Caused by 部分,很多时候真正的根源在最后一个 cause 里;
④ 如果堆栈里有业务代码行号,优先看那个,而不是框架内部的方法;
⑤ 最后再结合日志上下文(比如请求参数、用户 ID)复现问题。
异常信息是程序给我们的第一手线索,大部分问题靠仔细读堆栈都能找到方向。与其反复试错,不如把常见异常的特征和排查路径整理成自己的 check list,下次遇到同类问题就能直接按图索骥了。