生产环境使用jdb命令行添加断点调试java应用
关键词:调试 jdb 命令行加断点 命令行调试java应用 生产环境添加断点
原因/背景
生产环境有段旧代码如下:
public String saveMapping(JSONObject jsonObject) {
try {
String mapping = jsonObject.getJSONArray("mapping").toJSONString();
Long id = jsonObject.getLong("id");
repository.saveMapping(mapping, id);
return "保存mapping配置成功";
} catch (Exception e) {
throw new ServiceException("保存mapping配置失败");
}
}
该代码有时正常有时异常,但没有把异常变量打印到日志中,导致排查问题不方便,也就有了在生产环境加断点debug的需求。(最后发现e是org.springframework.dao.DataIntegrityViolationException “Value too long for column”,相信小伙伴已经知道怎么处理了)
目前的arthas(当前最新版本:3.5.5)很强大,但不支持断点调试,所以只有使用java内置的jdb.
原理
-Xrunjdwp:JVM使用(java debug wire protocol)来运行调试环境;
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=xxx
dt_socket:使用的通信方式
server:是主动连接调试器还是作为服务器等待调试器连接
suspend:是否在启动JVM时就暂停,并等待调试器连接
address:地址和端口,地址可以省略,两者用冒号分隔
根据文档和stackoverflow上的讨论,JVM 1.5以后的版本应该使用类似下面的命令(老的还是可以使用的):
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=xxx
步骤
服务端
在服务上执行一个web应用,命令行如下:
java -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar lab-jdb-remote-debug-0.0.1-SNAPSHOT.jar
客户端
注意:客户端和服务端可以是同一台机器。
连接
jdb -connect com.sun.jdi.SocketAttach:hostname=IP,port=5005
或
jdb -attach IP:5005
调试
- 添加方法断点/清除方法断点
用法: stop in ${classFullName}.${methodName}
如:stop in cn.valuetodays.lab.IndexService.saveUser
清除:clear ${classFullName}.${methodName}
- 添加行断点/清除行断点
用法: stop at ${classFullName}:${lineNumber}
如:stop at cn.valuetodays.lab.IndexService:19
清除:clear ${classFullName}:${lineNumber}
- 下一步
next
- 继续执行,若有断点则跳到下一个断点
cont
- 打印栈桢中的局部变量表(用到了jvm知识)
locals
- 打印变量的内容
print xxx
- 列出所有断点
clear (对,没错,就是列出所有断点,尽管命令的名字是clear)
- 退出
exit
回到第一部分的“原因”处
- 确认生产环境代码的tag
- 确认该类的该方法所在的位置及“throw new ServiceException(“保存mapping配置失败”);”的行号
- 使用行断点
- 前端再次请求
- 若能进入到断点,就print e即可看到内容。
实操
代码
create table if not exists USER (
id int not null primary key auto_increment,
name varchar(16)
);
package cn.valuetodays.lab;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
@Service
public class IndexService {
@Autowired
private JdbcTemplate jdbcTemplate;
public String saveUser(String name) {
String sql = "insert into user(name) values(?)";
try {
int update = jdbcTemplate.update(sql, name);
return "suc: " + update;
} catch (Exception e) {
return "fail";
}
}
}
实操步骤
## 以两个##开头的是说明
## 先连接到服务端,我是使用同一台机器操作的
[root@server201 ~]# jdb -connect com.sun.jdi.SocketAttach:hostname=server201,port=5005
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
Initializing jdb ...
## 在方法上加断点
> stop in cn.valuetodays.lab.IndexService.saveUser
Set breakpoint cn.valuetodays.lab.IndexService.saveUser
## 查看所有断点
> clear
Breakpoints set:
breakpoint cn.valuetodays.lab.IndexService.saveUser
## 此时触发请求(curl -XGET http://server201:50001/saveUser?name=a1111111b3333333333333 ),会在断点外停下
>
Breakpoint hit: "thread=http-nio-50001-exec-1", cn.valuetodays.lab.IndexService.saveUser(), line=14 bci=0
## 查看本地变量,有方法参数和本地变量
http-nio-50001-exec-1[1] locals
Method arguments:
name = "a1111111b3333333333333"
Local variables:
## 下一步
http-nio-50001-exec-1[1] next
>
Step completed: "thread=http-nio-50001-exec-1", cn.valuetodays.lab.IndexService.saveUser(), line=16 bci=3
## 再次查看本地变量
http-nio-50001-exec-1[1] locals
Method arguments:
name = "a1111111b3333333333333"
Local variables:
sql = "insert into user(name) values(?)"
## 下一步
http-nio-50001-exec-1[1] next
## 再次查看本地变量,此时就对应代码中的catch处,可以看到变量e
http-nio-50001-exec-1[1] locals
Method arguments:
name = "a1111111b3333333333333"
Local variables:
sql = "insert into user(name) values(?)"
e = instance of org.springframework.dao.DataIntegrityViolationException(id=6545)
## 查看变量e的内容,可以知道是字段太长了
http-nio-50001-exec-1[1] print e
e = "org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [insert into user(name) values(?)]; Value too long for column """NAME"" VARCHAR(16)": "'a1111111b3333333333333' (22)"; SQL statement:
insert into user(name) values(?) [22001-199]; nested exception is org.h2.jdbc.JdbcSQLDataException: Value too long for column """NAME"" VARCHAR(16)": "'a1111111b3333333333333' (22)"; SQL statement:
insert into user(name) values(?) [22001-199]"
## 退出
http-nio-50001-exec-1[1] exit
[root@server201 ~]#
小伙伴们可以操作一下。
参考
- jdb with javase https://blog.csdn.net/xixingzhe2/article/details/109384076
- 官方文档:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/jdb.html
其它
使用jdb命令连接到远程应用后输入help可以查看所有命令。
在docker中,我尝试了多种connectors,发现只有com.sun.jdi.SocketAttach能正常使用,理论上说在同一台机器(且同一个linux用户)中,提供进程id就应该能连接了,但是不行,我使用的docker镜像是openjdk:8-jdk-slim。但在linux系统中就能正常。
# docker中并未成功,linux系统中正常
jdb -connect com.sun.jdi.ProcessAttach:pid=18405,timeout=10
# docker中并未成功,,linux系统中正常
jdb -connect sun.jvm.hotspot.jdi.SAPIDAttachingConnector:pid=18405
最后编辑:张三 更新时间:2022-06-29 15:20