调试异步代码
最后修改时间:2023 年 8 月 23 日启用/禁用:设置 | 构建、执行、部署 | 调试器| 异步堆栈跟踪 | 仪表代理
调试异步代码是一项挑战,因为任务通常在一个线程中调度并在另一个线程中执行。每个线程都有自己的堆栈跟踪,因此很难弄清楚线程启动之前发生了什么。
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()
,有两个堆栈跟踪可供我们使用:
当前线程(工作线程)
主线程(任务被调度的地方)
使用异步注释
异步堆栈跟踪可与 Swing 和 Java 并发 API 一起使用,但也可以手动扩展以使用您自己的自定义类。这是使用特殊注释完成的。
笔记
为了使用异步注释,您需要将注释库包含到您的项目中。如果无法添加依赖项,您可以创建自己的注释。
注释用于定义捕获点和插入点:
捕获点是捕获堆栈跟踪的方法。在捕获点,堆栈跟踪被存储并分配一个键。捕获点用
@Async.Schedule
注释标记。插入点是一种将先前存储的堆栈跟踪之一附加到当前堆栈的方法。堆栈通过键进行匹配。插入点标有
@Async.Execute
注释。键是一个参数或对象引用,用作捕获的堆栈跟踪的唯一标识符。
定义捕获点和插入点
您可以注释方法或其参数:
如果您希望将对象引用 (
this
) 用作键,请注释方法本身,例如:@Async.Schedule private static void schedule(Integer i) { System.out.println("Scheduling " + i); queue.put(i); }
如果希望参数值作为key,请在方法参数上进行注释,例如:
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 Maven 存储库添加为项目的依赖项,则可以定义自己的注释并使用它们来代替默认注释。
为捕获点和插入点创建您自己的注释(您可以使用Async.java作为参考)。
按打开 IDE 设置,然后选择“构建”|“ 执行 | 部署| 调试器| 异步堆栈跟踪。CtrlAlt0S
单击配置注释。
在“异步注释配置”对话框中,单击将自定义注释添加到“异步计划注释”和“异步执行注释”。
高级配置
基于注释的方法依赖于检测代理,并且在大多数情况下都有效。有一种方法可以将所有工作完全委托给调试器。如果您满足以下条件,则可能需要这样做:
需要捕获局部变量
不能使用注释
无法使用检测代理
提示
在幕后,这种方法使用不可见的断点而不是注释。当到达此类不可见断点时,将计算指定的表达式,然后使用其结果来匹配异步堆栈跟踪的各个部分。
灵活性和性能之间需要权衡。对于性能至关重要的高并发项目,不建议使用此选项。
按打开 IDE 设置,然后选择“构建”|“ 执行 | 部署| 调试器| 异步堆栈跟踪。CtrlAlt0S
单击并提供以下信息:
捕获类名:应捕获堆栈跟踪的类的完全限定名称,例如,
javax.swing.SwingUtilities
捕获方法名称:不带参数列表和括号的方法名称,例如,
invokeLater
捕获关键表达式:其结果将用作关键的表达式。在表达式中,您可以使用框架上下文中可访问的所有内容。方法参数可以指定为
param_N
,其中N
是参数的从零开始的数字。示例:doRun
或param_0
插入类名:应匹配堆栈跟踪的类的完全限定名称,例如,
java.awt.event.InvocationEvent
插入方法名称:不带参数列表和括号的方法名称,例如,
dispatch
插入键表达式:其结果将用作键的表达式。在表达式中,您可以使用框架上下文中可访问的所有内容。方法参数可以指定为
param_N
,其中N
是参数的从零开始的数字。例子:runnable
笔记
评估复杂的表达式和方法调用可能会影响性能。
(可选)如果您还想捕获局部变量(基元和字符串值以及调用堆栈),请选择“捕获局部变量”选项。请注意,这可能会减慢调试过程。
您可以从以下存储库下载其他捕获设置:IntelliJ IDEA 调试器捕获点
查看远程 JVM 中的异步堆栈跟踪
如果您正在调试远程进程(例如在Docker容器中管理的进程),您仍然可以使用 JVM Instrumenting Agent 来显示异步堆栈跟踪,就像从 IDE 启动一样。
要远程使用代理,请执行以下操作:
将 <IDEA 安装文件夹>/lib/rt/debugger-agent.jar 复制到远程计算机上的任何位置
添加
-javaagent:<path to debugger-agent.jar>
到远程 JVM 选项
感谢您的反馈意见!