1. 项目概述
1.1 调试这件事,真的值得认真对待
做 Spring Boot 开发的人,大概率都有过这样的经历:代码写完了,启动也没报错,但接口返回的数据就是不对。或者更恼火的是——本地跑得好好的,一部署到服务器就出问题。这时候如果只会 System.out.println 到处打日志,排查效率会非常低。
调试(Debug)不是“代码写不出来才用”的东西,它是日常开发里最常用的技能之一。IDEA 和 Eclipse 作为 Java 开发者最常用的两个 IDE,内置的调试功能其实非常强大,但很多人只用到了最基础的断点、单步执行,很多高价值的功能一直没被发现。
这篇内容我会结合自己的实际使用经验,把 Spring Boot 项目在 IDEA 和 Eclipse 里的调试技巧从头到尾梳理一遍。从环境准备到断点进阶,从日志定位到远程调试,争取做到“拿来就能用、照着就能做”。适合刚入行的新手,也适合用 IDE 用了好几年但没深挖过调试功能的同学。
1.2 调试的本质是什么
调试的核心本质是三件事:让程序在指定的位置停下来、看清当前的状态、控制程序按你需要的方式继续走。
我见过不少同学调试就是“打个断点,然后一直点 Resume(F9)”,跑到哪算哪,看不明白就把断点删了换地方。这样也不是不行,但效率太低。如果你能把断点的触发条件、变量的求值、调用栈的分析、线程状态的观察都掌握了,调试就从“碰运气”变成了一种有章可循的系统性工作。
还有一个很多人忽略的点:调试工具只是辅助,真正的核心是思路。比如一个接口返回数据不对,你得先判断是 Controller 层的问题、Service 层的问题还是 Mapper 层的问题,然后决定在哪里打断点、断点打几处、分别看什么变量。脑子里有排查路径,手上的工具才有意义。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 调试前的环境准备:IDEA 与 Eclipse 的配置细节
2.1 IDEA 中调试 Spring Boot 项目的前置配置
IDEA 开箱即用的调试功能已经很完善了,但还是有几个配置值得你提前确认,否则调试过程中会遇到各种小麻烦。
第一,确认编译信息。 调试时断点能不能命中,跟编译输出的 class 文件是否包含调试信息有关。在 File > Settings > Build, Execution, Deployment > Compiler > Java Compiler 里,Generate debugging info 这个选项必须勾上。默认是开启的,但如果之前有人改过配置,断点就可能出现“打上但没反应”的情况。
第二,配置热部署。 Spring Boot 项目调试时,改代码后的重启很耽误时间。建议引入 spring-boot-devtools 依赖,它可以实现类变更后的自动重启,而且只重启应用上下文,速度比手动重启快很多。
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
注意 optional 要设为 true,这样部署到生产环境时不会把 devtools 打进去。另外,IDEA 里要开启 Build project automatically,并且在高级设置里勾选 Allow auto-make to start even if developed application is currently running,否则改了代码不会自动编译,devtools 也感知不到变化。
第三,设置日志输出。 调试时经常需要看 SQL、看参数,Spring Boot 默认的日志级别可能不够细。我一般会在 application.yml 里单独给 MyBatis 或 JPA 的 Mapper 包设置 DEBUG 级别:
yaml复制logging:
level:
com.example.demo.mapper: debug
org.springframework.jdbc.core: debug
这样既能看清楚 SQL 语句和参数,又不会被框架的无关日志刷屏。
2.2 Eclipse 中调试 Spring Boot 项目的前置配置
Eclipse 在国内的使用人群虽然不如 IDEA 多,但很多老项目和教学场景仍然在用。Eclipse 调试 Spring Boot 项目,建议用 Spring Tools 4(STS4)插件,或者直接装 Spring Tools Suite 这个集成版 IDE。
Eclipse 里调试前需要确认两个设置:
第一,编译器兼容级别。 在 Window > Preferences > Java > Compiler 里,Compiler compliance level 要和你项目用的 JDK 版本匹配。比如 JDK 8 的项目就选 1.8,如果这里配置错误,启动 Spring Boot 时可能出现 UnsupportedClassVersionError。
第二,调试信息的生成。 在同一个页面往下看,Classfile Generation 相关的选项默认都是开启的,确认 Add variable attributes 和 Add line number attributes 被勾选。否则调试时你看不到变量值,断点处的行号也可能错乱。
Eclipse 启动 Spring Boot 项目的方式很简单:在启动类上右键,选择 Run As > Spring Boot App 或者 Debug As > Spring Boot App。需要注意,如果项目中配置了 Maven 多模块,Eclipse 的 classpath 没有正确识别,可能会找不到启动类的依赖,这时候需要在项目上右键 Maven > Update Project。
2.3 环境配置的核心心得:先跑起来,再谈调试
我自己的经验是,能调试成功的前提永远是“能启动成功”。很多人的断点不生效、调试崩溃,根源不是调试技巧的问题,而是项目本身启动就有隐患。比如 Maven 依赖冲突、JDK 版本不对、端口被占用,这些都会干扰调试过程。
遇到启动问题,先看控制台报错,不要急着去 IDE 设置里翻来翻去。Spring Boot 的报错信息其实写得很清楚,比如端口被占用会直接告诉你 Port 8080 was already in use。用 lsof -i:8080(Mac/Linux)或者 netstat -ano | findstr 8080(Windows)找到占用进程,结束掉就行。这类基础问题解决干净了,调试才会顺畅。
3. IDEA 调试实战:从基础断点到高级技巧
3.1 断点的种类与使用场景
IDEA 的断点看似简单,其实分好几种,不同场景用不同断点,效率差别很大。
行断点是最常用的,点击代码行号右侧空白处即可。但行断点有个缺点是:如果这行代码在一个频繁调用的循环里,断点会命中无数次,你需要不停地按 Resume,很浪费时间。
方法断点:在方法声明行打断点,进入方法时和退出方法时都会暂停。适合排查“这个方法到底被谁调用了”“返回值是什么”这类问题。但方法断点会显著降低程序执行速度,Spring Boot 启动时不要在框架方法上打方法断点,否则启动过程会慢得让你怀疑人生。
字段断点:在字段声明行打断点,字段被读取或修改时触发。这个功能非常实用。比如有个全局变量,你不知道哪段代码改了它的值,在字段上打断点,勾选 Field access 和 Field modification,就能精准定位到修改的代码位置。
条件断点:右键断点,可以输入一个布尔表达式。比如循环到第 100 次的时候才想停下来,条件写成 i == 100 即可。条件断点解决的是“断点命中太频繁”的问题,但要注意,条件表达式不要写得太复杂,否则会影响性能。
异常断点:在 Run > View Breakpoints 里可以添加异常断点,比如 NullPointerException。一旦代码抛出这个异常,程序会在异常抛出的位置停下来,哪怕你没有在那个位置打任何断点。排查空指针问题时,这个功能比人肉找代码效率高太多了。
3.2 Frames 与 Variables:理清调用栈和变量关系
断点命中后,IDEA 的调试窗口会显示几个关键面板。很多人只看 Variables 面板,忽略了 Frames 面板,这是个常见错误。
Frames(调用栈)面板告诉你当前断点所在的调用链。比如 Controller 调 Service,Service 调 Mapper,断点打在 Mapper 里,Frames 从上到下就是 Mapper -> Service 实现类 -> Controller -> Spring 框架的调用入口。点击任意一层,右侧 Variables 面板会立刻切换成那一层的局部变量和参数。这意味着你不需要在每一层都打断点,只看调用栈就能回溯整个调用链的数据。
Variables 面板里的变量很多,但真正值的关注的是 this 引用的内容——比如 this 指向的是哪个 Mapper 实现类,里面注入的是什么数据源,这能帮你快速确认 Spring 容器里到底装配了哪个 Bean。
还有一个很实用的操作:在 Variables 面板里右键变量,选择 Copy Value 或 Copy Name。排查问题时经常要把变量值跟接口文档、数据库记录做比对,复制出来省去手打的时间。
3.3 Evaluate Expression:调试时直接执行代码
IDEA 的 Evaluate Expression(快捷键 Alt + F8)是我觉得最被低估的功能。断点停住时,你可以在弹窗里直接输入一段 Java 表达式,立刻得到计算结果。
它的能力和调试上下文是隔离的,所以可以在表达式里调用当前作用域里的变量、方法。举个例子:
java复制String result = userService.getUserNameById(1001L);
你可以上下文里只有 userId,没有调用过 userService,这没问题——只要在表达式里写 userService.getUserNameById(userId),IDEA 就会执行并显示结果,不需要在代码里临时加一行再重新部署。
还有一个非常实用的场景:修改变量的值。在 Variables 面板里直接右键变量 Set Value,或者用 Evaluate Expression 对变量重新赋值。比如你在排查一个优惠券金额计算的 bug,前一次调试时金额是 50 元,想要再测一次金额为 100 元的场景,不需要改代码再重启,调试时直接把变量值改成 100 就行。
3.4 Drop Frame:回到方法调用前重新执行
Drop Frame 这个功能,国内开发者用得相对少,但如果你有过“调到一个方法里,发现参数不对,只能退出重来”的经历,就一定需要它。
断点停在一个方法内部时,点击调试窗口里的 Drop Frame,程序会“回退”到调用这个方法的上一帧,也就是让你重新执行这个方法的调用。参数可以重新赋值,逻辑可以重新走一遍,不需要重启应用。
有一点要注意:Drop Frame 虽然叫“回到过去”,但已经产生的副作用不会回滚。比如你已经往数据库里插入了一条记录,Drop Frame 之后再执行一次,就会插入两条。这个功能适合纯计算逻辑的调试,涉及外部系统状态改变的要慎重。
3.5 Stream 调试:一行一行的 Trace
Spring Boot 项目里 Stream API 用得很多,比如从数据库查出一批订单,然后 filter、map、collect 一通操作。这种链式调用在调试时特别难看清中间过程,好在 IDEA 提供了一项隐藏能力。
在断点停住时,选中 Stream 表达式,然后使用 Trace Current Stream Chain 功能(右键点击断点行,或者通过调试窗口的按钮触发)。IDEA 会把 Stream 的每一个中间步骤的可视化视图弹出来,每行数据经过 filter 之后是保留还是过滤、map 之后变成什么值,都一目了然。
这个功能在排查复杂集合运算、判断某一批数据为什么缺失或多余时,非常高效。不过它只对 Stream 的中间操作有效,如果链里混了自定义的循环和递归,就没法追踪了。
4. Eclipse 调试实战:经典环境下的高效操作
4.1 Eclipse 的基础调试面板与快捷键
Eclipse 的调试视图布局和 IDEA 有差异,但核心交互逻辑相似。进入调试模式后,常用的快捷键:
| 功能 | Eclipse 快捷键 | IDEA 快捷键 |
|---|---|---|
| 单步进入方法 | F5 | F7 |
| 单步跳过 | F6 | F8 |
| 单步返回 | F7 | Shift + F8 |
| 继续执行 | F8 | F9 |
| 表达式求值 | Ctrl + Shift + I | Alt + F8 |
| 断点属性 | Ctrl + 双击断点 | 右键断点 |
很多从 IDEA 转到 Eclipse 的同学,最大的不习惯就是快捷键差异。但 Eclipse 支持修改按键绑定,在 Window > Preferences > General > Keys 里可以自定义。
Eclipse 的调试视角有两个重要面板:Debug 视图和 Variables 视图。Debug 视图里能看到线程列表、调用栈;Variables 视图里显示当前栈帧的局部变量、静态变量。还有 Breakpoints 视图,列出了所有断点的位置和状态,可以批量启用/禁用/删除断点。
4.2 断点属性:条件断点与命中次数
Eclipse 的断点右键选择 Breakpoint Properties,功能比 IDEA 更偏老派但很实用。断点属性里有几项配置值得留意:
Condition(条件):输入布尔表达式,只有表达式值为 true 时才暂停。Eclipse 的条件断点做得很规矩,如果表达式报错,它会提示你在断点位置检查代码,不会直接中断程序。
Hit Count(命中次数):指定断点被命中多少次后暂停。比如你想等循环执行到第 50 次时停下,不需要写条件表达式,直接设置 Hit Count 为 50 就行。这个功能在不知道循环变量当前值的情况下更直观——你只需要关心“执行到第几次了”。
Eclipse 还支持异常断点。在 Run > Add Java Exception Breakpoint 里输入异常类的全限定名,比如 java.lang.NullPointerException,任何线程抛出这个异常时都会自动停住。但我提醒一句:异常断点设为 java.lang.Exception 这种根异常会导致程序启动过程频繁暂停,因为 Spring Boot 启动时内部会有很多被捕获的异常,建议只针对你关心的异常类设置。
4.3 Eclipse 的远程调试配置(JPDA)
Eclipse 配合远程 Spring Boot 应用调试,这是我用过比较稳妥的方案。Spring Boot 应用启动时加上远程调试参数:
bash复制java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar demo.jar
其中 suspend=n 表示应用启动时不等待调试器接入,y 则表示要等。生产环境不建议开启远程调试,如果一定要在测试环境开,记得配合防火墙限制访问端口,不然会有安全风险(调试端口可以被远程代码执行)。
Eclipse 端配置步骤:
- 右键项目,选择
Debug As > Debug Configurations - 在左侧选择
Remote Java Application,新建配置 Host填服务器 IP,Port填 5005- 点击
Debug,连接成功后 Eclipse 会进入调试模式,断点直接打在本地代码对应位置
连接不上时,优先检查服务器安全组/防火墙是否开放了 5005 端口,以及应用的 JDWP 参数是否生效(启动日志里会有 Listening for transport dt_socket at address: 5005 类似的提示)。
4.4 Eclipse MAT 插件的内存调试辅助
内存泄漏和内存溢出(OOM)是 Spring Boot 应用常见的生产事故,Eclipse 生态里最经典的排查工具就是 MAT(Memory Analyzer Tool)。虽然它是独立运行的桌面程序,但 Eclipse 也可以安装 MAT 插件来集成使用。
用 MAT 排查内存问题的常规流程:
- 在 JVM 启动参数里加上
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump,OOM 发生时自动生成 heap dump 文件 - 或者用
jmap -dump:format=b,file=heap.bin <pid>手动导出 dump - 用 MAT 打开 dump 文件,查看
Leak Suspects和Dominator Tree
实操中我印象最深的一次排查:一个 Spring Boot 服务每隔几小时 OOM 一次,dump 分析后发现是 RabbitMQ 的消息监听器里,每条消息都往一个静态 List 里追加数据,没做清理。这种问题光看代码很难发现,但 MAT 一眼就能看到某个 List 对象的 retained size 巨大。
有一点要提醒:MAT 分析 dump 文件时很吃内存,建议给 MAT 分配足够的堆空间,一般 2G 起步。否则打开大 dump 文件时,MAT 自己会先 OOM。
5. 日志调试与 Spring Boot 特性结合
5.1 日志级别动态调整:线上问题不重启也能看
Spring Boot 的 spring-boot-actuator 模块提供了一项非常实用的能力:动态调整日志级别。生产环境排查问题时,不需要重启应用,就能把某个包的日志级别从 INFO 调成 DEBUG。
配置依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
然后在 application.yml 中暴露日志相关的端点:
yaml复制management:
endpoints:
web:
exposure:
include: loggers
接着,用 HTTP 请求动态修改日志级别:
bash复制curl -X POST http://localhost:8080/actuator/loggers/com.example.demo.mapper \
-H "Content-Type: application/json" \
-d '{"configuredLevel": "DEBUG"}'
这项操作本质上是修改内存中日志框架(Logback 或 Log4j2)的 Logger 配置,不需要改配置文件,也不需要重启应用。但它有安全隐患,比如在公网环境不设认证就暴露 actuator 端点,会有人恶意调整日志级别导致日志刷盘、拖垮性能。所以生产环境至少加一层 Spring Security 或 IP 白名单。
我自己的经验是,在测试环境多用这种方式验证“哪个包需要打印更多日志”,比反复打包部署效率高一个量级。
5.2 日志定位技巧:不是简单的 System.out
很多人调试时习惯加 System.out.println,然后跑一遍看输出。用 System.out 有两个明显的问题:一是输出没有时间戳、没有线程名,多线程并发时根本分不清哪条日志是谁打的;二是它不受日志框架的级别控制,打完忘了删就永久留在代码里。
正确的做法是使用 SLF4J + Logback(Spring Boot 默认):
java复制private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
log.info("查询用户开始,参数 userId={}", userId);
log.debug("SQL 执行结果,result={}", JSON.toJSONString(result));
log.error("调用远程接口失败,orderId={}, errorMsg={}", orderId, e.getMessage(), e);
在日志里打印对象时,用占位符加参数的方式,不要直接字符串拼接。"用户信息:" + user 在日志级别为 WARN 时,字符串拼接还是会发生,白白浪费性能;用 {} 占位符,只有真正要打印时才会执行参数转成字符串的逻辑。
调试时另一个比较强的定位技巧是:在 Service 层入口打一条 INFO 日志,在 Mapper 层打印 SQL 参数,在 Controller 层打印返回结果。三层日志相互对照,哪一层数据不对就能快速定位。日志要设计得“从上到下铺满流量路径”,不是每条都看,但出了问题时你顺手就能拉出一条完整链路。
5.3 Spring Boot 热部署与调试的组合拳
Spring Boot 下反复重启调试浪费时间,热部署能有效缓解。前面提到 devtools 的自动重启,但实际使用中有几个坑需要特别注意。
第一个坑:devtools 的自动重启依赖 IDE 自动编译。IDEA 里需要开启 Build project automatically,Eclipse 里需要开启 Project > Build Automatically。否则你改了代码,IDE 没有产生新的 class 文件,devtools 感知不到变化,自然也不会重启。
第二个坑:devtools 默认只用一个类加载器(Restart ClassLoader)加载项目自身的类,第三方依赖的类由 Base ClassLoader 加载。如果你改了依赖包里的源码(比如在本地仓库改了某个 jar 的类),devtools 不会自动重启,这个限制知道就好,平时很少遇到。
第三个坑:静态资源的修改没有必要触发重启,devtools 默认会把 /static、/templates 下的变化处理为浏览器自动刷新(需要配合 LiveReload)。如果你改的是前端页面,不需要 Java 类变更,就不会触发应用重启。
使用 JRebel 是另一种热部署方案,它比 devtools 更极客——不需要重启应用上下文,类字节码热替换即时生效。但 JRebel 是商业工具,配置上有学习成本,普通的 Spring Boot 调试用 devtools 完全够用。
5.4 Redis Stream 消息调试:断点打在监听器里
Spring Boot 集成了 Redis,而 Redis Stream 作为一种消息队列在很多项目里被使用。调试这类消息监听消费逻辑时,最常见的困惑是“消息压根没被消费到”,这时候断点怎么打都不生效。
先在配置层面确认监听器是否正常订阅了 Stream。Spring Data Redis 里典型的配置:
java复制@Bean
public StreamMessageListenerContainer<String, MapRecord<String, Object, Object>> streamMessageListenerContainer(
RedisConnectionFactory connectionFactory,
StreamMessageListenerContainerOptions<String, MapRecord<String, Object, Object>> options) {
StreamMessageListenerContainer<String, MapRecord<String, Object, Object>> container =
StreamMessageListenerContainer.create(connectionFactory, options);
return container;
}
配置没问题后,调试时先测量消息是否能从 Stream 里拉取到。最简单的办法是在 Stream 消息处理的方法入口打一个断点,然后用 XREAD 命令或者 Redis 客户端手动向 Stream 里追加一条消息:
bash复制XADD order-stream * orderId "1001" amount "99.9"
如果断点没生效,考虑两个原因:一是监听器容器没有注册到 Stream 的消费者组;二是 Rebalance 机制导致消息被分到了其他实例消费(如果有多副本同时订阅)。我遇到过几次:代码改了一版监听逻辑,但旧实例没停干净,新实例收不到消息。排查这种问题,优先看启动日志里的监听器注册信息,再去看 Redis 的 Stream 消费组状态 XINFO GROUPS order-stream。
6. 远程调试与容器化部署的实战经验
6.1 为什么需要远程调试
本地跑得好好的,部署到测试环境就挂——这是非常经典的场景。原因有很多:操作系统差异、环境变量不同、依赖的中间件版本不一致、数据库数据不同。如果只能靠日志排查,效率低。远程调试解决的核心问题是:在本地 IDE 里直接打断点,观察远程环境运行时各个变量的真实状态。
但远程调试也不是万能药,它有几个明显的限制:
- 网络延迟高的时候,单步执行会变得非常卡
- 远程调试会显著降低应用的执行速度,不适合高并发压测
- 调试端口会暴露在网络上,直接暴露到公网有被攻击的风险
所以我的建议是:远程调试适合在开发环境、测试环境做定点问题排查,生产环境尽量不用,生产环境的问题靠日志和监控解决。
6.2 远程调试的参数细节
Spring Boot 远程调试本质上依赖 JVM 的 JPDA 机制。不同 JDK 版本的启动参数略有差异,以 JDK 8 和 JDK 11 为例:
bash复制java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar demo.jar
JDK 9+ 的模块化后,有些版本的 JDK 会要求使用 address=*:5005 的形式,表示监听所有网卡。如果只写 address=5005,某些 JDK 版本默认只监听本机回环地址,这样远程客户端就无法连接。
参数详解:
| 参数项 | 作用 | 建议值 |
|---|---|---|
| transport | 通信方式,固定为 dt_socket | dt_socket |
| server | 应用作为调试服务端,等待调试器接入 | y |
| suspend | 启动时是否暂停等待调试器 | n(正常启动)/ y(排查启动阶段问题) |
| address | 调试监听端口 | 找一个不冲突的端口,比如 5005 |
suspend=y 有一个巧妙用法:如果应用启动阶段就抛异常,还没来得及让调试器连接就已经退出了,那么把 suspend=y 打开,应用会停在启动最初阶段,等调试器连接后再继续运行。这样就能在 Spring Boot 初始化早期代码上打断点排查了。
6.3 IDEA 与 Eclipse 的远程调试连接
IDEA 连接远程调试:
- 选择
Run > Edit Configurations - 添加
Remote JVM Debug配置 Host填远程服务器 IP,Port填 5005Command line arguments for remote JVM需要与本机 JDK 版本匹配,直接复制使用- 点击 Debug 开始连接
Eclipse 连接远程调试在 4.3 已经介绍过。有一点通用层面的经验要分享:远程调试要求本地代码和远程部署的代码版本保持一致。如果远程服务器上的 jar 包比本地代码新(或者旧),断点位置会错乱,调试出来的数据也没有参考意义。连接前先确认部署的版本号,最好让运维记录一下部署的 git commit hash,避免“调了半天发现版本不对”。
6.4 Docker 容器里的 Spring Boot 远程调试
现在的项目很多都用 Docker 部署,在容器里开远程调试和普通 JVM 进程略有不同。核心问题是端口映射和容器内的监听地址。
Docker 启动命令:
bash复制docker run -d \
-p 8080:8080 \
-p 5005:5005 \
-e JAVA_TOOL_OPTIONS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005" \
demo-image:latest
容器内的 JVM 监听 5005 端口,宿主机把 5005 端口映射出来,本地 IDE 连接宿主机的 IP 和 5005 端口就能调试。
用环境变量 JAVA_TOOL_OPTIONS 传 JVM 参数,比直接写死在 Dockerfile 里更灵活——开发、测试环境可以使用,生产环境不传这个环境变量就行。但要注意一点:JAVA_TOOL_OPTIONS 会被所有 JVM 进程读取,包括容器内可能有的其他 Java 工具,可能带来一些干扰。
另一个坑是 Docker 网络模式。如果用 bridge 模式,端口映射按上面方式没问题;如果用 host 模式,容器直接使用宿主机网络,那 -p 参数就不生效,JVM 监听 5005 端口就直接是宿主机 5005 端口,本地 IDE 连宿主机 IP 即可。
6.5 Docker Compose 场景下的调试配置
docker-compose.yml 里配置远程调试,可以使用 environment 或者 ports 组合,推荐在 service 定义中显式加入:
yaml复制services:
demo-app:
image: demo-image:latest
ports:
- "8080:8080"
- "5005:5005"
environment:
JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005"
有个小技巧值得分享:suspend=y 和 Docker Compose 配合时的时序问题。如果容器里 JVM 因为 suspend=y 而暂停等待调试器,但容器的健康检查(healthcheck)一直不通过,编排工具可能会反复重启容器。所以在 Compose 里设置 suspend=n 更稳妥,应用正常启动,调试器随时可以附着上去。
7. 常见问题与排查技巧实录
7.1 “找不到或无法加载主类 org.apache.catalina.startup.Bootstrap”
这个报错在 Eclipse 启动 Spring Boot 项目时偶尔出现,在网络上相关搜索热度也很高。出现这个问题的本质是:Eclipse 试图用 Tomcat 的方式启动应用,但 Tomcat 的类没有正确加载。
排查步骤:
- 在项目上右键
Maven > Update Project,强制刷新依赖 - 检查项目的
Deployment Assembly,确认没有错误地加入 Tomcat 相关依赖 - 检查启动方式是否正确。Spring Boot 项目不要用
Run As > Run on Server(这是给传统 Web 项目用的),应该用Run As > Java Application或者在启动类上直接Run As > Spring Boot App
我见过很多次这个报错,都是“用错了启动方式”导致的。Spring Boot 内嵌了 Tomcat,不需要外部的 Tomcat 容器,所以用 Java Application 启动才对。
7.2 IDEA 提示 “Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=GBK”
这个提示在 Windows 环境下非常常见,IDEA 控制台和 Eclipse 控制台都会出现。它表示系统环境变量里设置了 JAVA_TOOL_OPTIONS,JVM 启动时会自动读取这个环境变量的值。如果里面设置了 -Dfile.encoding=GBK,那么所有 JVM 进程的编码都会变成 GBK,尤其当你的项目使用 UTF-8 编码时,中文会出现乱码。
处理方式:
- 在系统环境变量里删除
JAVA_TOOL_OPTIONS,或者把编码值改成 UTF-8 - 在 IDEA 的
Help > Edit Custom VM Options中加一行-Dfile.encoding=UTF-8 - 在运行配置里,VM options 显式加上
-Dfile.encoding=UTF-8
注意,JAVA_TOOL_OPTIONS 是“全局生效”的环境变量,本机所有 Java 程序都会受影响。删掉它之后,某些 IDE 插件或者 Maven 脚本可能会失去原有的参数,建议改成在具体工具里配置编码,而不是依赖全局环境变量。
7.3 断点没命中的常见原因
断点打上了,但程序跑过那一行时没有暂停。这类问题排查的思路顺一下:
- 代码没有重新编译:确认 IDE 的自动构建是否开启,或者手动 Build 一次
- 调试时启动的是错误的运行配置:IDEA 里可能有两个同名的启动类,一个 Debug 一个 Run,确认点的是 Debug 按钮
- 类加载器不同:Spring Boot 项目如果有多个 Context,或者是 devtools 的 Restart ClassLoader 加载的类,断点仍能被识别,但某些场景下(比如容器里运行)可能失效
- CPU 架构不一致:远程调试时,本地是 x86 的 jar,远程是 ARM 的 jar,这类情况少见但确实存在
还有一种情况是方法被 JIT 编译内联了。代码执行频率很高时,JVM 可能会把方法内联,导致断点无法精确命中。处理方式是调整 JIT 参数,或者干脆在方法声明的第一行打断点,不要打在方法内部的某一五行代码上,减少内联带来的偏移。
7.4 调试时 Spring Boot 启动很慢
启动慢主要卡在几个地方:Spring 容器的 Bean 初始化、数据库连接池创建、第三方中间件连接(Redis、MQ 等)。调试时反复重启特别折磨人。
有几个实用优化:
- 跳过不需要测试的 Bean 初始化:比如某个定时任务组件在启动时连接外部系统,可以在测试环境通过
@Profile或@ConditionalOnProperty把它临时排除 - 拆开配置:把非核心的 Starter 依赖(比如 Actuator、Security)在本地调试时通过 profile 排除
- 关闭 devtools 的自动重启,手动控制重启时机,减少无谓的重新加载
还有一个很容易被忽略的:IDEA 增加内存或调整编译器输出目录没有解决启动慢。真正的瓶颈常常是 Spring Cloud 的服务发现,比如启动时注册到 Nacos 或者拉取配置中心,断网时会不断重试。本地调试可以配置使用本地环境,或者关闭自动注册。
7.5 远程调试连接被拒绝
远程调试端口连不上,按以下顺序排查:
- 确认远程 JVM 是否真的在监听该端口:
lsof -i:5005或netstat -antup | grep 5005 - 确认防火墙 / 云服务器的安全组是否开放了 5005 端口
- 确认应用启动日志里有没有
Listening for transport dt_socket at address: 5005的输出 - 确认 JDK 版本对应的启动参数格式是否匹配(
address=5005还是address=*:5005)
另外一个比较隐蔽的问题:如果服务器上多个 Spring Boot 实例同时部署,可能只有一个实例能绑定 5005 端口。不同实例建议使用不同的调试端口,比如 5005、5006、5007,然后在监听配置中分别指定。
8. 在真实项目里打磨调试思路
8.1 从一次线上问题反推调试流程
之前遇到过一个问题:用户反馈订单列表分页数据偶尔“多出来一条”。本地复现不了,代码逻辑看起来也没错。后来用远程调试的方式,在查询接口打上断点,每跑一次都看一眼 SQL 参数,最终发现问题出在分页插件的页数计算上——当用户请求第 0 页时,某些场景下会把页码直接传为 0,而 mybatis-plus 分页组件认为第 0 页和第 1 页等价,导致每次查到第二条记录时出现偏移。
这次排查看下来,真正起作用的不是哪个花哨的调试功能,而是把断点放在数据入口、数据加工、数据出口三个关键位置,逐层缩小范围,最终定位到参数值不对。调试的功夫不在“操作快捷键”上,而在“知道哪里该停下、该看什么”。
8.2 团队协作中的调试姿势
多人协作开发时,调试会引入一些额外问题:本地代码跟分支不一致、数据库被多人共用、同一时间多人调试同一个接口互相影响。
我比较推荐的做法是:
- 开发环境的数据库做独立副本,多人调试互不干扰
- 调试时禁止改数据库表结构(除非有迁移脚本)
- 本地调试用的配置(端口、profile、Mock 策略)统一放在
application-local.yml里,团队共享一份 - 遇到“本地跑不起来”的问题,第一时间拉最新代码、更新依赖,而不是凭着旧缓存反复折腾
远程调试时还有一个配合建议:如果同一个服务由多个同事同时远程调试,建议分配不同的调试端口。否则后连接的客户端会把先连接的顶掉(JPDA 同一端口同一时刻只允许一个调试器附着)。
8.3 给新人的三点建议
如果你刚开始学 Spring Boot 调试,不用急着把所有高端功能都掌握。把下面三件事做熟练,已经能处理大部分日常问题。
第一,熟练打断点、单步执行、看变量。这是基本功,每天写代码时都刻意练习。
第二,学会看调用栈。程序停下来之后,先别急着往下走,把 Frames 面板里每一层的类名、方法名、行号都看一遍,理清这次调用是从哪里发起的、经过了哪些层。
第三,练习用日志佐证。断点能帮你看到局部变量,但它的观察点是“瞬间的”;日志能看到一段时间内程序的行为轨迹。两者配合,才能形成一个完整的排查视角。
9. 工具选型与配置的高级经验
9.1 IDEA 社区版和付费版在调试上的差异
很多新手入门时会纠结用 IDEA Community 还是 Ultimate。实际上从调试功能的角度看,社区版已经覆盖了基本的断点、变量观察、求值表达式等核心能力。两者的差异主要体现在框架支持上,比如 Spring Boot 的专用启动配置、Run Dashboard、Spring 相关可视化,社区版都没有,但这些对调试本身的影响不大。
不过有一点要提醒:社区版对 Spring Boot 的自定义配置界面有阉割,比如没有 Spring 面板,导致你无法直观地查看 Bean 之间的依赖关系、无法查看 HTTP Mapping 列表。调试时如果你依赖这些视图来导航代码,建议升级 Ultimate。但学生、开源开发者可以申请免费授权,不用整天找“激活”之类的方案——从我个人的经验看,合法授权用起来才安心,安全合规永远是最重要的底线。
9.2 Eclipse 官方下载与 Spring Tools 的整合
Eclipse 的下载不要随便去第三方网站,认准官方源。下载 Eclipse IDE for Enterprise Java and Web Developer 版本,这个版本自带 WTP 插件,对 Web 开发更友好。装完之后,如果要做 Spring Boot 开发,可以在 Help > Eclipse Marketplace 里搜 Spring Tools 4 安装。
Spring Tools 4 装好后,原本的 Spring Boot App 启动方式就出来了。它会自动识别 @SpringBootApplication 类,调试时运行配置也会带上 Spring Boot 的 Classpath,省去很多手动配置。用 Eclipse 做 Spring Boot 开发,没有这个插件的话体验会差很多。
9.3 本地 Maven 配置对调试的影响
Maven 配置有问题时,调试时的表现常常是“依赖报错”“类找不到”。很多同学不知道,本地的 Maven settings.xml 里的镜像源、本地仓库路径都会影响 Spring Boot 项目的构建。
settings.xml 里常出的问题:
- 本地仓库路径用了中文目录,某些编译器插件会报错
- 镜像源配置了不可用的地址,导致依赖下载半途而废
- 全局 JDK 指向错误版本,编译出的 class 文件版本不对
推荐配置方式:在 IDEA 或 Eclipse 里给每个项目单独关联 settings.xml 文件,不要依赖全局默认配置。项目内的 .mvn 目录可以放置 maven.config 来指定参数,团队统一版本。这样本地的构建环境可复现,调试时依赖问题会少很多。
9.4 多模块 Maven 项目的断点调试策略
现在很多企业项目是 Maven 多模块结构,比如 common、dal、service、web 拆开。调试多模块项目时,断点不一定只在 web 模块里打,也可能打在 service 的某个方法里。
IDEA 导入多模块 Maven 项目后,Run Configuration 里选择启动类所在的那个模块。但有一个经常遇到的问题:common 或其他依赖模块改了源码后,IDEA 如果没重新编译,断点还是旧 class。遇到这种情况,在 Maven 面板里对根项目执行 clean compile 即可。
另一个经验:多模块项目的调试范围要控制在最小。比如你只改了 service 层,本地调试时不要去触发 common 里的静态初始化(可能连数据库),减少副作用。
10. 调试相关的 JVM 参数和工具链
10.1 常用 JVM 参数在调试场景中的用法
调试 Spring Boot 时,JVM 参数不只是启动时要写,有些参数在调试过程中会直接影响你的体验:
-Xmx和-Xms:限制堆内存。调试时如果代码有内存泄漏,堆无限上涨会让你以为“程序卡了”,实际上是在 GC 一直在尝试回收。限制堆大小之后,OOM 会提前暴露,更容易定位问题。-XX:+HeapDumpOnOutOfMemoryError:OOM 时自动导出 dump 文件,配合 MAT 分析-XX:+PrintGCDetails:打印 GC 详细日志。调试内存问题时能看出是否频繁 Full GC-Dserver.port=8081:本地调试时端口冲突了直接换端口,不用改配置文件
一个使用建议:本地调试的 IDE 运行配置里,把最大堆设置成和测试环境一致,这样某些内存相关的 bug 能在本地提前复现,不用等到部署到测试环境再发现。
10.2 jstack 与 jmap:当断点也无能为力时
有一类问题断点帮不上忙——比如应用已经出现了死锁、线程卡死、CPU 飙高。这种时候程序不一定停在你设断点的位置,用 IDE 的调试功能反而会加重卡顿。
处理这类问题,JDK 自带的工具比 IDE 更好用:
jstack <pid>:打印线程栈,能看到每个线程当前在哪个类的哪一行执行jmap -heap <pid>:查看堆内存使用概况jstat -gcutil <pid> 1000:实时查看 GC 情况jcmd <pid> help:查看支持的诊断命令
比如排查“接口响应变慢”的问题,先看线程是不是都阻塞在获取数据库连接上,可以用 jstack 找到大量 WAITING 状态的线程,堆栈上能看到类似 HikariCP.getConnection 的调用,就说明数据库连接池耗尽了。这种问题用 IDE 断点根本看不出全局情况,因为等待是“动态”的。
我把这些工具称为“进程级调试”,跟“代码级调试”互补。遇到线上问题,先通过 jstack / jmap 做整体判断,再决定要不要远程断点深入。
10.3 IDEA 的 Java Flight Recorder 集成
JDK 11 之后,JFR(Java Flight Recorder)开源,IDEA Ultimate 也内置了 JFR 的录制启动功能。它相当于给 JVM 做一次“黑匣子”录制,能记录方法调用耗时、GC 情况、锁竞争、IO 等,之后在 IDE 里打开分析。
调试时 JFR 的价值是:知道某个接口慢,但不知道慢在哪。断点只能告诉你当前这一步执行了,却很难精确统计某一步耗时。用 JFR 录制一次请求,能直接看到 Hot Methods——耗时占比最高的方法列表。
注意,JFR 采样本身有性能开销,本地调试时开着没问题,生产环境建议按需开启,录制约 1 分钟就停止,再下载分析。
11. 我的调试习惯与方法论
11.1 调试前先静下来想三分钟
很多同学(包括我自己刚入行时)都是一通操作猛如虎,断点打得密密麻麻,跑起来看哪个先命中就分析哪一个。这种“暴力调试法”不是完全无效,但效率很低。
现在我更习惯的做法是:拿到一个 bug,先花三分钟把问题描述清楚,明确“期望结果”和“实际结果”之间的差异在哪一层。然后想象请求从客户端到服务端的完整链路,把所有可能出问题的地方列出来,再决定在哪些位置打断点。断点数量不需要多,三五个关键位置足够。
11.2 用条件断点模拟多场景测试
平时测试一个接口,经常要分别验证参数为正常值、空值、超长字符串等不同的场景。传统方式是每次改请求参数、重新调用。有了条件断点,就可以在调试器里一次实现多分支观察。
比如 Controller 的入口方法上有断点,右键设置为条件断点,条件写 userId == null || userId.isEmpty(),那么只有传入参数异常时才会停下。这样正常请求不受断点影响,只在参数异常、你想确认代码行为时停住。
这个方法在分批处理、定时任务这类场景里特别有用。比如定时任务每天处理 10 万条记录,你只关心其中 100 条异常数据,直接给断点加条件 record.getStatus() == 2,其他记录一路放行,效率极高。
11.3 保存与复用调试配置
在一个项目里,你可能会反复调试同一个功能。IDEA 和 Eclipse 都支持保存调试配置快照。IDEA 里,运行配置点击 Save 之后,下次直接从主界面下拉框选取,不需要重新填参数。
多个项目之间也可以共享调试配置——IDEA 的运行配置可以选择存储在项目文件中(.run 文件),提交到 Git 仓库后,团队其他成员拉代码就能直接使用同一套启动/调试配置,省去反复沟通环境参数的时间。这个小习惯对团队协作非常友好。
11.4 何时不用调试器
调试器不是万能的。有些场景下,它甚至不如一个简单日志有效:
- 并发问题:断点会改变线程时序,在你暂停某个线程的时候,其他线程继续跑,可能掩盖了真正的竞态条件
- 生产环境:生产环境不允许随便断点,日志和 APM 是更好的选择
- 耗时性能问题:断点本身会打断时间测量,不可信
- 消息队列消费延迟:这类问题更多依赖监控和日志,而不是断点
判断一个 bug 该用什么手段,核心是看你要观察的“变量”是什么、以及观察行为本身是否会影响结果。这是我调试多年总结出来最重要的一句话。
12. 个人经验总结
调试技巧本质上不是“IDE 操作大全”,而是一套“让你的代码变得可见”的能力。IDEA 和 Eclipse 各有各的玩法,但核心逻辑一致:把人脑中推理代码执行过程的部分,交给工具去验证。工具越熟练,开发效率越高,排查问题越从容。
最后分享一个我坚持了很久的小习惯:每次调试解决完一个比较难的问题,我会顺手写一段笔记,记录三件事——问题现象是什么、断点打在了哪几个位置、最终是怎么定位到根因的。半年后再翻这些笔记,能发现自己对常用框架(Spring Boot、MyBatis、Redis 等)的理解在一次次调试中加深了。这个习惯虽然不起眼,但确实帮我积累了大量实战经验,也让我在面试和实际项目中更有底气。
