【Java】Debug断点调试常用技巧 ⟳
Debug适用场景⟳
- 在程序出现问题时,查看参数变化以及方法的调用。
- 查看参数结构
- 查看方法调用以及参数变化
debug设置⟳
debug断点状态⟳
先讲一个开发人员经常会遇见的现象~
A和B两个developer共同负责同一个项目P的开发,P在dev环境上只部署了一台机器。有一天,A需要远程调试P的接口1,于是他使用本地idea启动remote连接到了P,debug的不亦乐乎。而此时B正在调用这台机器的接口2,B突然发现刚才还好好的,突然就不能访问了(B一脸懵逼样)……
听完了故事,下面我们进入主题~
开发人员经常会使用到本地debug功能,有时候有场景需要远程debug日常环境的机器,在这种情况下可能会有多个人同时在使用这台机器,经常出现的现象是某一个人在远程debug这台机器,导致其他人一直在等待。而其他人也是一脸懵逼,不知道这台机器到底发生了什么……
本文的目的是站在debug操作者的角度,探讨如何最小化的避免自己远程debug时对其他人造成影响。
最佳实践
- Suspend 设置为 Thread (设置为默认 : Make Default)
- Condition 根据该断点上方的变量,编写只对自己生效的代码。
Enabled⟳
是否可用。标识该断点是否生效。优先级最高。
Suspend⟳
该断点的生效范围。优先级次于Enabled。
分为两个级别:
All
:对整个java应用生效。程序运行到这个断点时,其他的线程都会停止,直到这个断点放开。Thread
:仅对当前线程生效。程序运行到这个断点时,不影响其他的线程。
后端开发调试某个数据的时候,前端总是嫌弃后端断点,影响到他开发,这时候我们就可以使用thread级别。
Condition⟳
可以编写断点生效的条件。
虚拟机栈(调用堆栈)⟳
JVM 直接对 Java 栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
理解:也就是说完整的调用流程为,从栈底到栈顶。
下面写一个简单的代码
/**
* 栈帧
*
* @author: Nemo
*/
public class StackFrameTest {
public static void main(String[] args) {
method01();
}
private static int method01() {
System.out.println("方法1的开始");
int i = method02();
System.out.println("方法1的结束");
return i;
}
private static int method02() {
System.out.println("方法2的开始");
int i = method03();;
System.out.println("方法2的结束");
return i;
}
private static int method03() {
System.out.println("方法3的开始");
int i = 30;
System.out.println("方法3的结束");
return i;
}
}
输出结果为
方法1的开始
方法2的开始
方法3的开始
方法3的结束
方法2的结束
方法1的结束
满足栈先进后出的概念,通过 IDEA 的 DEBUG,能够看到栈信息
在debug的过程中,我们可以多关注一下调用栈,这样就能清楚的知道当前方法的调用链路。
Debug操作技巧⟳
Show Execution Point
⟳
将光标回到当前断点停顿的地方
Step Over
⟳
执行当前行代码,并将运行进度跳转到下一行。
Step Into
⟳
进入到当前代码行的方法内部。
Step Out
⟳
从方法内部出去
Force Step Into
⟳
强制进入Java自带方法的内部
Run to Cursor
⟳
将光标定位到想到达的代码行
点击Run to Cursor
Drop Frame
⟳
丢弃当前虚拟机栈帧
初始:
进入方法:
丢弃当前帧:
也就是说,我们退回了上一步进入方法之前。
Evaluate Expression
⟳
可以用它来评估表达式
如 p.getName()等。
Force Return | 避免操作资源⟳
我们在调试代码的时候中间出现了异常,但是我们又没有做异常捕获,稀里糊涂地把错误数据存到了数据库中,我们又需要将这些数据给删除,将数据库复原,才能达到之前我们需要的效果。
所以,接下来我们讲一讲如何避免操作资源,强制返回。
public static void saveResource() {
System.out.println("shit happens");
System.out.println("save to db");
System.out.println("save to redis");
System.out.println("send message to mq for money payout");
}
debug:
我们发现程序出现了异常
Force Return
它会只打印shit happens,不会继续向下执行了。
Trace Current Stream Chain | Stream Debug
⟳
public static void streamDebug() {
// stream chain
Arrays.asList(1, 2, 3, 45).stream()
.filter(i -> i % 2 == 0 || i % 3 == 0)
.map(i -> i * i)
.forEach(System.out::print);
}
左下角平铺模式Flat Mode:
断点常用技巧⟳
断点(Breakpoint)⟳
断点:如果把程序想象成一条平滑的线,那么断点就是一个结,可以让程序中断在需要的地方,从而方便其分析。
设置断点:在代码里需要调试的地方,鼠标双击代码行号的左边,再次双击即可取消断点。
在调试中可以设置的断点类型有五种:
-
行断点:
spring在注册Bean定义(registerBeanDefinition)时,如果是org.springframework.demo.MyBean,就挂起线程,可以开始单步调试了。
对于命中次数(hit count)的使用,一般是在循环中,第N个对象的处理有问题,设置hit count = N, 重调试时,可以方便到达需要调试的循环次数时,停下来调试。 -
方法断点:
方法断点的好处是可以从方法方法进入或者退出时停下来调试,类似行断点,而且只有行断点和方法断点有条件和访问次数的设置功能。
但是方法断点还有另外一个好处,如果代码编译时,指定不携带调试信息,行断点是不起作用的,只能打方法断点。
有兴趣的可以将Add line number…前的勾去掉,调试下看看。 -
观察断点:
在成员变量上打的断点。只有对象成员变量有效果,静态成员变量不起作用。
可以设置变量被访问或者设置的时候挂起线程/VM。 -
异常断点:
系统发生异常时,在被捕获异常的抛出位置处或者程序未捕获的异常抛出处挂起线程/VM, 也可以指定是否包括异常的子类也被检测。 -
类加载断点:
在类名上打的断点。接口上是打不了类加载断点的,但是抽象类是可以的,只是在调试的时候,断点不会明显进入classloader中,单步进入知会进入到子类的构造方法中,非抽象类在挂起线程后单步进入就会到classloader中(如果没有filter过滤掉的话)。类加载断点不管是打在抽象或者非抽象类上,都会在类第一次加载或者第一个子类第一次被加载时,挂起线程/VM。
注意:每种断点的设置有些许不一样,可以在断点上右键->Breakpoint properties进行设置,但一般在断点窗口有快速设置的界面,Breakpoint properties中多了filter, 其实比较鸡肋,用处不大。
调试状态⟳
启动服务开始调试:
- 方法一:例如上图的代码中,鼠标点击main方法-->右键Debug As-->Java Application开始java代码调试;
- 方法二:直接点击“调试”按钮,即点击小瓢虫边上的倒三角,选择Debug As-->Java Application;
- 方法三:快捷键F11;方法四,菜单栏选择Run-->Debug,还有其他方法此处不再赘述了。
开发工具首次调试会弹出提示,需要切换到Debug工作区,勾选“Remember my decision”,下次便不再提示。
调试执行:
功能 | 快捷键 | 描述 | 备注 |
---|---|---|---|
Step Info | F5 | 单步进入(如果有方法调用,将进入调用方法中进行调试) | 逐语句 |
Step Over | F6 | 单步跳过(不进入行的任何方法调用中,直接执行完当前代码行,并跳到下一行) | 逐过程 |
Step Return | F7 单步返回(执行完当前方法,并从调用栈中弹出当前方法,返回当前方法被调用处) | 跳出 | |
Resume | F8 | 恢复正常执行(直到遇到下一个断点) | 继续运行 |
Run to Line | Ctrl+R | 执行到当前行(将忽略中间所有断点,执行到当前光标所在行) | |
Drop To Frame | 无 | 回退到指定方法开始处执行,这个功能相当赞。 在方法调用栈上的某个方法右键,选择Drop To Frame就可以从该方法的开始处执行,比如 重新执行本方法,可以在本方法上用Drop To Frame,将从本方法的第一行重新执行。 当然对于有副作用的方法,比如 数据库操作,更改传入参数的对象内容等操作可能重新执行就不再是你想要的内容了。 |
|
Copy Stack | 无 | 拷贝当前线程栈信息 |
断点⟳
public class BreakPointDemo {
// 行断点
public static void line() {
System.out.println("this is the line break point");
}
// 详细断点(源断点)
public static void detailLine() {
System.out.println("this is the detail line break point");
}
// 方法断点 | 接口跳转实现类
public static void method() {
System.out.println("this is from method");
IService iservice = new IServiceImpl();
iservice.execute();
}
// 异常断点 | 全局捕获
public static void exception() {
Object o = null;
o.toString();
System.out.println("this line will never be print!");
}
// 字段断点 | 读写监控
public static void field() {
Person p = new Person("field", 10);
p.setAge(12);
System.out.println(p);
}
public static void main(String[] args) {
line();
detailLine();
method();
exception();
field();
}
}
行断点⟳
// 行断点
public static void line() {
System.out.println("this is the line break point");
}
使用鼠标左键点击代码左侧:
右键点击行断点,我们也可以进行一些断点停顿的条件设置:
如 i == 20等条件。
Suspend也可以选择线程模式,我们可以切换不同的线程,来观察不同线程的该语句的运行效果。(如果是All的话,那就是哪一个线程先过来,那就是哪个线程)
详细断点⟳
// 详细断点(源断点)
public static void detailLine() {
System.out.println("this is the detail line break point");
}
SHIFT+鼠标左键:
debug:
方法断点 | 接口跳转实现类⟳
方法断点 = 方法起始行断点 + 方法结尾行断点
// 方法断点 | 接口跳转实现类
public static void method() {
System.out.println("this is from method");
IService iservice = new IServiceImpl();
iservice.execute();
}
在方法上打断点:
debug:
第一个断点停留在方法体内第一行代码:
第二个断点停留在方法体内返回的最后一行代码:
在接口方法上打断点:
真正运行的是接口方法的实现类:
如果我们有很多的实现类,我们具体不知道是哪一个,我们只需要在接口方法上打一个断点,它就会自动地跳到接口具体的实现类方法上。
异常断点 | 全局捕获⟳
// 异常断点 | 全局捕获
public static void exception() {
Object o = null;
o.toString();
System.out.println("this line will never be print!");
}
异常断点会停顿在报出异常的具体代码行。
-
点击View Breakpoints
-
在异常断点处添加新的异常断点
-
接下来,只要你的程序遇到空指针异常,它就会停顿到发出空指针异常的那一行代码那里。
没有显式打断点:
debug:
这个异常断点对于我们异常调试很方便。
字段断点 | 读写监控⟳
// 字段断点 | 读写监控
public static void field() {
Person p = new Person("field", 10);
p.setAge(12);
System.out.println(p);
}
在类的字段属性上打断点:
我们在字段左边打了一个字段断点(小眼睛),它就会去监控该字段属性的整个生命周期的值的变化。
dubug:
第一个:构造方法修改了属性值
第二个:setter方法修改了属性值
不暂停的 debug⟳
通常情况下,我们断点的时候,会卡住当前线程。
假设我们在主预发或者公用环境进行debug,就老感觉背后有人在骂我,谁又在debug,环境怎么又不通了!所以当我们想要愉快在公用环境debug的时候要:
查看变量值⟳
不暂停直接观察相关变量值
强行改变变量值⟳
是谁调用了我⟳
可以直接观察到调用堆栈,类似 Arthas 的 trace
你进来了不⟳
如果执行了断点所在位置,会在控制台打出一行日志:
远程断点调试 Debug⟳
远程断点调试流程⟳
运行原理:
- 远程服务器开启提供调试的端口号
- IDE客户端通过此端口号连接上服务器
- 服务器通知客户端运行到了哪一行
正常情况下:
- 用户访问服务器ip地址
- 服务器返回结果
远程断点调试Debug情况下:
- 用户访问服务器
- 服务器得到了具体运行的那一行
- 然后就会去问一下本地跟它连接的IDEA,问它有没有这一行的断点
- IDEA检查了一下自己的断点列表
- 然后开发人员就开始debug调试
- 结束调试,将结束调试的信息返回给服务器
- 服务器返回运行结果给用户
实例⟳
代码:
@SpringBootApplication
@RestController
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
// 远程服务器地址 192.168.31.224
@RequestMapping("/{name}")
public String hello(@PathVariable("name") String name) {
System.out.println("debug is running");
return "hello world" + name;
}
}
点击
点击
选择Attach to remote JVM(依附上远程服务器的端口)
填写远程服务器的ip地址,指定远程服务器和本地IDE进行socket连接进行断点调试的端口号(随意即可)
科普:我们本地debug的时候也是用的socket连接,只不过此时的socket客户端和服务器都是本地而已。
可以看到下面生成了命令行参数,在服务器端使用java -Xdebug -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar demo.jar
,上面生成的为命令行参数(如果报错,就将 * 号进行转义处理),这样服务器就启动了。
运行Debug程序
就可以正常Debug了
问题:如果我们把本地的代码修改了之后,服务器返回给用户的信息会改变吗?
回答:不会改变,虽然说我们把本地的代码改了,但是我们本地的修改没有部署到线上去,所以我们程序运行的逻辑还是按照线上来运行的
实例⟳
启动无报错调试⟳
场景需求⟳
今天在项目添加了微信支付后,原本以为绝对OK,启动SpingBoot服务,好家伙,直接起不来,控制台也不输出异常信息。一猜就应该是自己手欠,没办法,只能用最朴实的办法解决了,debug。
调试方法⟳
步骤一
在你的项目启动类的这行代码打一个断点,并复制这行代码
步骤二
同时按下快捷键 Art+F8,然后在框框中输入,如下所示
步骤三
点击按钮 Evaluate,进行分析
然后你会看到对应的错误提示,注意,有的不是在第一层,你需要点击cause这里,层层点击进去,看到没,是不是很熟悉的场景,bean注入失败
检查⟳
我这里是引入了redssion客户端导致的,版本不一致的原因。
写在最后
关于spring,一定要好好掌握,尤其是理解容器管理的含义,按照自己能够理解的方式形象记忆,这样你才能快速定位问题。
笔者将不定期更新【考研或就业】的专业相关知识以及自身理解,希望大家能【关注】我。
如果觉得对您有用,请点击左下角的【点赞】按钮,给我一些鼓励,谢谢!
如果有更好的理解或建议,请在【评论】中写出,我会及时修改,谢谢啦!
本文来自博客园,作者:Nemo&
转载请注明原文链接:https://www.cnblogs.com/blknemo/p/14907523.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· Trae初体验