Logpoints 实战指南:使用 IntelliJ IDEA 日志断点高效调试
现代开发工具,尤其是 IntelliJ IDEA,拥有如此全面的调试支持,以至于几乎任何小众用例都有专门的工具来应对。这可能会让人不知从何下手。如果您是调试工具的新手,并希望在学习投入上获得最大回报,那么最好的起点功能就是日志断点。
即使使用它们就像用普通的 println 语句进行调试一样简单(甚至可以说更简单),但它们极大地拓宽了您可以调试的问题范围。对于某些问题,日志断点是唯一实用的方法。而对于其余问题,日志断点提供的便利可以节省大量时间和精力。
此外,在 IntelliJ IDEA 2026.2 中,日志断点获得了一些非常酷的改进,所以现在是探索它们的完美时机。
我们的示例问题
这里有一个迷你客户端和服务器,它们使用 gRPC 进行通信。服务器有一个 bug,导致它为某些租户返回错误的折扣值。
因此,我们将遵循通常的调试流程。我们将重现问题,设置对服务器内部运作的可观测性,发送一个有问题的请求,并观察它是如何产生错误结果的。
重现 bug
为了模拟在另一个环境中运行,项目捆绑了一个 Dockerfile,并暴露了监听端口和调试端口。您可以使用提供的 GrpcQuoteServerContainer 运行配置,或直接从命令行启动它:
然后,对于有问题的请求,使用 GrpcQuoteClientLoop 运行配置,它会定期查询服务器。这样我们就不用手动发送请求,而可以更专注于服务器上发生的事情。
当服务器和客户端循环都在运行时,控制台显示如下:
但我们期望看到的是:
附加到服务器
服务器不是从本地 IntelliJ IDEA 调试会话启动的,但它监听调试器连接,因此我们仍然可以使用提供的 GrpcQuoteServer 附加运行配置来附加到它。
值得注意的是,对于调试器来说,无论进程是在本地、独立环境还是远程主机上运行,都没有区别。在所有三种情况下,通信都通过套接字进行,因此我们的练习对于调试任何 Java 进程都是有效的,无论其位置如何。
日志断点
日志断点与 println 语句类似,它们不会暂停程序,而只是将必要的详细信息记录到控制台。然而,与 println 语句不同的是,它们可以在不重新构建或重新部署应用程序的情况下进行更改。
您可能已经知道如何设置日志断点,但 IntelliJ IDEA 2026.2 引入了一种更快的方法。单击任意两个可执行行之间的装订线区域,然后输入要记录的表达式。作为起点,我们可以使用查询处理方法(QuoteEndpoint:12)的开头:
[LOADING...]
警告: 注意热路径中的重计算。它们在同一 VM 中执行,并非凭空免费。从 2026.2 版本开始,IntelliJ IDEA 通过插桩移除了调试器引入的开销,但繁重的日志记录表达式仍可能需要时间执行。
对于每个传入请求,控制台现在会打印:
现在,在请求循环运行的情况下,我们可以逐步更改和添加日志断点,直到输出指向 bug。只需添加更多日志断点或更新现有的日志断点,并在新请求到来时观察控制台中的新消息。
在跟踪调用链之后(这使我们能够排除最初可能有的任何怀疑),我们到达了 discountBpsFor() 方法:
[LOADING...]
控制台指出租户名称未被正确规范化:
此外,未应用折扣这一点告诉我们,包含正确折扣的代码块从未被进入。规范化租户名称应该可以修复这个 bug:
专业提示:当不确定特定的控制台输出是由什么产生时,单击控制台中的该行,IntelliJ IDEA 将带您到相关的日志断点或代码段: [LOADING...]
即使您使用 println 语句进行日志记录,只要您使用 IntelliJ IDEA 的调试器运行进程,导航功能也会对您起作用。
测试修复
让我们趁热打铁测试一下修复。日志断点用于日志记录,而不是修改程序,但实际上没有什么能阻止我们测试特定修复的行为: [LOADING...]
原型按预期工作:
现在我们已经测试了修复,我们可以更改实际的服务器代码。
为什么不使用 println 语句?
您可能认为日志断点看起来像是更漂亮的 println 语句。从某种意义上说,确实如此,因为核心机制是相同的。在两种情况下,您都以不影响程序运行方式的简单方式添加探针。
尽管有相似之处,但日志断点可能是更好选择的几个原因:
- 它们不会使代码变得杂乱,这意味着您不必花费精力进行清理(也不会将多余的日志断点提交到生产环境)。
- 它们在记录什么以及何时记录方面非常灵活。例如,如果您只想对频繁事件进行采样,可以这样配置。
- 它们允许您在依赖项内部插入日志记录(我们很快就会这样做)。
- 对于此场景最重要的是,它们可以为您节省代价高昂的重新部署。仅为了在玩具项目中添加日志记录而重新运行本地 Docker 容器可能可以接受,但在大型实际项目中通常不行。
这些优势使日志断点与 println 语句区别开来,并使它们更像它们所是的专业调试工具。
为什么不使用常规断点?
在使用调试器时,大多数开发人员会倾向于使用断点。但在我们一直在研究的特定场景中,日志断点更合适,这不仅仅是偏好问题。
让我们看看如果使用常规断点会发生什么。附加到服务器后,在 GrpcQuoteServer.java:55 处设置一个行断点。来自循环的下一个请求将暂停服务器:
[LOADING...]
但在查看程序状态并单步执行几次后,我们发现自己进入了取消路径: [LOADING...]
一旦我们到了那里,就没有有用的信息,因为这不是请求通常结束的地方。发生这种情况是因为客户端设置的截止时间已过期,服务器放弃了对该请求的进一步处理。您可以看到 IntelliJ IDEA 将不会执行的代码部分灰显了。为了重现有问题的状态,我们必须一个接一个地发送请求,并在超时窗口内完成调试工作。
发生这种情况是因为我们的客户端为远程调用设置了截止时间。与典型的 HTTP/REST 客户端超时不同,在后者中超时仅在客户端发出失败信号,而 gRPC 可以将客户端的截止时间传播到服务器。因此,客户端实际上可以取消服务器端的工作,而不仅仅是停止等待响应。
另一方面,日志断点为我们提供了与在调试器 UI 中相同的信息,只不过我们是在控制台中观察它。重要的是,使用它们不会暂停服务器,因此我们可以在不触发超时的情况下提取所需的信息。
额外技巧:移除超时
如果您更倾向于针对此场景的另一种方法,这里还有另一种调试方式。对于我们的 gRPC 示例,问题出在超时上,我们可以在运行时使用……日志断点来移除它!
正如您刚刚看到的,日志断点表达式可以通过副作用修改正在运行的程序。在这里,我们可以使用这种技术来调整传入的请求。
首先,找到设置超时的库方法。我们可以使用几种方法。其中之一是 io.grpc.internal.ServerImpl.createContext:
[LOADING...]
在该方法中,我们可以在 timeoutNanos 局部变量赋值后立即重写它的值:
[LOADING...]
设置此日志断点后,每次从请求头读取 gRPC 超时值时,它都会立即被替换为五分钟的截止时间。这意味着我们可以再次暂停服务器。
如果您只想为重现器请求延长超时,并保持服务器正常运行——例如,如果它运行在共享的暂存实例上——您可以在日志断点内使用多行逻辑: [LOADING...]
以下是复制到日志断点的代码:
多行表达式解析请求头,并仅对带有 Debug 头(我们的测试客户端会添加)的请求延长超时。其他请求保持正常的截止时间。if 分支返回 "Timeout reset",确认该分支何时被访问。
专业提示: 通过将日志断点与标记对象功能结合使用,您可以在日志断点表达式字段中访问任意对象。这篇文章更详细地介绍了该技术。
ij-debugger AI 代理技能
当然,上述方法需要熟悉该库或花时间探索它。如果您两者都没有,只想快速更改运行时行为,您可以使用捆绑的 ij-debugger AI 代理技能将任务委托给 AI 代理:
[LOADING...] [LOADING...]
AI 代理遵循我们手动遵循的相同路径,找到读取截止时间的位置,并生成一个仅更改我们关心的请求的日志断点表达式。因此,即使事先不了解 gRPC 内部结构,我们仍然可以找到有针对性的解决方法并继续调试。
结论
在本文中,我们研究了一个用例,其中日志断点提供了比断点或 println 日志记录更简单、更优雅的替代方案。我们:
- 附加到远程进程
- 在运行时使用日志断点添加诊断
- 动态调整日志记录
- 在不更改应用程序代码的情况下测试修复
- 更改正在运行的服务器的超时
- 使用
ij-debuggerAI 代理技能
我希望您学到了一些新东西,并且下次当 println 语句或断点碍事时,您有了更好的选择。在本系列的下一篇文章中,我们将探讨调试器插桩,这是使新日志断点如此快速的基本机制。
调试愉快!