IntelliJ IDEA 2025.1 Help

调试异步代码

调试异步代码是一项挑战,因为任务通常在一个线程中调度,而在另一个线程中执行。 每个线程都有自己的堆栈跟踪,这使得很难弄清楚线程启动之前发生了什么。

IntelliJ IDEA 通过在不同线程中的帧之间建立连接来简化操作。 这使您能够从一个工作线程回溯到任务被调度的位置,并像在同一线程中执行一样调试程序。

要尝试异步堆栈跟踪,请调试以下示例:

import java.util.*; import java.util.concurrent.*; public class AsyncExample { static List<Task> tasks = new ArrayList<>(); static ExecutorService executor = Executors.newScheduledThreadPool(4); public static void main(String[] args) { createTasks(); executeTasks(); } private static void createTasks() { for (int i = 0; i < 20; i++) { tasks.add(new Task(i)); } } private static void executeTasks() { for (Task task : tasks) { executor.submit(task); } } static class Task extends Thread { int num; public void run() { try { Thread.sleep(new Random().nextInt(2000)); } catch (InterruptedException e) { e.printStackTrace(); } printNum(); } private void printNum() { // Set a breakpoint at the following line System.out.print(num + " "); } public Task(int num) { this.num = num; } } }

当我们在 printNum() 方法中的断点处停止时,我们有两个可用的堆栈跟踪:

  • 当前线程(worker)

  • 主线程(任务被调度的地方)

线程选项卡显示堆栈跟踪的两个部分——一个是工作线程的,另一个是调度线程的

控制台中的异步堆栈跟踪

默认情况下,当您调试代码或运行 JUnit/TestNG 单元测试时,异步堆栈跟踪会显示在控制台中。

在控制台中打印的失败测试的异步堆栈跟踪

您可以通过清除相应运行配置中的 打印异常的异步堆栈跟踪 复选框来禁用测试的异步堆栈跟踪。

运行配置对话框中的“打印异常的异步堆栈跟踪”复选框

要为调试会话启用或禁用异步堆栈跟踪,请使用 插装代理 选项,在 设置 | 构建、执行、部署 | 调试器 | 异步堆栈跟踪 中进行设置。

异步注解

异步堆栈跟踪开箱即用地支持 Swing 和 Java 并发 API,但也可以手动扩展以支持您自己的自定义类。 这是通过使用特殊注释完成的。

注解用于定义捕获和插入点:

  • 捕获点是一种捕获堆栈跟踪的方法。 在 capture points,堆栈跟踪会被存储并分配一个键。 捕获点用 @Async.Schedule 注释标记。

  • 插入点是一种方法,将以前存储的堆栈跟踪之一附加到当前堆栈。 插入点用 @Async.Execute 注释标记。

  • 一个 key是一个参数或对象引用,用作捕获的堆栈跟踪的唯一标识符。 它用于匹配异步堆栈跟踪的各部分。

定义捕获和插入点

您可以注释方法或它们的参数:

  • 如果您希望将对象引用 (this) 用作键,请对方法本身进行注解,例如:

    @Async.Schedule private static void schedule(Integer i) { System.out.println("Scheduling " + i); queue.put(i); }
  • 如果您希望将参数值用作键,请注释方法参数,例如:

    private static void schedule(@Async.Schedule Integer i) { System.out.println("Scheduling " + i); queue.put(i); }

要测试注解的工作方式,您可以使用以下示例:

import org.jetbrains.annotations.Async; import java.util.concurrent.*; public class AsyncSchedulerExample { private static final BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(); public static void main(String[] args) throws InterruptedException { new Thread(() -> { try { while (true) { process(queue.take()); } } catch (InterruptedException e) { e.printStackTrace(); } }).start(); schedule(1); schedule(2); schedule(3); } private static void schedule(@Async.Schedule Integer i) throws InterruptedException { System.out.println("Scheduling " + i); queue.put(i); } private static void process(@Async.Execute Integer i) { // Set a breakpoint at the following line System.out.println("Processing " + i); } }

定义自定义注解

如果您不想为项目添加 JetBrains 注解依赖项 ,您可以定义自己的注解并使用它们代替默认注解。

  1. 为捕获点和插入点创建您自己的注解。 参考请参见 Async.java

  2. Ctrl+Alt+S 打开设置,然后选择 构建|执行|部署|调试器|异步堆栈跟踪

  3. 点击 配置注解

  4. 异步注解配置 对话框中,点击 添加 将您的自定义注解添加到 异步调度注解异步执行注解

高级配置

基于注解的方法依赖于 instrumenting agent,并且在大多数情况下都有效。 有一种方法可以将所有工作完全委托给调试器。 这可能是必需的,如果您:

  • 需要捕获局部变量

  • 无法使用注解

  • 无法使用 instrumenting agent

在底层,此方法使用隐藏断点代替注解。 当到达这样的隐藏断点时,会评估指定的表达式,其结果随后用于匹配异步堆栈跟踪的各部分。

灵活性和性能之间存在权衡。 此选项不推荐用于性能至关重要的高并发项目。

  1. Ctrl+Alt+S 打开设置,然后选择 构建|执行|部署|调试器|异步堆栈跟踪

  2. 点击 添加 并提供以下信息:

    • 捕获类名 :捕获堆栈跟踪的类的全限定名,例如, javax.swing.SwingUtilities

    • 捕获方法名 :没有参数列表和括号的方法名称,例如, invokeLater

    • 捕获关键表达式 :其结果将用作键的表达式。 在表达式中,您可以使用框架上下文中所有可访问的内容。 方法参数可以指定为 param_N ,其中 N 是参数的从零开始的编号,例如, param_0

    • 插入类名 :应匹配堆栈跟踪的类的完全限定名称,例如, java.awt.event.InvocationEvent

    • 插入方法名称 :没有参数列表和括号的方法名称,例如, dispatch

    • 插入关键表达式 :其结果将用作键的表达式。 在表达式中,您可以使用框架上下文中所有可访问的内容。 方法参数可以指定为 param_N ,其中 N 是参数的从零开始的编号,例如, param_0

  3. (可选)如果您还想捕获局部变量(原始类型和 String 值以及调用堆栈,请选择 捕获局部变量 选项。 请注意,这可能会减慢调试过程。

您可以从以下存储库下载其他捕获设置: IntelliJ IDEA debugger Capture Points

在远程 JVM 中查看异步堆栈跟踪

如果您正在调试一个远程进程,例如在 Docker 容器中管理的进程,您仍然可以使用 JVM Instrumenting Agent 来显示 Async Stack Traces,就像从 IDE 启动一样。

若要远程使用该代理,请执行以下操作:

  • 将 <IDEA installation folder>/lib/rt/debugger-agent.jar 复制到远程机器上的任意位置

  • -javaagent:<path to debugger-agent.jar> 添加到远程 JVM 选项

最后修改日期: 2025年 4月 24日