前言

本文如题目,基于 IDEA,基于 Java,本来想用 Dart 来着,寻思了一下还是用更多人擅长的来讲吧

调试是个很牛逼的东西,因为你不能保证你能看懂代码抛出的每一个异常是什么意思,代码的每次编译错误都是什么问题,服务抛出的各种问题你都能找到位置

所以说,调试在越复杂的项目里面越实用。

貌似,jetbrain 家的IDE 都可以沿用类似的这一套调试流程和方法

进行调试之前,我们需要了解什么

还是先说一下 IDEA 中如何进行调试,我确实没特意看过这个内容,全是我自己摸索出来的,也顺便总结一下

首先,调试的情况分为如下类别

  • 本地调试:最常见的调试方式,针对当前开发环境中运行的 Java 程序(如本地启动的 Spring Boot 应用、普通 Javamain 方法等)。代码和程序均在本地机器运行,调试器直接连接本地进程。
  • 远程调试:针对运行在远程服务器(或其他机器)上的 Java 程序进行调试,本地 IDEA 通过网络连接远程进程。代码在本地,程序在远程运行,通过调试端口建立通信。
  • 测试调试:针对 JUnit、TestNG 等单元测试用例的调试,仅运行指定的测试方法,断点可设在测试代码或被测试代码中。
  • 条件断点调试:对断点设置触发条件,仅当满足条件时才暂停程序,避免无意义的中断。就是右键断点图标,勾选Condition,输入 Java 表达式
  • 异常断点调试:当程序抛出指定异常时自动暂停,无需在代码中手动设置断点。一般是排查未捕获的异常,定位异常发生的具体位置。在Breakpoints窗口(Ctrl+Shift+F8)点击+,选择Java Exception Breakpoints,指定异常类型(如java.lang.NullPointerException)。
  • 分布式调试:针对分布式系统(如微服务架构)的调试,涉及多个服务实例之间的调用跟踪。一般是用工具

其实可以去看官方文档,肯定比我全

http://www.jetbrains.com/help/idea/debugging-code.html

了解IDEA调试中各个页面的作用

基础通用部分

先看一下页面,随着idea版本的更新Debug模式的图标设计虽有微调整,但是差不多

image-20250720235249504

绿色三角大家都知道是运行,绿色虫子大家都知道是调试,最左边那个 “Toggle Smart Step Into”(智能步入切换) 功能按钮,差不多它的作用就是调试时使用 “智能步入”,遇到方法调用,会智能判断,优先进入用户代码而非库代码,让调试更高效,避免频繁进入不想看的框架、库等代码里,精准控制调试步骤 。

image-20250720235440922

建议每个人把 在断点上使应用程序获得焦点 选项打开

如果你希望 调试 工具窗口在命中断点之前保持隐藏,请在相应的 运行/调试配置 中取消选中 启动时打开运行/调试工具窗口 复选框。

如果你的IDEA底部没有显示工具栏或状态栏,可以在View里打开,显示出工具栏会方便我们使用。访问请求到达第一个断点后,会自动激活Debug窗口。如果没有自动激活,可以去设置里设置。

以我的一个微服务项目为例子,微服务的调试界面就比较复杂了,介绍一下各个窗口的作用

image-20250720235759166

位于调试窗口的上方,包含了一系列用于控制调试流程的按钮,把鼠标光标放到上面就能查看,从左到右通常有:

