Redian新闻
>
不规范的枚举类代码引发的一场事故

不规范的枚举类代码引发的一场事故

科技

阿里妹导读


作者参与了一个问题排查,最后得到的结论和枚举类的规范有关系,本文将过程总结在这里提供大家一起学习交流。
今天开发跟我反馈了一个很奇怪的问题,说有个对象的状态属性是枚举类,设置了该对象的状态后,插入数据库,这个状态没了,凭空消失了,变成了空白字符串。这让人感觉非常奇怪,我参与了整个问题排查过程,得到的结论和枚举类的规范有关系,我将过程总结在这里提供大家一起学习交流。

问题代码

让我们先看看出问题的部分代码是什么样子的:
    @Override    public String insert(PayRequest payRequest) {        // 省略部分无关代码        PayRequestDO payRequestDO = convertor.toDO(payRequest);        payMapper.insert(payRequestDO);        return payRequest.getPayNo();    }

这个方法很简单,就是把传过来的 PayRequest 对象转成 PayRequestDO 对象,然后插入数据库。

PayRequest 和 PayRequestDO 都是普通的 pojo 对象,没什么复杂的,只是 PayRequestDO 的 status 换成了 String:
public class PayRequest {    private String payId;    private String payNo;    private Status status;    // 省略其它属性和 getter setter}
public class PayRequestDO {    private String payId;    private String payNo;    private String status;    // 省略其它属性和 getter setter}

至于 PayConvertor#toDO 方法,也很简单,就是属性拷贝:

public PayRequestDO toDO(PayRequest payRequest) {    PayRequestDO payRequestDO = new PayRequestDO();    payRequestDO.setPayId(payRequest.getPayId());    payRequestDO.setPayNo(payRequest.getPayNo());    payRequestDO.setStatus(payRequest.getStatus().getCode());        // 省略其它代码
return payRequestDO;}
开发一再强调,入参的 PayRequest 里面的 status 一定是有值的,而且是写死的,根本不可能是空,然后这些代码也多次检查过了,mybatis 的 mapper xml 写的也绝对没有问题,但是插入数据库就是没值,WHY ?

排查问题

先简单花些时间,排除掉一些写了代码没发布、或是部署错了版本等等类似的低级问题,确保服务器上面跑的代码就是上面贴出来的代码,这一点非常非常重要,永远是查问题时第一件要做的事情(其实大部分的问题在这一步就可以得到解决)。

这里提供两种快速确定线上的代码版本的方案:
方案一:使用 git-commit-id-plugin maven 插件
开启 spring boot 的 info actuator:
# 配置开放的 Actuator 端点,开放 endpoint 需要注意数据安全,可以配置不同的 management port 或脱敏敏感内容management.endpoints.web.exposure.include=info

mvn packge 构建并以 java -jar 启动后,接下来就可以访问 localhost:8080/actuator/info 来获得当前的 git 提交信息:

{  "git":{    "commit":{      "time":{        "epochSecond":17011234567,        "nano":0      },      "id":"1234567"    },    "branch":"master"  }}

通过这个 commit id 就可以找到代码具体是哪个版本。

方案二:
如果提前没有集成过 git maven 插件,或者没有打开 info endpoint。你还可以把 fat jar 包 down 下来,通过反编译来确定代码版本。当然也有一些在线就可以 dump 代码的方案,例如下面即将出场的 Arthas 有个 jad 命令,还有 JDK 自带的 HSDB,也可以直接 dump 内存中的 class 到本地磁盘,感兴趣可以自行搜索。
------------------------------ 分割线 ------------------------------
排除掉了低级问题,接下来我们分析问题出在哪里。因为问题的表现就是插到数据库里面值丢了,我们可以先看下 db 的 digest 日志分析下:
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

通过上面的 digest 日志的 sql 可以看出来,insert sql 里面的 status 字段,传的就已经是 '' 空白字符了,这说明问题不是发生在 orm 框架里面,这里排除掉了 xml 中 sql 的语句写的不对的问题。所以问题一定发生在插入数据库之前的业务方法中。

接下来,我怀疑入参传过来的 PayRequest 里面 status 字段是没值的,我需要看下正在运行中的线程栈中 PayRequest 对象的属性值,这个场景非常适合使用 Arthas 的 watch 方法,我的 idea 里面装了一个 Arthas 的插件(名字叫 Arthas Idea by 汪小哥),我们只需要在 insert 上面右键选择 Arthas Command,选择 Watch 子菜单,就可以拿到 watch 命令了,非常方便。
接下来我们登录到服务器上,切换到 admin 用户(Arthas 启动要求你用和 java 启动同一个用户),输入刚刚拷贝下来的命令:
watch com.xxxxx.paycenter.service.repository.impl.PayRequestRepositoryImpl insert '{params,returnObj,throwExp}'  -n 1  -x 3

接着我们等待方法执行到 insert,就可以观察 Arthas watch 输出的内容:

method=com.xxxxx.paycenter.service.repository.impl.PayRequestRepositoryImpl$$EnhancerBySpringCGLIB$$4f979dec.insert location=AtExitts=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 属性还是有值的:Status.INIT

接下来,我们去看 mapper 的写数据的时候,status 属性还在不在,同样用 Arthas watch 命令:
watch com.xxxxx.paycenter.infrastructure.dal.mapper.PayMapper insert '{params,returnObj,throwExp}'  -n 1  -x 3

接着等待方法执行到 mapper 的 insert,观察 Arthas watch 到的内容:

Affect(class count: 2 , method count: 1) cost in 289 ms, listenerId: 10method=com.sun.proxy.$Proxy153.insert location=AtExitts=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,]

可以看到,很明显,到插入数据库时候,status 已经变成空了!!!

入参的时候有,写入数据库的时候没了,那说明唯一的问题,就在中间的对象转换方法 PayConvertor#toDO 了。
用 Arthas watch 一下 toDO 的入参和出参:
watch com.xxxxx.paycenter.core.convertor.PayConvertor toDO '{params,returnObj,throwExp}'  -n 1  -x 3 

输出:

method=com.xxxxx.paycenter.core.convertor.PayConvertor.toDO location=AtExitts=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,]

看输出结果,问题确实发生在 toDO 的内部,数据转换后 status 的属性没了

确切来说,下面这行代码,丢了属性:
payRequestDO.setStatus(payRequest.getStatus().getCode());

找到原因

至此我们发现了原因,大概是 status 属性背后的枚举类 Status,在 getCode 的时候返回了空。
Status 的代码如下:
public enum Status {
INIT("INIT", "初始态"), SUCCESS("SUCCESS", "成功"),
FAILED("FAILED", "失败"), ;
private String code;
private String desc;
Status(String code, String desc) { this.code = code; this.desc = desc; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getDesc() { return desc; }
public void setDesc(String desc) { this.desc = desc; }}

看着枚举类属性的 setter 方法,我不由得陷入了沉思:为什么一个枚举类的属性,要提供 setter 方法?

通常来说枚举类的属性,一定要设置为 final 关键字修饰,不能提供 setter 方法。试想下如果我按照下面的方式通过 setter 把 FAILEDSUCCESS 的 code 换过来,那这代码还能不能继续愉快的玩耍下去了?
Status.FAILED.setCode("SUCCESS");
Status.SUCCESS.setCode("FAILED");

很显然这里提供的 setter 调用直接破坏了枚举类,所以,最好的办法就是为枚举类属性加上 final。

接下来通过 watch Status 的 '{target}' 参数,'{target}' 可以打印对象内部的状态,结果输出也进一步验证了我的猜想,Status 枚举类部分枚举的 code 属性已经成了空白字符串了:
watch com.xxxxx.paycenter.core.enums.Status getCode '{target}'  -n 1  -x 3
method=com.xxxxx.paycenter.core.enums.Status.getCode location=AtExitts=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) {    try {        throw new RuntimeException();    } catch (Exception e) {      log.error("code before: {}, after: {}", this.code, code, e);    }    this.code = code;}

有的小伙伴反馈抛异常来看堆栈太丑了,这也提供一个不用抛异常的方案:

public void setCode(String code) {    StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();    log.error("code before: {}, after: {}", this.code, code, formatStackTrace(stackTrace));    this.code = code;}
public static String formatStackTrace(StackTraceElement[] stackTrace) { StringBuilder stringBuilder = new StringBuilder(); for (StackTraceElement element : stackTrace) { stringBuilder.append(element.getClassName()) .append(".") .append(element.getMethodName()) .append("(") .append(element.getFileName()) .append(":") .append(element.getLineNumber()) .append(")") .append(System.lineSeparator()); } return stringBuilder.toString();}

增加了代码后发布上去,很快打印出来了堆栈:

这是 podam 这个第三方的库引起的问题(始作俑者还是我引入的这个库),这个库的作用是可以通过传一个 class 对象,解析出来它的属性,进行赋值,简单来说就是根据 class 生成随机对象随机属性,测试的工具会用到这个功能,这个库在解析枚举类的时候可能没实现好,导致了通过反射调用了枚举的 setter 方法,最终导致了问题。

改进措施

我们从一个数据库插入属性丢失的问题排查,最终发现问题的原因是枚举类写的不规范导致的问题。
首要的是写代码还是要注意规范,最好本地装一些扫描工具,例如 sonar,发现风险一定要尽快按照建议修复。
其次是 podam 这个第三方的库对枚举的实现方式还是有问题的,需要尽快修复掉这个 bug。
最后我也在联系代码扫描的团队,看看能不能把这样的 case 落地成平台检测能力,提交代码的时候就扫描问题,这样可以把问题扼杀在摇篮里面。
可喜的是,我联系了代码扫描的同事,我司确实有这样一个检测,只是没有加到代码扫描的规则里面,在我的建议下,这个规则加到了代码扫描。

欢迎加入【阿里云开发者公众号】读者群


这是一个专门面向“阿里云开发者”公众号的读者交流空间
💡 在这里你可以探讨技术和实践,我们也会定期发布群福利和活动~
欢迎添加微信:argentinaliu 入群

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
ICLR 2023 | 神经规范场:渲染引导空间规范变换《移居蒙城20周年》误诊?这个病例值得所有医生警惕!女子停经2个月就医治疗月经不规则,医生开禁服药后十余天发现怀孕丨医眼看法如何画出规范的 UML 用例图硬核观察 #1193 Rust 有了一个制定规范的团队,但不负责定义该语言今日聚焦:让警察出示证件,实习律师就被铐走?!失范的权力,是如何走向疯狂的?飓风引发的入侵物种?传播危害南卡罗来纳州水域生态与经济策略 + 枚举 优雅的消灭 if-else上海发布控烟标识系统和设置规范,进一步规范设置禁烟标识、室外吸烟点标识及引导标识第四章 牛刀初试华为云低代码引擎 TinyEngine 服务端正式开源TscanPlus v1.2 发布 | 新增域名枚举等功能【健康】一场由缺觉引发的炎症风暴,5种慢病趁虚而入四川一起因早恋引发的惨案,给家长一个沉重警示!AI也造代码屎山!研究发现GitHub Copilot代码可维护性差,偏爱“无脑重写”而非重构复用已有代码出行:羽田机场事故后,至少5家航司推出了免费退改政策,适用于中美航班研究新发现,月经不规律可能有这两种可怕风险!能挣钱的,开源 SpringBoot 和 Vue 的企业级项目,代码很规范!6024 血壮山河之随枣会战 第三章 3【注意】关于在法中国公民加强安全防范的领事提醒日本偶像鼻祖“杰尼斯”解体:一场性侵案引发的娱乐海啸“给男友买了镂空不规则背心,这该怎么穿??”看到实物人傻了呀...好心妹子给陌生男捐肾,没想到,却见证了一场爱引发的奇迹一方手帕引发的悲剧探寻西西里与马耳他的历史脚印(17)gorm中如何使用枚举值一场「南方小土豆」引发的娇妻文学狂欢另一个角度!朱令案:一场豪门恩怨引发的血案?!也许猪周期本身就不规律国家药监局关于发布医疗器械经营质量管理规范的公告重大线上事故!三元表达式引发的空指针问题…酒吧,餐厅老板注意了!BC省将要进行执法突击,查酒牌+执照许可证,以防不规范营业“一碗豆浆引发的婆媳矛盾”上热搜:我把你当妈妈,你把我当敌人?一个 List.of 引发的“血案”探寻西西里与马耳他的历史脚印(18)
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。