3. 断点艺术

调试这件事,说白了就是跟代码「对质」。你问它为什么这么跑,它不吭声。你加个断点,它才老实交代。

但很多人用断点,还停留在「点一下,停住,看变量」的阶段。我刚开始做项目时也这样。直到有一次线上问题排查了整整两天,最后发现是循环里一个边界条件写错了。从那以后,我开始认真研究断点的各种玩法。

这一章,我把这些年攒下的断点心得全掏出来。你想想看,如果断点能带条件、能打日志、能自动捕获异常,甚至能分组管理……调试效率能翻多少倍?

核心观点:断点不是「暂停工具」,而是「代码探针」。用好了,你就是在代码里装了一双眼睛。

3.1 条件断点:只在需要的时候停

最常见的场景是什么?循环里某个特定值出问题了。你总不能每次循环都停一次吧?

条件断点就是干这个的。它只在条件满足时才触发暂停。

// 假设有个循环,i=1024 时数据异常
for (int i = 0; i < 2048; i++) {
    process(data[i]);
}

你可以在断点上右键,设置条件为 i == 1024。嗯,就这么简单。

我个人习惯用条件断点来排查「偶发bug」。比如某个用户ID为 10086 的订单总是算错金额,其他用户都正常。加个条件 userId == 10086,一跑就中。

小技巧:条件表达式里可以调用方法,但要注意性能。如果条件里写了个耗时的数据库查询,那每次循环都会执行一次。我建议只使用简单的变量判断。

3.2 日志断点:不停下来也能看现场

有些场景你不想停。比如生产环境模拟、高并发链路、或者动画渲染过程。一停就破坏了时序。

日志断点(也叫 Tracepoint)就是答案。它不暂停程序,只是在断点位置输出一行日志。

在 IntelliJ IDEA 里,右键断点,勾选「Log message to console」。在 VS Code 里,右键断点选「Logpoint」。

// 日志断点输出示例
[DEBUG] process() called - userId: 10086, amount: 299.00, timestamp: 2024-01-15 10:30:22

我曾经排查过一个「用户点击按钮后页面卡死」的问题。UI 线程不能停,停了就复现不了。用日志断点把每次事件分发的参数打出来,很快就发现是某个回调里传了个死循环引用。

注意:日志断点在生产环境慎用。如果日志量太大,会拖慢程序。我一般只在开发环境或预发布环境用。

3.3 异常断点:抓住那些「偷偷溜走」的错误

你有没有遇到过这种情况:程序抛了个异常,但 try-catch 吞掉了,你根本不知道哪里出的问题?

异常断点就是用来抓这种「幽灵异常」的。它可以在异常被抛出的一瞬间就停住,不管有没有被捕获。

设置方式很简单:在断点面板里添加「Exception Breakpoint」,指定异常类型,比如 NullPointerException 或 ArrayIndexOutOfBoundsException。

异常类型 常用场景 建议
NullPointerException 空指针排查 必加,几乎所有项目都有用
IllegalArgumentException 参数校验失败 调试框架代码时很有用
自定义业务异常 订单状态异常、库存不足等 配合条件断点使用效果更佳

我记得有一次,线上日志里全是「系统繁忙」的提示,但就是找不到根因。后来加了异常断点,捕获到一个 SQLException,发现是连接池泄漏了。如果没有异常断点,这个异常被最外层的 catch 吞掉,你根本不知道具体位置。

3.4 断点组管理:别让你的断点变成一团乱麻

项目大了,断点也多了。有时候一个模块调试完,另一个模块的断点还在。你想想看,十几个断点同时生效,跑起来到处停,烦不烦?

断点组就是用来解决这个问题的。你可以把断点按功能分组,比如「登录模块」、「支付流程」、「数据同步」。需要调试哪一组,就启用哪一组。

在 IntelliJ IDEA 里,断点面板有个「Group by」功能。我习惯按模块名分组,再给每组加个颜色标签。一眼就能看出来哪些断点是当前需要的。

我的习惯:每次开始调试前,先禁用所有断点组,只启用当前要调试的那一组。调试完立刻禁用。这样不会干扰其他模块的调试。

3.5 断点的高级技巧:不止是暂停

断点还能干很多事。我挑几个最实用的说说。

3.5.1 断点命中次数

有时候你只想知道「第 N 次调用时发生了什么」。比如一个方法被调用了 1000 次,第 500 次时数据开始异常。

设置断点的「Pass count」为 500,它会在第 500 次命中时才暂停。前 499 次直接跳过。

3.5.2 断点表达式求值

断点停住后,你可以在「Evaluate Expression」窗口里执行任意代码。比如修改某个变量的值,或者调用一个工具方法。

我曾经用这个技巧修复过一个线上问题:某个配置项读错了,导致所有用户都看到错误页面。我在断点处直接执行 config.setValue("correct_value"),然后继续运行。虽然治标不治本,但至少让服务先恢复。

3.5.3 断点依赖

有些断点只有在另一个断点触发后才有意义。比如你先在「登录成功」处加个断点,然后在「下单」处加个断点。只有登录成功后,下单的断点才需要生效。

在 VS Code 或 IntelliJ 里,可以设置断点的「触发条件」或「依赖关系」。这个功能我用的不多,但在调试复杂状态机时非常有用。

避坑指南:我曾经在调试时同时启用了条件断点和日志断点,结果日志断点先触发,条件断点后触发,导致日志顺序乱了。后来我养成了一个习惯:同一位置只用一个断点类型。

3.6 知识体系总览

下面这张图,是我对断点艺术的理解。你可以把它当成一个「调试武器库」的索引。

断点艺术 · 知识体系 断点(Breakpoint) 条件断点 日志断点 异常断点 适用场景 循环特定值 偶发bug定位 用户/ID过滤 适用场景 生产环境模拟 高并发链路 UI/动画调试 适用场景 空指针排查 参数校验失败 业务异常捕获 高级技巧 命中次数控制 表达式求值 断点依赖/分组

这张图把断点分成了三大类加一组高级技巧。你不需要一次性全记住。先挑一个最常用的练手,比如条件断点。用熟了再学下一个。

调试这件事,说到底就是「用最小的成本获取最多的信息」。断点就是你手里的探针。探针越多、越精准,你定位问题的速度就越快。

嗯,这一章就到这里。记住:别让断点成为你的负担,让它成为你的武器。

一句话总结:条件断点省时间,日志断点不打扰,异常断点抓幽灵,分组管理不混乱,高级技巧定乾坤。