05-debug

1. 简介

  • 追踪代码流程
  • 确认运行过程中参数变化
  • 分析定位程序的错误
  • 线上问题追踪

2. 准备工作

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * Created By L
 * Description:
 */
public class ToSum {

    public static int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int sub = target - nums[i];
            // 查找余数是否在表中,有则返回两个数的索引
            // 没有则将其信息添加进哈希表
            if (map.containsKey(sub)) {
                return new int[]{map.get(sub), i};
            }
            map.put(nums[i], i);
        }
        throw new IllegalArgumentException("No two sum solution");
    }

    public static void main(String[] args) {
        int[] nums = {2, 7, 11, 15};
        int target = 9;
        int[] ints = twoSum(nums, target);
        System.out.println(Arrays.toString(ints));
    }
}

Debug 窗口没有在 IDEA 底部显示

RVRpb3JSVHM1aEsxcWRKc0c1UEdsRVR3U0RXbDZjRlkvRkVaTkp3PQ==

右键 Debug Toolbar 还可以选择,让窗口出现在上下左右任意位置

aUE0Qk1xWUFBMVZ4SE5BVWJiK0tiVVR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

3. 主界面介绍

  1. Debug:启动 Debug 功能
  2. 断点:可在左边栏单击,打上断点
  3. 服务按钮:可以在这里关闭服务、启动服务、修改 Debug 配置,设置断点等
  4. 调试按钮:共8个其中包括:Show Execution PointStep Over (F8)Step Into (F7)Force Step IntoStep OutDrop FrameRun to CursorEvaluate Expression,后面会详细解释功能
  5. Frames:显示了该线程调试所经过的所有方法,勾选右上角的漏斗(Show All Frames)按钮,就不会显示其它类库的方法了,否则这里会有一大堆的方法
  6. Variables:可以查看变量具体的值,也可以修改
  7. Watches:查看变量,可以将 Variables 区中的变量拖到 Watches 中查看,默认的 Debug 窗口,可能并没有这个窗口,你需要点击 Variables 窗口的“眼镜”👓图标,此窗口才会出现。再点击 Watches 列里“眼镜”👓图标,即可收回
  8. Debug窗口:可以设置 Debug 窗口是否固定,或显示在哪个边栏,只需右键即可选择。你甚至可以拖动该 Debug 窗口,使其以独立窗口形式存在
SFk1R0FVenF3OVVHeWhRQSswcmlqVVR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

4. 各功能键详解

接下来,我们依次讲讲服务按钮、调试按钮、Watches操作栏的作用:

RWRqdXRNOE1TK201RDNIbFBHcFFQVVR3U0RXbDZjRlkvRkVaTkp3PQ==

服务按钮

服务按钮如上图中1区所示,从上至下,共10个按钮,IDEA版本不同,可能有小部分不一致。

  • Rerun ‘xxx’:重新运行程序,点击会以Debug方式重启服务
  • Edit Run Configuration:“xxx”:更改运行时配置,其中包括运行Java版本更,环境变量等更改,还可以用于进行远程调试,之后详解
  • Resume Program (F9):恢复程序,从一个断点运行至下一个断点,没有断点则运行完整个程序
  • Pause Program:暂停程序,当我们执行某步时间太长,或遇到死循环时,可使用其暂停程序,如想继续执行,再点击Resume Program (F9)即可
  • Stop ‘xxx’ (Ctrl + F2):终止当前Debug程序
  • View Breakpoints:查看所有断点,还可选择按包、文件或class类进行断点分组,还可对断点进行一些配置,包括条件配置,多线程配置等,后面会详解
  • Mute Breakpoints:沉默所有断点,选择这个后,所有断点变为灰色,断点失效。再次点击,断点变为红色,有效。如果只想使某一个断点失效,可以在断点上右键取消Enabled,会显示空心的红圈,表示当前断点不可用
  • Get Thread Dump:导出当前堆栈信息,可用作其它分析
  • Settings:一些显示设置,可以设置是否在代码界面显示Values、Return Values等,开启即可
  • Pin Tab:是否固定当前Debug标签页

调试按钮

调试按钮如上图中2区所示,从左至右,共9个按钮,也是我们平时除服务按钮外用的最多的操作了。

  • Show Execution Point:定位,点击此按钮可定位到当前代码执行的行
  • Step Over (F8):步过,一行一行地往下走,如果执行到子方法,子方法内又无断点的情况下,会跳过子方法继续执行
  • Step Into (F7):步入,一行一行地往下走,如果执行到子方法,无论子方法有无断点,都会进入到子方法,子方法执行完后返回,继续执行
  • Force Step Into (Alt + Shift + F7):强制步入,能进入任何方法,查看底层源码的时候可以用这个进入官方类库的方法
  • Step Out (Shift + F8):步出,从步入的方法内退出到方法调用处,此时方法已执行完毕,只是还没有完成赋值
  • Drop Frame:回退断点,相当于后悔药,后面会详解
  • Run to Cursor:使程序运行至光标处,而光标处不需要打断点,运行过程中,有断点会在断点处停止
  • Evaluate Expression…:计算表达式,后面章节详细说明
  • Trace Current Stream Chain:显示Stream处理的整体过程。Lambda调试神器,之后会详解