image-20250721000239433
  • 重新以 Debug 形式启动

  • 停止运行

  • 继续:快捷键 F9,程序会继续执行,直到遇到下一个断点或程序结束。在暂停状态下恢复程序的执行。

  • 暂停:快捷键忘了,程序会被暂停,用于查看情况

  • 步过:快捷键 F8,会执行当前行代码,如果当前行是方法调用,会直接执行完整个方法,不会进入方法内部。常用于快速跳过已知逻辑正确的方法。

    • 在调试程序时,步过是一种调试技术,它允许你逐行执行代码,而不会进入当前行中的函数或方法。当你在调试过程中遇到一个函数或方法调用时,使用步过命令可以直接跳过该函数或方法的执行,继续执行下一行代码。
  • 步入:快捷键 F7,如果当前执行点在方法调用处,会进入被调用的方法内部,逐行执行方法内的代码。适用于深入查看方法内部的执行逻辑。

    • 步入是另一种调试技术,它允许你进入当前行中的函数或方法,并逐行执行其中的代码。当你在调试过程中遇到一个函数或方法调用时,使用步入命令可以进入该函数或方法内部,逐行执行其中的代码。
  • 步出:快捷键 Shift+F8,会从当前方法返回到调用它的方法,继续执行调用方法中的后续代码。当你在方法内部调试完,想回到调用处继续执行时使用。

    • 步出是一种调试技术,它允许你从当前正在执行的函数或方法中退出,并返回到调用该函数或方法的地方。当你在调试过程中进入了一个函数或方法内部,但你已经完成了对该函数或方法的调试,可以使用步出命令来退出该函数或方法,并返回到调用它的地方。
  • 查看断点:快捷键 Ctrl+Shift+F8,会打开断点管理窗口,你可以在其中查看、启用、禁用和删除所有断点,还能设置断点的条件等。

    image-20250721000439313
  • 忽略端点:没啥好说的,就是不要这个断点了

  • 更多:如下内容,自己了解一下,不多说

    image-20250721000704672
    • 说一下其中智能步入是什么意思,就是如果一行中有多个方法调用,IntelliJ IDEA 会询问您要进入哪个方法。 可以配置 智能步入 使其在每次同一行有多个方法调用时自动使用
    • 强制步过和强制单步执行用到的很少,但是还是说一下
      • 强制步过在 Windows 和 Linux 系统中一般是Ctrl + Shift + F8;当执行到方法调用语句时,使用 “强制步过”,调试器会将被调用的方法当作一个整体执行完毕,然后停留在方法调用语句的下一行,不会进入到被调用方法的内部。即便被调用方法中存在断点,调试器也不会在那些断点处暂停。
      • 当你对某个方法的内部逻辑已经很清楚,不需要深入查看其具体执行过程,只关心方法调用后的结果和程序后续执行情况时,就可以使用 “强制步过”。
      • 强制单步执行在 Windows 和 Linux 系统中是Alt + Shift + F7;“强制单步执行” 会深入到任何方法内部,包括 Java 标准库方法、第三方框架的方法以及 Lambda 表达式等。不管被调用的方法是用户自定义的还是系统提供的,调试器都会进入该方法内部,逐行执行代码,并且会在遇到的每个断点处暂停。
      • 当你想要深入了解某个方法的具体执行逻辑,查看其内部变量的变化情况,排查方法内部可能存在的问题时,就需要使用 “强制单步执行”。一般看源码的时候用到的多

来到线程和变量面板

image-20250731094505067

可以看到这里会展示一些你的调试的信息,包括变量,值,也就是说,如果你需要进行查看各个变量的执行情况和各个方法的执行情况,都会在这里进行显示

查看变量可以将Variables区中的变量拖到Watches中查看

可用的调试会话被分隔在 调试 工具窗口顶部的选项卡中。也就是这些

image-20250731094635947

如果为特定的运行/调试配置 启用了服务工具窗口 ,那么当您调试这些配置时,调试工具窗口的整个视图将显示在服务工具窗口中。

这边其实还藏着一个选项卡页面,每个会话都会显示以下选项卡

image-20250731094923616

帧(Frames)

  • 作用:显示当前线程的调用堆栈(方法调用链)。
  • 通俗理解:比如你的代码执行到方法 C,而方法 C 是被方法 B 调用的,方法 B 又是被方法 A 调用的,“帧” 就会按顺序列出 A→B→C 这样的调用关系。你可以点击切换不同的 “帧”,查看对应方法执行时的变量状态,相当于 “回退” 到上层方法查看当时的情况。

