百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

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

toyiye 2024-06-21 11:58 12 浏览 0 评论

点击链接阅读原文,获取更多技术内容:不规范的枚举类代码引发的一场事故-阿里云开发者社区

作者参与了一个问题排查,最后得到的结论和枚举类的规范有关系,本文将过程总结在这里提供大家一起学习交流。

作者 | 萧易

来源 | 阿里云开发者公众号


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


问题代码

让我们先看看出问题的部分代码是什么样子的:

    @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=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 属性还是有值的: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: 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,

]

可以看到,很明显,到插入数据库时候,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=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,

]

看输出结果,问题确实发生在 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=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],

        ],


内容剩余60%,完整内容可点击下方链接查看:不规范的枚举类代码引发的一场事故-阿里云开发者社区

阿里云开发者社区,千万开发者的选择。百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,尽在:阿里云开发者社区-云计算社区-阿里云

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码