Watches操作栏

Watches窗口是观察变量变化的窗口,其操作栏如上图中3区所示,共6个按钮。

  • new Watch…:新增观察变量
  • Remove Watch:删除观察变量
  • Move Watch Up:向上移动观察变量
  • Move Watch Down:向下移动观察变量
  • Duplicate Watch:复制观察变量
  • Show watches in variables tab:Watches不会默认打开为新窗口,点击“眼镜”👓图标后,才会打开为新窗口,再次点击会回到Variables边栏

5. 查看变量

查看变量值的方式有四种,这里我们首先确定我们的服务按钮中的Setting中有选择在代码界面显示Values、Return Values等。如下图所示即可,没找到也没关系,默认都会显示的。

MFhhVWttQXB6QXZNbDd3ZUJLZDJYVVR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

在代码行的后面,有灰色的变量值显示。

Q0N4TU4zZDZuRi8wNS9IZ00wUDdYMFR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

光标悬停到参数上,也可显示当前变量信息。如果是复杂的变量,还可以点开查看详细变量值

cVdaTDIyblhzayt5TGlyd2hrMW9Wa1R3U0RXbDZjRlkvRkVaTkp4SlFRPT0=
UkNSS3RNNDdPRXZFQWtTd0tjVjNqa1R3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

也可在Variables里查看变量值,甚至是操作变量值,这里也是变量操作最多的地方。比如,我们运行到一半,想更改某一个变量的值怎么办?重新发一次请求吗,还是重新运行一次?其实不用这么麻烦,直接更改值即可!选择 Set Value… 即可

QWtHaVFENE52emE2QUI4aEdEVmxuMFR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

按下回车,当前target变量的值就改变了,而后可以继续运行。

QU9veGVVaHJBVWtsM2dMWmNvcjdDMFR3U0RXbDZjRlkvRkVaTkp3PQ==

在Watches也可以观察变量值,我们可以通过从Variables里拖变量到Watches中观察,也可以通过点击 new Watch… 添加观察变量。

这里和Variables的区别是:Variables窗口只会显示当前代码行能看到的变量,Watches只要添加了该变量,则会一直存在Watches窗口。

bmRTMGF6RzFyS29mdDFUM1pyWjl1MFR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

6. 更改变量值

其实上一节,已经讲了如何在Variables窗口里更改变量值,这里再讲一种如何更改变量值,如下图所示,这里还可以把变量加入Watches窗口中。

N01LRWRBcC9udjV0b0k1a0FDdWY0RVR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

比如这里我们这把num[0]改成了22

TDBYNlErUFkzUkRLeHNFVG8yejZTRVR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

7. 设置断点条件

想像一种情况,我们要遍历一个大集合,我们只有当集合为null时,才停下来,那我们难道要一次一次的点过去吗,点到900次的时候,你一时手快,跳过了怎么办?其实我们可以设置断点停留的条件,当条件满足的时候才停下,否则继续运行。以下方代码为例:

a1FkTkhrdysxSTRFK3ZLUmcwVHBDa1R3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

我们想在值为null值时,停下断点,看看是哪里为null,然后去更改数据库,进行如下设置即可。

ODV1d0tNOEJHRHdNaUE5YVFQTmNKa1R3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

只有当 n == null 这个条件为空时,这个断点才会停止!

8. 计算表达式

我们在Debug时,想知道运行时某个方法的调用值或表达式的结果值,但是这个方法又没写在代码中,我们总是会添加上该方法代码,然后重新运行一次对吧。

其实不用重新运行也可以计算其值,点击Evaluate Expression…按键即可。比如,我想知道此时map.keySet()的值会是多少,直接点击Evaluate即可。

NXZjZ3NtOUpoNFNhWGl3UzRMbUlyMFR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

当然我们也可以计算一些复杂的表达式

ZmU2TTdFYkdHV0VIOHJCaDRUQkNHVVR3U0RXbDZjRlkvRkVaTkp3PQ==

这里的Evaluate功能其实就是:用当前代码的上下文环境,写出代码表达式,最后计算出临时的结果值。可方便我们的各种假设性代码实现,而不用每次都去重启代码,也不会影响原代码的正常运行。

9. 回退操作

回退操作即是Debug中的后悔药,IDEA中只能回退到上一个方法调用处,并不能一步一步回退或是回退到上一个断点处。还有一点值得注意的是,回退只是重新走一下流程,之前的某些参数/数据的状态已经改变了的,是无法回退到之前的状态的,如对象、集合、更新了数据库数据等等。

