Debug

223 阅读6分钟

一、Debug 简介

Debug用来追踪代码的运行流程,通常在程序运行过程中出现异常,启用Debug模式可以分析定位异常发生的位置,以及在运行过程中参数的变化。通常我们也可以启用Debug模式来跟踪代码的运行流程去学习三方框架的源码

为了方便使用,在设置里勾选Show debug window on breakpoint,则请求进入到断点后自动激活Debug窗口。

二、Deubg 操作

2.1、Debug 模式下的界面

  1. Debug 模式启动服务。在开发中,我一般会直接以 Debug 模式运行程序,方便随时调试代码。
  2. 断点,我们可以在行数栏左侧直接单击设置,也可以使用快捷键 Ctrl+F8 设置或者取消断点。
  3. Debug 窗口:当请求到达第一个断点后,Debug 窗口会被激活。
  4. 调试 Debug 按钮:我们在调试过程中主要使用这几个按钮,鼠标悬浮按钮上面可以显示快捷键。
  5. Debug 服务按钮:在这里我们可以开启、关闭 Debug 服务等。
  6. 方法区:这里会显示调试过程中执行的方法。
  7. 参数区:这里会显示当前断点前所有参数的值。

2.2、开启Debug

先设置一个断点,然后以 Debug 模式运行: 注:我们还可以在执行程序的过程中添加/删除断点。

三、Debug 中常用调试按钮

3.1、跳转到当前执行代码的行

我们先在一个页面设置一个断点,然后再切换到其他页面,点击这个按钮,发现又跳转到了执行代码所在的行:

3.2、步过

步过就是一步一步往下走,跳过所有方法: 上面的例子中即便是遇到了 system 打印方法和 test1 方法,也会跨过去继续往下执行。

3.3、步入

在执行的过程中如果遇到了自定义的方法,可以进入方法内部,不会进入 JDK 类库中的方法。 上面的例子中,遇到了 system 方法会自动跨过去,但是遇到了自定义的方法,则会进入到方法中执行,等执行完则会返回到方法的调用处。

3.4、智能步入

如果一行代码里有好几个方法,怎么只选择某一个方法进入。使用Step Into (Alt + F7) 或者Force Step Into (Alt + Shift + F7)进入到方法内部,但这两个操作会根据方法调用顺序依次进入,这比较麻烦。 那么智能步入就很方便了,智能步入这个功能在Run里可以看到Smart Step Into (Shift + F7),如图: 图片 按Shift + F7,会自动定位到当前断点行,并列出需要进入的方法,如下图点击方法进入方法内部。如果只有一个方法,则直接进入。 图片

3.5、强制步入

不管遇到 JDK 的类库方法还是自定义方法,都会进入到方法中执行。 上面的例子中不管遇到了JDK 类库中的 system 方法还是自定义的 test1方法,都会进入到方法中执行。

3.6 步出

步出就是从进入的方法内部退回到方法调用处。 上面的例子中我们进入到了 test1 方法的内部,当点击步出按钮后,又回到了调用 test1 方法的地方。

3.6 回退断点处

回退断点处意思就是可以回退到指定方法的调用处。 上面的例子中我们依次执行了 test1、test2、method2 方法,但是我们可以选择直接回退到 test1 方法的调用处。

步出和回退断点的区别:

  • 都是回到方法的调用处
  • 步出只能回到当前方法的调用处
  • 回退断点可以回到指定方法的调用处,前提是该方法已经被执行过。

3.7、定位到光标处

如果我们写的代码有几百行,一步一步往下执行也比较费时间。这时候我们可以先把光标放到一个指定位置,然后点击定位光标处按钮,这时候代码就会立即执行到光标处了。 上面的例子中我们把鼠标光标移动到了下面某一行,然后点击定位光标处按钮,代码立刻执行到了这一行。

3.8、计算表达式

计算表达式可以帮助我们计算一些表达式的返回值。 从上面的例子中可以看出,我们可以在调用某些方法之前使用一些自定义参数去计算该方法的返回值。

3.9、中断Debug

