阿里梅指南。
笔者参与了一次排查,最终结论与枚举类的规范有关,本文在这里总结一程,供大家学习习交流。 今天开发者给我讲了一个很奇怪的问题,说一个对象的 state 属性是一个枚举类,设置好对象的状态后,插入到数据库中,这个状态就消失了,凭空消失了,变成了一个空白的字符串。 这感觉很奇怪,我参与了整个故障排除过程,得到的结论与枚举类的规范有关,我在这里总结了这个过程,供大家一起学习和习。 问题**。
让我们先看看有问题的零件是什么样子的:
该方法非常简单,只需将传递的 payrequest 对象转换为 payrequestdo 对象并将其插入数据库即可。@override
public string insert(payrequest payrequest)
PayRequest 和 PayRequestDo 是普通的 pojo 对象,没什么复杂的,只是 PayRequestDo 的状态被替换为字符串:
public class payrequest
public class payrequestdo
至于 PayConvertor Todo 方法,也很简单,它是一个属性副本:
public payrequestdo todo(payrequest payrequest)
开发中反复强调,带有输入参数的 payrequest 中的状态必须有一个值,并且写成死的,根本不能是空的,然后这些**也查过很多遍,mybatis 的 mapper xml 绝对没有问题,但是插入数据库就没有值了, 为什么?排查问题。
首先,简单地花一些时间去消除一些底层的问题,比如没有发布,或者部署了错误的版本等等,并确保服务器上运行的就是上面发布的,这是非常非常重要的,而且始终是检查问题时首先要做的(其实大部分问题都可以在这一步解决)。
以下是快速确定在线版本的两种方法: 解决方案 1:使用 git-commit-id-plugin m**en 插件启用 Spring Boot 的信息执行器:
一旦 mvn packge 构建完并使用 j**a -jar 启动,您就可以访问 localhost:8080 actuator info 来获取当前的 git 提交信息:配置开放执行器端点,开放端点需要注意数据安全,可以配置不同的管理端口或对敏感内容脱敏
management.endpoints.web.exposure.include=info
使用此提交 ID 可确定它是哪个版本。"id":"1234567"
"branch":"master"
场景 2:如果您没有提前集成 git m**en 插件,或者没有打开信息端点。 您也可以将 fat jar 包拆下来并反编译以确定版本。 当然也有一些**可以dump**的解决方法,比如下面即将推出的arthas有一个jad命令,还有JDK自带的hsdb,也可以直接将内存中的类转储到本地磁盘,有兴趣的可以自己搜索。 --分界线--消除底层问题,然后我们分析问题出在哪里。 因为问题的表现是插入到数据库中的值丢失了,我们可以先看一下db的摘要日志分析
2023-11-14 21:06:04.221|paycenter|00|8||n|trace8423002774916857900o38o50|paycenter|pay_request_record|pay_request_record|insert|insert into pay_request (id, pay_id, pay_no, status) values (null, 'pay2023001', '20231114000000001', '')|0|servicehandler-11.2.60.188:20880-thread-8|0.1
从上面的摘要日志中的 SQL 可以看出,插入 SQL 中的 status 字段已经传递了''空白字符,这意味着在ORM框架中不会出现该问题,并且消除了XML中SQL语句不正确的问题。 因此,在插入数据库之前,业务方法中一定发生了问题。
接下来,我怀疑输入参数传递的payrequest中的status字段是没有价值的,我需要查看运行线程栈中payrequest对象的属性值,这个场景很适合使用arthas的watch方法,我的想法里面安装了一个arthas插件(名字叫王晓歌的arthas创意), 我们只需要右键点击插入并选择Arthas命令,选择watch子菜单,就可以得到watch命令,非常方便。
接下来,我们登录到服务器,切换到 admin 用户(arthas startup 要求你启动与 j**a 相同的用户),并输入我们刚刚复制的命令:
watch com.xxxxx.paycenter.service.repository.impl.payrequestrepositoryimpl insert '' -n 1 -x 3
然后我们等待方法执行到插入,然后我们可以观察 arthas watch 的输出
可以清楚地看到,这里添加参数时,status 属性还是有一个值的:statusinitmethod=com.xxxxx.paycenter.service.repository.impl.payrequestrepositoryimpl$$enhancerbyspringcglib$$4f979dec.insert location=atexit
ts=2023-11-15 14:54:49; [cost=6.001557ms] result=@arraylist[
@object[
@payrequest[
serialversionuid=@long[1],
payid=@string[pay2023001],
payno=@string[20231114000000001],
status=@status[init],
@string[20231114000000001],
null,
接下来,当我们查看映射器的写入数据时,status 属性仍然存在,我们还使用 arthas watch 命令:
watch com.xxxxx.paycenter.infrastructure.dal.mapper.paymapper insert '' -n 1 -x 3
然后等待方法执行到映射器的插入中,并观察 arthas 观看的内容:
正如你所看到的,很明显,当数据库入时,状态已经变成了空!affect(class count: 2 , method count: 1) cost in 289 ms, listenerid: 10
method=com.sun.proxy.$proxy153.insert location=atexit
ts=2023-11-15 14:51:36; [cost=3.933553ms] result=@arraylist[
@object[
@payrequestdo[
id=null,
payid=@string[pay2023002],
payno=@string[20231114000000002],
status=@string,
@integer[1],
null,
插入时有参数,但写入数据库时就消失了,这意味着唯一的问题是中间的对象转换方法payconvertor todo。 使用 arthas 观察 todo 的输入和输出参数:
watch com.xxxxx.paycenter.core.convertor.payconvertor todo '' -n 1 -x 3
输出:
查看输出,问题确实发生在 todo 内部,并且 status 属性在数据转换后消失了。method=com.xxxxx.paycenter.core.convertor.payconvertor.todo location=atexit
ts=2023-11-15 15:01:35; [cost=0.432887ms] result=@arraylist[
@object[
@payrequest[
serialversionuid=@long[1],
payid=@string[pay2023003],
payno=@string[20231114000000003],
status=@status[init],
@payrequestdo[
id=null,
payid=@string[pay2023003],
payno=@string[20231114000000003],
status=@string,
null,
确切地说,以下行 ** 丢失了该属性:
payrequestdo.setstatus(payrequest.getstatus().getcode())
找出原因。
到目前为止,我们已经发现原因可能是 status 属性后面的枚举类 status 在 getcode 时返回空。 状态的**如下:
看着枚举类属性的 setter 方法,我不禁思考:为什么枚举类的属性要提供 setter 方法?public enum status
public string getcode()
public void setcode(string code)
public string getdesc()
public void setdesc(string desc)
一般来说,枚举类的属性必须设置为最终的关键字修饰,不能提供 setter 方法。 试想一下,如果我通过二传手用以下方式交换失败和成功码,这个**能继续快乐地玩吗?
显然,此处提供的 setter 调用直接破坏了枚举类,因此最好的办法是将 final 添加到枚举类属性中。status.failed.setcode("success");
status.success.setcode("failed");
接下来传递手表状态''参数''可以打印对象内部的状态,结果输出进一步证实了我的怀疑,即状态枚举类中枚举的 code 属性已成为空白字符串
根源。watch com.xxxxx.paycenter.core.enums.status getcode '' -n 1 -x 3
method=com.xxxxx.paycenter.core.enums.status.getcode location=atexit
ts=2023-11-15 15:05:36; [cost=0.005638ms] result=@arraylist[
@status[
init=@status[
init=@status[init],
success=@status[success],
failed=@status[failed],
code=@string,
desc=@string,
name=@string[init],
ordinal=@integer[0],
直接原因基本已经找到了,然后我们需要确切知道 **中调用的枚举类的 setcode 方法是什么,为了什么需求,因为我在整个项目中没有找到显示的调用,所以修改枚举的 setcode,添加一些 **,以便在调用 setcode 时打印调用栈:
有朋友反映抛出异常看栈太难看了,这也提供了一个不需要抛出异常的解决方案:public void setcode(string code) catch (exception e) ,after: {", this.code, code, e);
this.code = code;
添加**发布后,很快就从堆栈中打印出来了:public void setcode(string code) ,after: {", this.code, code, formatstacktrace(stacktrace));
this.code = code;
public static string formatstacktrace(stacktraceelement stacktrace)
return stringbuilder.tostring();
这是 podam 引起的问题,第三方库(发起者还是我介绍的库),这个库的作用是传递一个类对象,解析出它的属性,并赋值,简单来说,就是按照类生成随机对象的随机属性,测试工具会用到这个函数, 这个库在解析枚举类时可能没有实现好,导致枚举 setter 方法被反射调用,最终导致问题。改进。
我们排查了数据库中缺少插入属性的问题,最后发现问题的原因是一个非标准枚举类写入引起的问题。 首先是写**或者注意规范,最好在当地安装一些扫描工具,比如声纳,发现风险必须按照建议尽快修复。 二是第三方库 podam 在枚举的实现上还存在问题,这个 bug 需要尽快修复。 最后,我也在联系**扫描团队,看看能不能把这样的案例实现到平台检测能力中,在提交的时候扫描问题,这样就可以把问题扼杀在摇篮里。 好在联系了**扫描的一位同事,我们公司确实有这样的检测,但是没有加入**扫描规则,在我的建议下,将这条规则加入到**扫描中。
欢迎加入【阿里云开发者***》读者群】
这是“阿里云开发者”读者的特殊交流空间。