比如,main方法调用A方法,A方法里删除了数据库里的一行数据,我们也执行了,这时我们回退,会回退到main方法中调用A方法处,我们A方法中的局部变量的确是回退了,但是数据库里的值已经被删了,回退不了了,其它第三方对象、集合原理也类似,回退不了。回退的方式很简单,在Frames窗口中,点击Drop Frame按键即可。

MXhlZ0Z0TWdBZk8wSDJzVzNMdlVMRVR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

也可一次回退几个方法,比如我这里是main调用a方法,a调用b方法,b调用c方法,我这里直接选中a方法Frame,右键点击Drop Frame,这样就直接回退到main方法了,右键的Drop Frame和操作栏上的效果是一致的。

Z3lnY2tlRDM1Ynh1TCtQVXRGVjRDa1R3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

10. 强制返回

想象一个场景,当前bug已经找到了,你不想再继续执行了,再执行后面的代码会删除数据,你不想执行了,此时你们怎么办?直接停掉整个服务吗?能不能只停掉当前线程,而不停掉整个服务?答案是可以的,我们给他一个返回值。这里使用Force Return强制返回即可。

czJIWEZ1dXRucUgvYXFlcXhMeFJwRVR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

比如这里,twoSum方法要返回int[],我们直接new int[]{2, 7}返回即可,当然我们也可以返回点让前端开心点的数据,哄哄前端。

L3dVV2doT3dvVTlaSkcxL2loWXpQVVR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

或者我们直接抛出异常也是可以的

NVZGanNINFJyQi9wdFdlbnlBYTVCRVR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=
YWQwU0VYYkpxbWVKQ0wrNHhsTEUvMFR3U0RXbDZjRlkvRkVaTkp3PQ==

11. 多线程调试

Debug默认使用时,会阻塞所有线程,只留当前线程。平时如果只是学习可能不会觉得麻烦,但是开发中,某个接口,其他人在用,难道要别人等一下,等你Debug完再用吗,其实不用的。只需要选中如下所示,即可多线程调试,没有打断点的线程会直接执行,打了断点的线程,才会被阻塞,当然,同一方法入口,也可以进入多个线程,在Frames中进行线程的切换即可。

VnNzSnhtYUdJSk9yci9PK1kyaFdqRVR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

也可选择 View Breakpoints,然后再选择Thread开启多线程调度模式。

K1hZQkRDR1AzQ0JuNnhoVXRPYzIyVVR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

12. Stream 调试

有没有发现,用Debug调试Lambda表达式时很难受,尤其是哪种一行十几个方法哪种,一写就是一串,写时爽的一P,Debug时反向爽的一P,如果你也有这些问题,那么接下来这个神器要好好学习一下了。

不知你有没有发现,调试按钮里Trace Current Stream Chain 按钮常年都是黑色的,为什么呢,因为他是做Stream调试的,只有断点在Stream表达式时才能使用,使用方法即断点在Stream时,点击该按钮即可。

cW44K0xMRUU4Z0xjaTdmTUY4YlhuMFR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=
SUpzRlF5RTA2ZExFejJNM1lNVkE3a1R3U0RXbDZjRlkvRkVaTkp3PQ==

看出该按钮的妙处了吧,能直接把每一步的转换方式或者结果展示在我们面前,而且还有Split Mode展示方式,能一步一步展示转换结果。

MlN2Qk1RRjZKUDBCQ01UVmNaUGhIa1R3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

13. 远程Debug

当我们遇到某些问题,只能在生产环境下复现,开发环境不能复现的bug时,我们就需要使用远程Debug功能了。只需要我们本地代码和远程部署的代码一致即可。当然还要在远程启动jar包时,加入-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=xxx参数,其中xxx为监听端口,详细步骤如下:

点击 Edit Configuration…

YkpYUnVRVk5LemwxK2JlMXpGR28wRVR3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

依次选择“+”,然后选择 Remote JVM Debug

Ky9yQitjNVg1VlZEQVE1VjdUZTFYa1R3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

然后填写一下配置,Name是之后启动的名称,Host 是远程服务器的 ip,port: 用于远程socket 连接的端口,注意不能和项目端口一致,否则会启动失败,然后,idea 会为我们自动生成一条命令行参数:-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

VCsvUFBRdjAvU2k0aXUxRmpXSkVta1R3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

接着在远程启动jar时,加上我们的参数

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar xxx.jar

在远程项目启动成功后, 在本地以Debug方式运行第一步配置的服务

aVB0U0xYWTlkUnd5dVUyT1FhMStRa1R3U0RXbDZjRlkvRkVaTkp4SlFRPT0=

然后再请求远程原服务,我们在本地的断点就起作用了,是不是很秀

d3hodVcvVmtXd3NVRSt4OXhiVnFJa1R3U0RXbDZjRlkvRkVaTkp4SlFRPT0=