变量(Variables)

  • 作用:展示当前暂停位置(断点处)可见的所有变量及其值。
  • 特点:不仅能看,还能直接修改变量的值(比如把count=0改成count=10),让程序继续执行时使用新值,方便快速测试不同场景,不用重新运行程序。

监视(Watches)

  • 作用:自定义跟踪某些表达式或变量的变化。
  • 用法:比如你关心user.name这个值在整个调试过程中的变化,就可以把它添加到 “监视” 中,无论程序执行到哪里,都会持续显示这个值,不用每次在 “变量” 面板里找。默认和 “变量” 在同一个标签页,数量多的时候可以单独显示。

控制台(Console)

  • 作用:显示程序运行时的输出内容。
  • 不同场景
    • 本地调试时:和正常运行程序的控制台一样,会显示System.outprint输出的内容,同时额外显示调试相关的日志(比如 “命中断点” 的提示)。
    • 附加到已运行的进程调试时:只会显示调试器自己的日志,不会显示原程序的输出(因为原程序输出可能在它自己的控制台里)。

线程(Threads)

  • 作用:列出程序中所有正在运行的线程,包括它们的状态(运行中、阻塞、等待等)。
  • 用途:可以切换到不同线程查看其调用堆栈(配合 “帧” 使用),适合调试多线程问题(比如死锁、线程安全问题)。还能导出线程快照(线程转储),用于分析线程状态。

内存(Memory)

  • 作用:展示 JVM 堆内存中对象的信息,比如某个类有多少个实例、占用多少内存等。
  • 用途:帮助分析内存使用情况,比如查找内存泄漏(某些对象明明不用了却没被回收,数量一直在增加)。

开销(Overhead)

  • 作用:监控调试过程中各种功能消耗的系统资源(比如 CPU、内存)。
  • 用途:如果调试时发现程序运行变慢,可能是某些调试功能(比如监视了太多表达式、内存分析等)导致的,通过 “开销” 面板可以找到资源消耗高的功能,关闭或优化它们来提高调试效率。

运行页面可以找到调试的大部分内容和快捷键

image-20250731141715935

Spring相关调试部分

继续看 Bean 这个页面,通常是和 Spring 框架 集成相关的调试视图,主要用于在调试过程中查看、分析 Spring 容器中 Bean 的相关信息

image-20250731095343353
  • 关联 Spring 框架:如果你开发的是 Spring(含 Spring Boot 等)应用,Spring 容器会管理大量 Bean(如 Controller、Service、Repository 等组件)。调试时,借助这个视图能直观了解 Bean 的加载、配置和依赖关系 。
  • 调试场景:比如排查 Bean 注入失败、配置未生效、依赖关系异常等问题时,可通过这里查看 Bean 实际的属性值、依赖关联,辅助定位问题。

其中图模式慎点,小心电脑内存瞬间爆炸

image-20250731095932638

继续看运行情况这个页面

image-20250731141242483

这个需要 IDEA 的 Profiler 功能(如 Async Profiler ),需要调试会话(Debug Session)和性能分析会话同时开启。如果只是普通 Debug(只走断点调试,没启用 Profiler ),“运行状况” 就会不可用。需要启动调试时,选带 “Profiler” 的启动配置(如 Debug with Profiler ),而不是普通的 Debug

这个部分的核心是 监控程序调试过程中的性能数据、执行耗时、方法调用热点 ,帮你排查性能问题、定位代码里的慢操作。老版本好像叫“性能分析

image-20251117115438963

它一般会进行方法执行耗时统计,展示程序执行的 “热点方法”(被频繁调用或耗时久的方法 ),查看方法执行时的内存分配、CPU 占用,判断是 “计算密集型” 还是 “内存泄漏型” 问题。跟性能相关基本都在这看

现在他好像比 JVisualVM 好用,以前我们都用这个进行性能分析

继续看映射这个页面

这个页面主要就是展示 接口路径与处理方法的映射关系,左边路径列就是对应显示接口的访问路径,包含 HTTP 方法,右边方法列就是展示路径对应的 Java 方法,格式一般是 [控制器类]#[方法名]

