𝓝𝓮𝓶𝓸&博客

【Java】Debug断点调试常用技巧

Debug适用场景

  1. 在程序出现问题时,查看参数变化以及方法的调用。
  2. 查看参数结构
  3. 查看方法调用以及参数变化

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 根据该断点上方的变量,编写只对自己生效的代码。

多人同时远程Debug冲突解决方案

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 Pointimage

将光标回到当前断点停顿的地方
image

Step Overimage

执行当前行代码,并将运行进度跳转到下一行。

Step Intoimage

进入到当前代码行的方法内部。
image

image

Step Outimage

从方法内部出去
image

image

Force Step Intoimage

强制进入Java自带方法的内部
image

image

Run to Cursorimage

image

将光标定位到想到达的代码行
image

点击Run to Cursor
image

Drop Frameimage

丢弃当前虚拟机栈帧

初始:
image

进入方法:
image

丢弃当前帧:
image

也就是说,我们退回了上一步进入方法之前。

Evaluate Expressionimage

可以用它来评估表达式
image
如 p.getName()等。
image

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:
image

我们发现程序出现了异常
image

Force Return
image

它会只打印shit happens,不会继续向下执行了。
image

Trace Current Stream Chain | Stream Debugimage

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);
}

image

image

image

image

左下角平铺模式Flat Mode:
image

断点常用技巧

断点(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");
	}

使用鼠标左键点击代码左侧:
image

右键点击行断点,我们也可以进行一些断点停顿的条件设置:
image

如 i == 20等条件。

Suspend也可以选择线程模式,我们可以切换不同的线程,来观察不同线程的该语句的运行效果。(如果是All的话,那就是哪一个线程先过来,那就是哪个线程)
image

详细断点

	// 详细断点(源断点)
	public static void detailLine() {
		System.out.println("this is the detail line break point");
	}

SHIFT+鼠标左键:
image
image

debug:
image

方法断点 | 接口跳转实现类

方法断点 = 方法起始行断点 + 方法结尾行断点

	// 方法断点 | 接口跳转实现类
	public static void method() {
		System.out.println("this is from method");
		IService iservice = new IServiceImpl();
		iservice.execute();
	}

在方法上打断点:
image

debug:
第一个断点停留在方法体内第一行代码:
image

第二个断点停留在方法体内返回的最后一行代码:
image


在接口方法上打断点:
image

真正运行的是接口方法的实现类:
image

如果我们有很多的实现类,我们具体不知道是哪一个,我们只需要在接口方法上打一个断点,它就会自动地跳到接口具体的实现类方法上。

异常断点 | 全局捕获

	// 异常断点 | 全局捕获
	public static void exception() {
		Object o = null;
		o.toString();
		System.out.println("this line will never be print!");
	}

异常断点会停顿在报出异常的具体代码行。

  1. 点击View Breakpoints
    image

  2. 在异常断点处添加新的异常断点
    image
    image
    image
    image

  3. 接下来,只要你的程序遇到空指针异常,它就会停顿到发出空指针异常的那一行代码那里。

没有显式打断点:
image

debug:
image

这个异常断点对于我们异常调试很方便。

字段断点 | 读写监控

	// 字段断点 | 读写监控
	public static void field() {
		Person p = new Person("field", 10);
		p.setAge(12);
		System.out.println(p);
	}

在类的字段属性上打断点:
image

我们在字段左边打了一个字段断点(小眼睛),它就会去监控该字段属性的整个生命周期的值的变化。

dubug:
第一个:构造方法修改了属性值
image

第二个:setter方法修改了属性值
image

不暂停的 debug

通常情况下,我们断点的时候,会卡住当前线程。
假设我们在主预发或者公用环境进行debug,就老感觉背后有人在骂我,谁又在debug,环境怎么又不通了!所以当我们想要愉快在公用环境debug的时候要:

查看变量值

不暂停直接观察相关变量值
image

强行改变变量值

image

是谁调用了我

可以直接观察到调用堆栈,类似 Arthas 的 trace
image

你进来了不

如果执行了断点所在位置,会在控制台打出一行日志:
image

远程断点调试 Debug

远程断点调试流程

image

运行原理:

  • 远程服务器开启提供调试的端口号
  • IDE客户端通过此端口号连接上服务器
  • 服务器通知客户端运行到了哪一行

正常情况下:

  1. 用户访问服务器ip地址
  2. 服务器返回结果

远程断点调试Debug情况下:

  1. 用户访问服务器
  2. 服务器得到了具体运行的那一行
  3. 然后就会去问一下本地跟它连接的IDEA,问它有没有这一行的断点
  4. IDEA检查了一下自己的断点列表
  5. 然后开发人员就开始debug调试
  6. 结束调试,将结束调试的信息返回给服务器
  7. 服务器返回运行结果给用户

实例

代码:

@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;
	}
}

点击image

点击image

选择Attach to remote JVM(依附上远程服务器的端口)
image

填写远程服务器的ip地址,指定远程服务器和本地IDE进行socket连接进行断点调试的端口号(随意即可)
image

科普:我们本地debug的时候也是用的socket连接,只不过此时的socket客户端和服务器都是本地而已。
image

可以看到下面生成了命令行参数,在服务器端使用java -Xdebug -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar demo.jar,上面生成的为命令行参数(如果报错,就将 * 号进行转义处理),这样服务器就启动了。
image

运行Debug程序
image
就可以正常Debug了


问题:如果我们把本地的代码修改了之后,服务器返回给用户的信息会改变吗?
回答:不会改变,虽然说我们把本地的代码改了,但是我们本地的修改没有部署到线上去,所以我们程序运行的逻辑还是按照线上来运行的

实例

启动无报错调试

场景需求

今天在项目添加了微信支付后,原本以为绝对OK,启动SpingBoot服务,好家伙,直接起不来,控制台也不输出异常信息。一猜就应该是自己手欠,没办法,只能用最朴实的办法解决了,debug。
image

调试方法

步骤一
在你的项目启动类的这行代码打一个断点,并复制这行代码
image

步骤二
同时按下快捷键 Art+F8,然后在框框中输入,如下所示
image

步骤三
点击按钮 Evaluate,进行分析
image

然后你会看到对应的错误提示,注意,有的不是在第一层,你需要点击cause这里,层层点击进去,看到没,是不是很熟悉的场景,bean注入失败

检查

我这里是引入了redssion客户端导致的,版本不一致的原因。

写在最后
关于spring,一定要好好掌握,尤其是理解容器管理的含义,按照自己能够理解的方式形象记忆,这样你才能快速定位问题。

posted @ 2021-06-20 19:15  Nemo&  阅读(5654)  评论(0编辑  收藏  举报