想要在Debug的时候,中断请求,不要再走剩余的流程了。有些时候,我们看到传入的参数有误后,不想走后面的流程了,怎么中断这次请求呢(后面的流程要删除数据库数据呢),难道要关闭服务重新启动程序?确切的说,我也没发现可以直接中断请求的方式(除了关闭服务),但可以通过Force Return,即强制返回来避免后续的流程, 点击Force Return,弹出Return Value的窗口,我这个方法的返回类型为Map,所以我这里直接返回 results来强制返回,从而不再进行后续的流程。或者你可以new HashMap<>()中断断点.gif

四、查看参数

4.1、参数所在行后面显示

4.2、光标悬浮查看

光标悬浮到参数上,显示当前变量的信息,我经常使用这种方法,特别方便。

4.3、在Variables里查看

这里显示当前方法里的所有变量。

4.4、在Watches里查看

Watches 里,点击New Watch,输入需要查看的变量:

五、Debug服务设置按钮

5.1、执行到下一个断点处直到结束

该按钮的作用:如果下面有断点,就跳转到下一个断点处。如果没有,程序就执行结束。

5.2、断点静音

有时候我们在执行到某一步的时候已经知道了结果,但是后面还有一堆断点。我想让这些断点失效,但是第二次跟踪我还想用这些断点,这时候就可以使用断点静音

5.3、查看/清除断点

我们在执行完代码后要清除所有断点,但是一个个去清除太浪费时间,这时候就可以使用这个按钮查看所有设置的断点,或者清除所有断点。

如下图:点击View Breakpoints (Ctrl + Shift + F8),查看所有断点。

  • Java Line Breakpoints 显示了所有的断点,在右边勾选Condition,设置断点的条件。
  • 勾选Log message to console,则会将当前断点行输出到控制台。
  • 勾选Evaluate and log可以在执行这行代码是计算表达式的值,并将结果输出到控制台。 image.png 右边的Filters过滤,一般情况下不常用。
  • Instance filters:实例过滤,输入实例ID。
  • Class filters:类过滤,根据类名过滤。
  • Pass count:用于循环中,如果断点在循环中,可以设置该值,循环多少次后停在断点处,之后的循环都会停在断点处。

5.4、返回到第一个断点的地方

代码执行到某一行想返回到第一个断点处:

六、断点类型

6.1、条件断点

有时候我们代码中会包含很多 for 语句,但是使用断点调试的时候会执行很多次。这时候我们可以选择断点,鼠标右键设置一个条件,只有满足该条件时,断点才会执行到此处。 上面的例子中我们给断点设置一个条件:i==50,所以以 Debug 模式运行该程序的时候我们发现此时 i 就是50。

6.2、异常断点

通过设置异常断点,在程序中出现需要拦截的异常时,会自动定位到异常行。我这里添加了一个NullPointerException异常断点,Debug启动项目,当出现空指针异常时,自动定位在空指针异常行。 异常断点.gif

6.3、字段断点

如果你阅读源码,你一定会有个困扰,类中的某个字段的值到底是在哪里改变的,你要一点点追踪调用栈,逐步排查,稍不留神,就可能有遗漏。

我们可以在IntelliJ IDEA中为某个字段添加断点,当字段值有修改时,自动跳到相应方法位置。

  1. 在字段定义处鼠标左键添加断点(会出现「眼睛」的图标)。
  2. 在「眼睛」图标上鼠标右键。
  3. 在弹框中勾选上 Field access 和 Field modification 两个选项。 图片 如果修改字段值的方法比较多,也可以在 Condition 的地方定义断点进入条件。

6.4、方法断点

当阅读源码时,比如Spring,一个接口的方法可能被多个子类实现,当运行时,需要查看调用栈逐步定位实现类,IDEA 同样支持在接口方法上添加断点(快捷键 cmd+F8/ctrl+F8):

  1. 鼠标左键在方法处点击断点(♦️形状)。
  2. 断点上鼠标右键勾选上绿色框线上的内容,同样可以自定义跳转条件Condition图片

