枚举类的final修饰

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

问题代码

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

@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-12-04 21:06:04.221|PayCenter|00|8||N|trace8423002774916857900o38o50|payCente

通过上面的 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 把 FAILED 和 SUCCESS 的 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 3method=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) {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。

如果提交代码的时候就扫描问题,这样可以把问题扼杀在摇篮里面。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/213168.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据分析基础之《matplotlib(5)—直方图》

一、直方图介绍 1、什么是直方图 直方图,形状类似柱状图却有着与柱状图完全不同的含义。直方图牵涉统计学的概念,首先要对数据进行分组,然后统计每个分组内数据元的数量。在坐标系中,横轴标出每个组的端点,纵轴表示频…

持续集成交付CICD:使用Maven命令下载Nexus制品

目录 一、实验 1.Maven安装 2.Nexus搭建公共组仓库及Maven全局配置文件 3.使用Maven命令下载Nexus制品 一、实验 1.Maven安装 (1)CentOS环境安装步骤 tar -xf apache-maven-3.8.6-bin.tar.gz #解压 mv apache-maven-3.8.6 /usr/local/maven #移动…

文献计量学方法与应用、主题确定、检索与数据采集、VOSviewer可视化绘图、Citespace可视化绘图、R语言文献计量学绘图分析

目录 一、文献计量学方法与应用简介 二、主题确定、检索与数据采集 三、VOSviewer可视化绘图 四、Citespace可视化绘图 五、R语言文献计量学绘图分析 六、论文写作 七、论文投稿 更多应用 文献计量学是指用数学和统计学的方法,定量地分析一切知识载体的交叉…

Linux--学习记录(1)

CtrlAltT:打开终端 各个文件的含义&#xff1a; cmd [-options] [parameter]:中括号为可选的&#xff0c;<>内必须填写 如何设置PATH&#xff1a; 1、临时设置&#xff0c;在命令行中输入&#xff1a;export PATH$PATH:/home/wangxianyue/桌面&#xff0c;其中:/home/wa…

Navicat 技术指引 | 适用于 GaussDB 分布式的模型功能

Navicat Premium&#xff08;16.3.3 Windows 版或以上&#xff09;正式支持 GaussDB 分布式数据库。GaussDB 分布式模式更适合对系统可用性和数据处理能力要求较高的场景。Navicat 工具不仅提供可视化数据查看和编辑功能&#xff0c;还提供强大的高阶功能&#xff08;如模型、结…

使用Microsoft Dynamics AX 2012 - 5. 生产控制

生产控制的主要职责是生产成品。为了完成这项任务&#xff0c;制造业需要消耗物品和资源能力&#xff08;人员和机械&#xff09;。制造过程可能包括半成品的生产和库存。半成品是指物品包括在成品材料清单中。 制造业的业务流程 根据公司的要求&#xff0c;您可以选择申请Dy…

聚观早报 |华为畅享 70正式开售;梦饷科技双12玩法

【聚观365】12月8日消息 华为畅享 70正式开售 梦饷科技双12玩法 华为Mate X5应对火海挑战 谷歌发布AI模型Gemini 字节跳动开启新一轮回购 华为畅享 70正式开售 精致外观与创新科技兼具的华为畅享 70正式开售&#xff0c;1199元起搭载6000mAh超大电池&#xff0c;带来超强…

C/C++端口复用SO_REUSEADDR(setsockopt参数),test ok

端口复用最常用的用途应该是防止服务器重启时之前绑定的端口还未释放或者程序突然退出而系统没有释放端口。这种情况下如果设定了端口复用&#xff0c;则新启动的服务器进程可以直接绑定端口。如果没有设定端口复用&#xff0c;绑定会失败&#xff0c;提示ADDR已经在使用中——…

1-Maven基础

文章目录 Maven基础Maven相关概念构建依赖 Maven用途Maven的工作机制 Maven使用-1-Maven软件的解压与配置步骤1&#xff1a;下载步骤2&#xff1a;解压Maven核心程序步骤3&#xff1a;指定本地仓库步骤4&#xff1a;配置阿里云提供的镜像仓库步骤5&#xff1a;配置 Maven工程的…

【小白专用】MySQL入门(详细总结)

3. 创建数据库 使用 create database 数据库名; 创建数据库。 create database MyDB_one; create database DBAliTest; 创建数据库成功后&#xff0c;数据库的数量变成了6个&#xff0c;多了刚才创建的 dbalitest 。 4. 创建数据库时设置字符编码 使用 create database 数据…

sfp8472学习CDR

1,cdr名称解释 因为光信号传输至一定距离的时候,通常是长距离传输,其波形会出现一定程度的失真,接收端接收到的信号是一个个长短不一的脉冲信号,这个时候在接收端,我们就无法得到我们需要的数据。所以,这个时候就需要有信号的再生,信号的再生功能为再放大、再整形和再…

【Python】 Python web开发库大全

库排序是按照使用人数和文档的活跃度为参考进行的&#xff0c;建议大家使用排名靠前的框架&#xff0c;因为它们的文档更齐全&#xff0c;技术积累要更多&#xff0c;社区更繁盛&#xff0c;能得到更好的支持&#xff0c;这样在遇到自己无法解决的问题&#xff0c;可以更快更高…

京东运营数据分析:10月京东奶粉行业销售数据分析

近年来&#xff0c;随着出生人口红利逐渐消逝&#xff0c;婴幼儿奶粉竞争进入红海时代&#xff0c;产品逐渐过剩。在这种情况下&#xff0c;我国奶粉市场进入调整阶段&#xff0c;企业开始将目光投向奶粉的品类细分领域&#xff0c;如有机奶粉、羊奶粉、特殊配方奶粉、成人奶粉…

elementUI中的 “this.$confirm“ 基本用法,“this.$confirm“ 调换 “确认“、“取消“ 按钮的位置

文章目录 前言具体操作总结 前言 elementUI中的 "this.$confirm" 基本用法&#xff0c;"this.$confirm" 调换 "确认"、"取消" 按钮的位置 具体操作 基本用法 <script> this.$confirm(这是数据&#xff08;res.data&#xff0…

kafka学习笔记--安装部署、简单操作

本文内容来自尚硅谷B站公开教学视频&#xff0c;仅做个人总结、学习、复习使用&#xff0c;任何对此文章的引用&#xff0c;应当说明源出处为尚硅谷&#xff0c;不得用于商业用途。 如有侵权、联系速删 视频教程链接&#xff1a;【尚硅谷】Kafka3.x教程&#xff08;从入门到调优…

现代皮质沙发模型材质编辑

在线工具推荐&#xff1a; 3D数字孪生场景编辑器 - GLTF/GLB材质纹理编辑器 - 3D模型在线转换 - Three.js AI自动纹理开发包 - YOLO 虚幻合成数据生成器 - 三维模型预览图生成器 - 3D模型语义搜索引擎 当谈到游戏角色的3D模型风格时&#xff0c;有几种不同的风格&#xf…

vite配置nework访问ip

如果没有进行配置&#xff0c;运行项目之后&#xff0c;看到的访问地址是本地访问地址&#xff0c;其他人同个局域网的人访问不了。 如下&#xff1a; 如果想要其他人也可以访问&#xff0c;需要设置内网 ip 访问地址&#xff0c;设置方法如下&#xff1a; 一、配置 “ vite…

【UE5 c++】c++创建AI对象SpawnAIFromClass

根据蓝图节点&#xff0c;可以发现此方法在AIBlueprintHelperLibrary中。 在.h文件中声明被创建的Class和需要使用的AITree void SpawnMyAI();UPROPERTY(EditAnywhere,BlueprintReadWrite,Category"Class")TSubclassOf<APawn> myclass;UPROPERTY(EditAnywhere…

Qt内存管理、UI编辑器、客制化组件、弹出对话框、常用部件类

头文件的小技巧 #include <QtWidgets> // 在自动生成的 .h 里面加上此句 适用条件&#xff1a; QT 的内存管理 当父窗体被关闭时&#xff0c;子部件的内存会自动释放。 对象树是一种管理对象生命周期的机制。当一个对象被添加到另一个对象的子对象列表中时&#xff0…

Python网络爬虫的基础理解-对应的自我理解误区

##通过一个中国大学大学排名爬虫的示例进行基础性理解 以软科中国最好大学排名为分析对象&#xff0c;基于requests库和bs4库编写爬虫程序&#xff0c;对2015年至2019年间的中国大学排名数据进行爬取&#xff1a;&#xff08;1&#xff09;按照排名先后顺序输出不同年份的前10…