image-20250731100119748
  • 路由可视化:清晰列出 Web 应用中,外部可访问的接口路径(如 /api/v1/iterations ),以及背后对应的 Java 处理方法(如 IterationController#queryIteration )。不管是自己开发的业务接口,还是 Actuator 这类监控端点(像 /actuator/threaddump ),都能直观看到 “路径 ↔︎ 方法” 的对应关系。

  • 调试辅助:调试时,若你想排查某个接口的逻辑,通过这里能快速定位到接口对应的 Controller 方法,直接找到代码入口;也能反过来,从代码里的 Controller 方法,对应到实际对外暴露的路径,验证路由配置是否正确。

继续看环境这个界面

这个页面是程序 “运行时环境参数的总览窗口”,把分散在配置文件、系统变量、环境变量里的参数,集中展示出来。无论你调试的是 Spring Boot、普通 Java 程序,还是其他框架项目,都能在这里快速查看、验证运行时生效的配置

image-20250731100009617

一般在实际开发的过程中,都是在这看看配置是否生效,或者 profiles 的情况

非 Spring 项目在这里一般都是JVM 系统属性和环境变量,本质是统一管理程序运行时的 “环境上下文”,让你不用到处找配置、敲命令查参数,调试时更聚焦代码逻辑

IDEA调试技巧

如何查看变量

查看变量是我们再进行调试过程中,确认项目是否正确运行的前提,通过传递参数的情况来判断项目是否有问题,有什么问题,之后会怎么样

在 IDEA 中,参数所在行后面会显示当前变量的值

例如,我调试我项目的登录过程

image-20251117115943983

发现在environment后面给出了其参数的值

image-20251117120144727

或者在下面,线程和变量的控制台,也可以看到其变量及其值

image-20251117120216377

光标悬停到参数上,显示当前变量信息。然后可以打开,这种很方便

image-20251117120238605

在遍历 EnvironmentPostProcessor 并执行其 postProcessEnvironment 方法。其中出现了循环,说明ApplicationEnvironmentPreparedEvent 被重复发布,进而重复执行这段遍历逻辑。

发现是我项目中通过 spring.factories 或自动配置重复注册了同一个 EnvironmentPostProcessor 实现类,导致遍历列表中存在多个相同实例,反复执行。

image-20251117120456103

这个部分就是一个比较好的详细查看变量的例子

在调试中,检查变量的目的不仅是检查变量值是否符合预期,而且当程序抛出异常(如 NullPointerExceptionIndexOutOfBoundsException)时,通过查看变量可快速定位原因。

而且观察变量在代码执行过程中的变化轨迹也很有用,是确保逻辑正确的标准。例如:跟踪用户输入数据从接收、校验到存储的全流程,可以确认每一步处理是否正确

打算法竞赛时候,也可以根据这个分析复杂对象状态,通过展开变量查看内部细节,判断是否存在数据遗漏、重复或错误赋值

如何计算表达式

在前面提到的计算表达式按钮,Evaluate Expression (Alt + F8) 。可以使用这个操作在调试过程中计算某个表达式的值,而不用再去打印信息。

image-20251117121624901

继续我们的调试,我们发现程序在 SpringApplication.run() 方法的创建应用上下文(ApplicationContext)” 这步

image-20251117120800702

众所周知,这一步是 Spring Boot 启动流程的关键步骤,ApplicationContext 是 Spring 容器的核心

那么这一步中并没有进入上下文初始化,此时 context 我们求值一下,发现为 null,所以这步是完全正常的,因为这行代码的作用就是创建 ApplicationContext 实例并赋值给 context

那么结合变量查看部分,我们发现想要的查看变量直接就能被计算出来

image-20251117121641836

而且这个表达式不仅可以是一般变量或参数,也可以是方法,当你的一行代码中调用了几个方法时,就可以通过这种方式查看查看某个方法的返回值。

我们的例子就是计算了 this.createApplicationContext(); 这个方法在此时的环境上下文初始状态

表达式求值在这部分

image-20251117121732328

如何回退断点

尤其是 spring boot 这种比较繁杂的项目的时候,经常因为“下一步”按太快,而导致跳过了想要深入分析的那段代码

在IDEA中就提供了一个帮助你回退代码的机会,但这个方法并不是万能的。

image-20251117121944503

这个小的回转箭头就是 IDEA 的 Reset Frame 回退操作

在调试时,IDEA 的 “调用栈”(Call Stack)面板会显示当前线程的执行路径(从方法调用的起点到当前断点的所有方法层级)。Reset Frame 功能允许你将执行位置回退到调用栈中某个上层方法的断点处,仿佛 “时光倒流” 到该方法执行的某个状态,从而重新调试这段代码。

为什么说回退不是万能的

先来看Reset Frame 能回退的场景

  • 纯 Java 方法调用(无状态修改)

    当方法仅进行局部变量运算无副作用的参数传递只读操作时,回退效果最理想。例如:

    1
    2
    3
    4
    public int add(int a, int b) {
    int c = a + b; // 断点1
    return c; // 断点2
    }

    若在 “断点 2” 执行后回退到 “断点 1”,局部变量c会恢复到计算前的状态,可重新执行加法逻辑。

  • 未修改外部状态的方法

    若方法仅读取对象属性(不修改)、不操作全局变量 / 静态变量、不涉及 IO / 网络等外部资源,回退时能准确恢复到之前的状态。

再来看Reset Frame 不能回退的场景(局限性)

  • 修改了对象状态或全局变量

    若方法中修改了对象的属性静态变量集合内容等,回退仅能重置当前方法的局部变量和执行位置,但已修改的外部状态不会恢复。例如:

    1
    2
    3
    4
    5
    6
    7
    public class UserService {
    private User user = new User();

    public void updateName(String name) {
    user.setName(name); // 断点:修改对象状态
    }
    }

    若调用updateName("test")后回退,user.name仍会保持 “test”(已被修改),回退后重新执行会再次叠加修改,导致状态混乱。

  • 涉及 IO、网络、数据库等外部操作

    若方法执行了文件写入数据库更新发送网络请求等操作,这些 “副作用” 是不可逆的。例如:

    1
    2
    3
    public void saveToDb() {
    jdbcTemplate.update("INSERT INTO logs VALUES (?)", "test"); // 断点
    }
  • 多线程环境

    若当前线程修改了共享变量,其他线程可能已读取该变量并执行了后续逻辑。回退当前线程后,其他线程的状态无法同步回退,会导致线程间状态不一致,调试结果失真。

  • native 方法或 JNI 调用

    对于调用底层 C/C++ 实现的native方法(如System.currentTimeMillis()),回退无法重置其执行结果(如时间戳已被获取),因为这类方法的执行不受 Java 调试器控制。

  • 异常抛出后

    若方法中抛出了异常且已被捕获或传播,回退到异常抛出前的栈帧时,异常状态可能无法完全清除,导致重新执行时逻辑异常。

那么为什么回退不是万能的,根本原因是:Java 是 “按值传递” 的语言,且调试器只能控制代码执行流程和局部变量状态,无法逆转 “副作用”

回退本质是 “重置当前线程的调用栈和局部变量”,但对方法执行过程中产生的外部状态修改(对象属性、静态变量、IO 操作等)无能为力。

对于 Spring Boot 这类复杂框架,其内部大量涉及 Bean 状态修改、容器初始化、资源加载等操作,回退后很可能因状态不一致导致后续调试混乱(例如重复初始化 Bean 引发冲突)。

它的位置在这里

image-20251117122142598

复杂场景谨慎使用,例如在我的例子中,在 Spring Boot 的核心流程(如ApplicationContext初始化、Bean 生命周期)中,若已涉及容器状态修改,回退可能导致调试状态异常,这时候一般重启调试更稳妥。

在关键代码行设置条件断点(如满足特定参数时暂停),避免因 “下一步” 太快跳过目标代码,从源头减少对回退功能的依赖。

如何进行debug中断

怎么中断正在debug的请求,也就是放弃此次http请求

有时候我们用IDEA进行debug,跑进来了,debug到某个断点或某一行,如果此时我们不想继续走下去(中断此次http请求,或者说中断此次debug),要怎么做?

为什么需要中断此次debug?

因为大部分请求不是 GET 的都可能涉及到状态的修改

这可能会导致因为调试导致数据库不干净了就,这是很不理想的调试,可能造成麻烦

此时需要一种方式:在不执行后续代码的前提下,直接终止当前请求,且尽可能避免已执行的状态修改(或减少影响)

利用 drop frame 回到最顶层(一般是controller方法),右键点击栈帧选择Froce Reutrn (也就是强制返回)即可(方法如果有返回值需要输入Return Value,填入return null (或者直接填入null即可,return可省略))

Force Return 允许你在调试到任意栈帧时,直接终止当前方法的执行并强行返回指定的结果,跳过后续所有代码(包括状态修改逻辑),从而中断整个请求流程。

  1. 定位请求入口栈帧

    当 debug 停在某个断点时,打开 IDEA 调试面板的 “Call Stack”(调用栈) 窗口,找到当前 HTTP 请求的顶层入口方法(通常是 Controller 层的接口方法,如UserController.addUser(...))。

    • 若当前断点在深层方法(如 Service、DAO 层),可通过 Drop Frame 回退到上层栈帧(右键点击上层方法栈帧 → Drop Frame),逐步回退到 Controller 层(避免在深层方法强制返回导致中间层逻辑未处理)。
    image-20251117123105981
  2. 执行Force Return

    在调用栈中右键点击顶层 Controller 方法栈帧,选择 Force Return(强制返回):

    • 若方法有返回值(如ResponseEntity<String>),会弹出输入框,需指定返回值(例如输入null,或构造一个空响应对象如new ResponseEntity<>(HttpStatus.OK))。
    • 若方法是void类型,直接点击OK即可,无需输入返回值。
    image-20251117123052519

执行后,当前方法会立即终止,后续代码(如 Service 层的save、DAO 层的insert)不会执行,HTTP 请求会以你指定的返回值响应客户端,且不会触发后续状态修改。

Force Return 本质是强制当前方法提前退出,调试器会直接从当前栈帧返回,忽略方法内未执行的代码。

如果出现了方法里调用别的方法的情况,如果想要输入一次Force Return就彻底退出,就可以一直回退到不能再回退,脱离方法内部的方法回到顶层再Force Return

image-20251117123355094

它的位置在这里

image-20251117122641743

我这里离进入Controller还有114514步(spring boot环境准备,Nacos,Mysql,Redis,Redission……),所以我就找了网上的演示

注意:已执行的代码(如前序步骤的数据库更新)无法回滚,因此需在状态修改逻辑执行前使用该功能(例如在进入service.save()方法前中断)。

例:若调试到controller层时,还未调用service.updateUser(),此时Force Return可完全避免用户信息被修改;若已调用service.updateUser()并执行了 SQL,即使中断,数据库修改也已生效(需手动回滚或依赖事务)。

还注意,在测试类或 Controller 方法上添加@Transactional注解,并配置rollbackFor = Exception.class,调试时即使执行了save操作,方法结束后事务会自动回滚(需确保数据库支持事务,如 InnoDB)。

注意这个和停止调试不一样啊,停止调试终止整个应用进程,Force Return 是终止当前方法,返回指定的结果

细说智能步入

想想,一行代码里有好几个方法,怎么只选择某一个方法进入。

我们知道步入,使用Step Into (Alt + F7) 进入到方法内部

但是这两个操作会根据方法调用顺序依次进入,如果方法的流程较长,这很麻烦,而且不必要

那么智能步入就很方便了,智能步入,这个功能在Run里可以看到,Smart Step Into (Shift + F7)

它的位置在这里

image-20251117123642986

例如我们继续调试 Spring 的初始化相关内容

image-20251117123713602

你看这里有这么多方法

我们已经来到了 Assert.state(!environment.containsProperty("spring.main.environment-prefix"), "Environment prefix cannot be set via properties.");这步断言了,这步断言有两个方法

光标对着方法,直接 Shift + F7,会自动定位到当前断点行,并列出需要进入的方法

image-20251117124051663

进入就行

image-20251117124124931

为断点设置条件进行调试

这种情况一般会在多递归中常用

算法吃必备了属于是

在断点的地方右键可以打开断点条件设置窗口,这里也支持多线程的设置

image-20251117124729688

在 “挂起(Suspend)” 选项中,可选择 “所有(All)”(所有线程触发断点时都暂停)或“线程(Thread)”(仅当前线程触发时暂停),适用于多线程场景下的精准调试。

在 Condition 中设置我们想要的条件就行,注意其中坑出来了

IDEA 的条件断点支持Java 语法的布尔表达式,可直接使用当前作用域的变量、方法调用、对象属性等构造条件

  • 简单变量判断:userId == 123name.equals("admin")
  • 集合 / 数组判断:list.size() > 5array[0] == null
  • 方法调用:userService.isAdmin(userId)(需确保方法可访问且无副作用)

什么意思?

假如这样一个场景,在递归算法中(如斐波那契、二叉树遍历),可通过条件断点仅在特定递归深度或参数下暂停,避免无效断点干扰,我们希望当递归到第 5 层时才会暂停,方便观察该层级的计算状态。

1
2
3
4
public int fib(int n) {
if (n <= 1) return n;
return fib(n-1) + fib(n-2); // 在此行设置条件断点:n == 5
}

那么该输入什么呢?

n = 5

不对,idea认为条件 n=5 与预测结果Boolean不对,会报错

所以是 设置条件n == 5,才正确

在 debug 的断点条件设置中,你设置的条件最后输出的结果应该是一个 boolean 类型的值,如果你的条件非 boolean 类型最后只是将你的语句执行了,而无法进入预期的条件。

为什么说条件断点是 oi 吃用的多,因为条件断点的表达式会在每次代码执行到该行时计算,若表达式包含数据库查询、网络请求、大循环等耗时操作,会严重拖慢调试速度。

注意,条件表达式中只能使用当前方法作用域内的变量,若引用了外层方法或其他类的变量,会提示 “无法解析符号”。

多线程调试

多线程调试,需要先掌握以下两个核心要点。

查看运行栈帧 && 切换线程

在 Threads & Variables 这个窗口,进行线程之间切换。

image-20251117125324341

断点暂停方式,的选择 Thread

image-20251117125356486

这个是最为重要的。

建议多线程调试,选择 Make Default,也就是默认,点击图中设为默认,后续所有断点都是 Thread,如果不选择 Thread,则无法进行线程断点追踪!所有线程将直接运行结束。

什么意思?

在 IDEA 的断点设置中,“Suspend”(暂停)选项有两个核心模式:

  • All(所有线程):当断点触发时,整个应用的所有线程都会被暂停(包括主线程、其他工作线程),直到手动继续执行(如按 “下一步”)。
  • Thread(当前线程):当断点触发时,只有进入该断点的线程会被暂停,其他线程会继续正常运行(不受影响)。

在多线程场景中(如并发任务、线程池处理),如果断点默认是 “All” 模式,会导致:

  1. 线程执行顺序被强行打断

    假设线程 A 触发断点,此时 “All” 模式会暂停所有线程(包括线程 B、C)。但实际业务中,线程 B、C 可能需要继续执行才能触发后续断点(如线程间的协作、数据传递),强行暂停会导致调试场景与真实运行场景不一致,甚至错过关键断点。

  2. 无法追踪单个线程的完整流程

    多线程调试的核心需求是跟踪某一个线程的执行路径(如线程 A 从启动到结束的所有方法调用)。若用 “All” 模式,每次断点都会冻结所有线程,切换线程调试时需要手动唤醒其他线程,操作繁琐且容易混乱。

  3. 可能导致线程 “假死” 或逻辑异常

    某些线程依赖超时机制(如wait(1000))或其他线程的信号(如notify()),若被 “All” 模式长期暂停,可能触发超时、死锁等非预期行为,干扰调试判断。

当你在某个断点的设置中选择 “Thread” 模式,并点击 “Make Default” 后,后续所有新添加的断点都会默认使用 “Thread” 模式,无需每次手动修改。

这在多线程项目中非常实用

例如主线程此时主线程正在执行 FutureTaskget(long timeout, TimeUnit unit) 方法,并且触发了 TimeoutException 异常。

  • 核心逻辑FutureTask 是 Java 并发包中用于异步任务结果获取的类,主线程调用 get(1, SECONDS) 表示 “等待异步任务结果,最多等待 1 秒”。
  • 当前状态:由于异步任务在 1 秒内未完成,触发了超时异常(TimeoutException),主线程此时停在异常抛出的断点处,用于调试超时场景下的逻辑。
image-20251117125742574

有这样一个子线程,该线程正在执行 Reference 类的 waitForReferencePendingList 方法。

image-20251117125842902

这是 Java 引用处理机制的底层逻辑,用于等待 JVM 的 “待处理引用列表”(Pending Reference List)非空,通常与垃圾回收、弱引用 / 软引用的处理相关。

该线程可能在后台处理对象引用的生命周期(如弱引用的回收、引用队列的入队等),属于 JVM 内部的并发辅助线程。

还发现有这样的一个子线程2

image-20251117125930496

该线程正在执行 Thread 类的 sleep0 方法(这是一个 native 方法,由底层操作系统实现线程休眠)。

  • 核心逻辑sleep0Thread.sleep() 的底层实现,负责让线程暂停指定时间(纳秒级)。
  • 当前状态:该线程处于休眠状态,可能是异步任务中包含了线程休眠逻辑,导致主线程等待超时。

继续下去,切换回到主线程进行下一步

image-20251117130114119

这步是在Spring Cloud 网络工具类(InetUtils)的 convertAddress 方法中,处理网络地址到主机信息的转换逻辑,属于Spring 应用启动时获取本地主机信息( hostname 和 IP 地址)的关键步骤

  • 异步提交了一个任务(通过 ExecutorService),用于获取网络地址(InetAddress)对应的主机名(getHostName)。
  • 现在主线程在尝试通过 Future.get(...) 方法等待异步任务的结果,若任务超时或抛出异常,会捕获并将主机名默认设为 localhost,最终封装成 HostInfo 对象返回。

来看休眠线程

image-20251117130026810

可以看到休眠线程结束,引导了另一个线程启动

image-20251117130051972

来看 Reference Handler 线程,它等待 JVM 虚拟机的重置,可以看到上面的处理已经完成了

image-20251117130221413

IDEA 在 Debug 时默认阻塞级别是 all,会阻塞其它线程,只有在当前调试线程走完时才会走其它线程; Thread 模式在 Remote 调试时不阻塞他人请求

若需精准调试某线程,可对目标线程的代码设置 “Thread 级挂起” 的条件断点 。例如:

  • Thread.sleep0 方法设置条件断点 Thread.currentThread().getName().contains("sleep"),仅当该线程执行时暂停;
  • FutureTask.get 方法设置条件断点 timeout == 1,聚焦超时逻辑。

日志断点

IDEA 还支持 “日志断点”(右键断点 → 选择 “日志(Log)”),可在不暂停程序的情况下输出变量值,结合条件表达式使用,堪称 “无侵入式调试”。

例如,设置日志内容为"当前用户:{user.id}, 年龄:{user.age}",并添加条件user.getAge() < 18,即可在控制台实时输出符合条件的用户信息,无需暂停程序。

image-20251117131828347