当以Debug模式运行程序的时候,会自动进入实现类的方法(注意断点形状): 图片

七、断点处添加日志

我们在调试代码时都喜欢打印一些内容,这样看起来更直观,打印完之后又很容易忘记删除掉这些没用的内容,最终将代码提交到远程仓库,合并的时候又不得不删减这些内容重新提交,不但增加不必要的工作量。

IntelliJ IDEA 提供 Evaluate and Log at Breakpoints 功能恰巧可以帮助我们解决这个问题, 来看下面代码:

public static void main(String[] args) {
  ThreadLocalRandom random = ThreadLocalRandom.current();
  int count = 0;
  for (int i = 0; i < 5; i++) {
   if (isInterested(random.nextInt(10))) {
    count++;
   }
  }
  System.out.printf("Found %d interested values%n", count);
 }

 private static boolean isInterested(int i) {
  return i % 2 == 0;
 }

假如我们想在第 15 行查看每次调用,随即出来的 i 的值到底是多少,我们没必要在这个地方添加任何log,在正常加断点的地方使用快捷键 Shift + 鼠标左键,就会弹出下面的内容:

图片

勾选上 Evaluate and log, 并自定义你想查看的变量,比如这里的 "interested" + i, 这样以Debug模式运行程序(正常模式运行,不会打印这些信息):

interested 7
interested 5
interested 1
interested 2
interested 0
Found 2 interested values

如果你在多处添加了这种断点,简单的看 log 可能偶尔还是不够直观,可以勾选上面图片绿色框线的 "Breakpoint hit" message :

Breakpoint reached at top.dayarch.TestDebug.isInterested(TestDebug.java:49)
interested 6
Breakpoint reached at top.dayarch.TestDebug.isInterested(TestDebug.java:49)
interested 0
Breakpoint reached at top.dayarch.TestDebug.isInterested(TestDebug.java:49)
interested 9
Breakpoint reached at top.dayarch.TestDebug.isInterested(TestDebug.java:49)
interested 8
Breakpoint reached at top.dayarch.TestDebug.isInterested(TestDebug.java:49)
interested 1
Found 3 interested values
Disconnected from the target VM, address: '127.0.0.1:0', transport: 'socket'

Process finished with exit code

如果你想要更详细的信息,那就勾选上 Stack trace

八、流式调试

先来看下面这段代码:

public static void main(String[] args) {
  Object[] res = Stream.of(1,2,3,4,5,6,7,8).filter( i -> i%2 == 0).filter( i -> i>3).toArray();
  System.out.println(Arrays.toString(res));
}

我们可以在Stream操作处打上断点,逐步查看结果,就像这样: 图片 打开可视化调试,让我们快速查看我们Stream结果,同样先选择行断点,以 Debug 模式进入程序: 图片 接下来会弹出 Stream Trace,整个 Stream 操作尽显眼前 图片 同样可以点击左下角的 Flat Mode 按钮,将整个视图扁平化。

图片

在实际业务中,我们通常对集合进行各种Stream操作,我们再来个复杂一些的例子:

List<Optional<Customer>> customers = Arrays.asList(
    Optional.of(new Customer("日拱一兵"18)),
    Optional.of(new Customer("卑微的小开发"22)),
    Optional.empty(),
    Optional.of(new Customer("OOT"21)),
    Optional.empty(),
    Optional.of(new Customer("温柔一刀"23)),
    Optional.empty()
  );

  long numberOf65PlusCustomers = customers
    .stream()
    .flatMap(c -> c
      .map(Stream::of)
      .orElseGet(Stream::empty))
    .filter(c -> c.getAge() > 18)
    .count();

  System.out.println(numberOf65PlusCustomers);

同样按照上面的操作得到可视化Stream Trace视图,直观了解整个Stream流程,查看对象属性等 图片

九、多线程调试

因为CPU执行线程的顺序是随机的,但是我们使用断点调试可以自定义执行下一个线程。 首先将两个线程的断点都设置成线程模式: 然后在方法区选择指定的线程去执行:

十、总结

IDEA支持的各种断点调试类型: 图片