一次日志记录中使用fastjson涉及到ByteBuffer的教训

背景

目前本人在公司负责的模块中,有一个模块是负责数据同步的,主要是将我们数据产线使用的 AWS Dynamodb 同步的我们的测试QA 的环境的 MongoDB 的库中,去年开始也提供了使用 EMR 批量同步的功能,但是有时候业务也需要少量的数据进行同步测试,所以也通过了抽样数据同步的功能。

过程

本周的时候,LS项目研发的同事,和我反馈一个问题,主要是表现是发现同步到MongoDB库中的部分表的字段有问题,下游没法解析使用,随后我就开始排查代码
看下的这边同步的部分代码

/*** Synchronizes data by rewriting from source to sink database.** @param syncMessage The message model containing synchronization details.* @param source_db   The source DynamoDB client.* @param sink_db     The sink DynamoDB client.* @param parameters  Parameters for querying the source database.* @return The number of successful sync operations.*/private static int syncDataByRewrite(SyncPartDDBMessageModel syncMessage, AmazonDynamoDBClient source_db, AmazonDynamoDBClient sink_db, List<String> parameters) {int syncSuccessCount = 0;String table_name = syncMessage.getTable_name();List<Map<String, AttributeValue>> sync_items = new ArrayList<>();for (String parameter : parameters) {QueryResult queryResult = queryDataFromSource(source_db, syncMessage, parameter);if (queryResult != null && queryResult.getItems() != null && !queryResult.getItems().isEmpty()) {sync_items.addAll(queryResult.getItems());syncSuccessCount++;}}try {log.info("start sync data:" + JSON.toJSONString(sync_items));AWSDynamoDBUtils.batchWrite(sink_db, table_name, sync_items);} catch (Exception e) {log.error("Error syncing data for table {}: {}", table_name, e.getMessage(), e);}return syncSuccessCount;}

其实核心代码就如上面一样,咋一看,这段代码是没有问题的,因为之前我也对齐进行过测试,数据同步也是 OK 的,后来同事和我说 正常的字段都是 OK 的,但是 Binary 类型的字段存在问题,由于公司的之前使用的 Dynamodb的数据库,Dynamodb对每个 Item 的大小有限制,即512K,为了更多存储,减少不必要的 Chunk,我们将很多大的字段进行的 GZIP 压缩后用二进制的Binary 存储。

定位问题

当我听到是 Binary 类型的字段有问题的时候,第一反应是 想,是不是我们的 proxy 有问题,因为我这边使用的了写 Mongodb 适配Dynamodb 的 proxy, 后来问了组件的同事,说代码也已经很久没问题了,应该没问题的~
后来也是考虑是不是batchWrite的底层写得有问题,也是各种猜测去定位问题,浪费了很长时间
后来无意之前,我注释了下代码,仅仅 就注释了一行代码,你们猜是什么?
··
··
··
就是 log.info(“start sync data:” + JSON.toJSONString(sync_items));
对,我注释了一样记录日志的代码,居然程序没问题了这搞的我很尴尬哎呀

寻找问题的根源

最近也比较忙,当时定位到问题后,我想找找为什么会这样~
刚好今天周六有时间,我吧 FastJson 的源码来了来,我调试看下,我使用的版本是: fastjson2 git:(2.0.26)

测试

在实际我们的数据同步过程中,Map<String, AttributeValue> 里面的AttributeValue 是 Dynamodb返回的实体对象,是一个比较复杂的对象,我就不使用了,我这边只是测试了Buffer的

   @Testpublic void testBufferObjectToJsonString1() throws Exception {String drugId = "53b8acd8694f4ddabefc76b3a6d58976";ByteBuffer byteBuffer = compressString(drugId);System.out.println("byteBuffer:" + byteBuffer);System.out.println("drugIdJSONString:" + JSON.toJSONString(byteBuffer));System.out.println("byteBuffer2:" + byteBuffer);System.out.println("drugIdJSONString2:" + JSON.toJSONString(byteBuffer));}@Testpublic void testBufferObjectToJsonString2() throws Exception {String drugId = "53b8acd8694f4ddabefc76b3a6d5897653b8acd8694f4ddabefc76b3a6d5897653b8acd8694f4ddabefc76b3a6d58976";ByteBuffer byteBuffer = compressString(drugId);System.out.println("byteBuffer:" + byteBuffer);System.out.println("drugIdJSONString:" + JSON.toJSONString(byteBuffer));System.out.println("byteBuffer2:" + byteBuffer);System.out.println("drugIdJSONString2:" + JSON.toJSONString(byteBuffer));}

可以给大家看下 这2个测试方法的返回:
第一个测试的返回:

byteBuffer:java.nio.HeapByteBuffer[pos=0 lim=32 cap=32]
drugIdJSONString:{"char":"㔳","direct":false,"double":1.4039733842967137E165,"float":2.1439479E-7,"int":1684103781,"long":7377801321278300470,"readOnly":false,"short":25653}
byteBuffer2:java.nio.HeapByteBuffer[pos=28 lim=32 cap=32]com.alibaba.fastjson.JSONException: toJSONString errorat com.alibaba.fastjson.JSON.toJSONString(JSON.java:1477)at com.patsnap.data.process.platform.job.syncPartDDB.SyncPartDDBJobTest.testBufferObjectToJsonString1(SyncPartDDBJobTest.java:143)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at junit.framework.TestCase.runTest(TestCase.java:176)at junit.framework.TestCase.runBare(TestCase.java:141)at junit.framework.TestResult$1.protect(TestResult.java:122)at junit.framework.TestResult.runProtected(TestResult.java:142)at junit.framework.TestResult.run(TestResult.java:125)at junit.framework.TestCase.run(TestCase.java:129)at junit.framework.TestSuite.runTest(TestSuite.java:252)at junit.framework.TestSuite.run(TestSuite.java:247)at org.junit.internal.runners.JUnit38ClassRunner.run(JUnit38ClassRunner.java:86)at org.junit.runner.JUnitCore.run(JUnitCore.java:137)at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
Caused by: java.nio.BufferUnderflowExceptionat java.nio.Buffer.nextGetIndex(Buffer.java:510)at java.nio.HeapByteBuffer.getDouble(HeapByteBuffer.java:531)at com.alibaba.fastjson2.writer.FieldWriterDoubleValueFunc.write(FieldWriterDoubleValueFunc.java:44)at com.alibaba.fastjson2.writer.ObjectWriter8.write(ObjectWriter8.java:97)at com.alibaba.fastjson.JSON.toJSONString(JSON.java:1469)... 21 more

第二个测试的返回:

byteBuffer:java.nio.HeapByteBuffer[pos=0 lim=96 cap=96]
drugIdJSONString:{"char":"㔳","direct":false,"double":1.4039733842967137E165,"float":2.1439479E-7,"int":1684103781,"long":7377801321278300470,"readOnly":false,"short":25653}
byteBuffer2:java.nio.HeapByteBuffer[pos=28 lim=96 cap=96]
drugIdJSONString2:{"char":"㠹","direct":false,"double":9.958328793464587E-43,"float":1.3592432E22,"int":879113316,"long":7233170664182724406,"readOnly":false,"short":25139}

第一个测试居然报错了,从报错的堆栈上看,java.nio.HeapByteBuffer.getDouble(HeapByteBuffer.java:531)这个异常了
第二个测试没有报错,但是存在问题,就是明显2个对象不一样了,我们从第二个HeapByteBuffer的position的值可以看到,他的值偏移了,说明了什么 ,说明了 FastJosn 读取了ByteBuffer的内容,但是没有做buffer rewind重置位置的操作,这也太坑了

查看源码

static String toJSONString(Object object) {
JSONWriter.Context writeContext = new JSONWriter.Context(JSONFactory.defaultObjectWriterProvider);boolean pretty = (writeContext.features & JSONWriter.Feature.PrettyFormat.mask) != 0;JSONWriter jsonWriter;
if (JVM_VERSION == 8) {jsonWriter = new JSONWriterUTF16JDK8(writeContext);
} else if ((writeContext.features & JSONWriter.Feature.OptimizedForAscii.mask) != 0) {....
} else {....
}try (JSONWriter writer = pretty ?new JSONWriterPretty(jsonWriter) : jsonWriter) {if (object == null) {writer.writeNull();} else {writer.rootObject = object;writer.path = JSONWriter.Path.ROOT;Class<?> valueClass = object.getClass();if (valueClass == JSONObject.class) {writer.write((JSONObject) object);} else {JSONWriter.Context context = writer.context;boolean fieldBased = (context.features & JSONWriter.Feature.FieldBased.mask) != 0;ObjectWriter<?> objectWriter = context.provider.getObjectWriter(valueClass, valueClass, fieldBased);objectWriter.write(writer, object, null, null, 0);}}return writer.toString();
} catch (NullPointerException | NumberFormatException e) {throw new JSONException("JSON#toJSONString cannot serialize '" + object + "'", e);
}
}

上面的toJSONString的部分代码的截取,主要是看 objectWriter.write(writer, object, null, null, 0);这个地方的实现,最终我调试发现调用了com.alibaba.fastjson2.writer.ObjectWriter8 里面的 write方法

 @Overridepublic void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {long featuresAll = features | this.features | jsonWriter.getFeatures();boolean beanToArray = (featuresAll & BeanToArray.mask) != 0;if (jsonWriter.jsonb) {if (beanToArray) {writeArrayMappingJSONB(jsonWriter, object, fieldName, fieldType, features);return;}writeJSONB(jsonWriter, object, fieldName, fieldType, features);return;}if (beanToArray) {writeArrayMapping(jsonWriter, object, fieldName, fieldType, features | this.features);return;}if (!serializable) {if ((featuresAll & JSONWriter.Feature.ErrorOnNoneSerializable.mask) != 0) {errorOnNoneSerializable();return;}if ((featuresAll & JSONWriter.Feature.IgnoreNoneSerializable.mask) != 0) {jsonWriter.writeNull();return;}}if (hasFilter(jsonWriter)) {writeWithFilter(jsonWriter, object, fieldName, fieldType, 0);return;}jsonWriter.startObject();if (((features | this.features) & WriteClassName.mask) != 0 || jsonWriter.isWriteTypeInfo(object, features)) {writeTypeInfo(jsonWriter);}fieldWriter0.write(jsonWriter, object);fieldWriter1.write(jsonWriter, object);fieldWriter2.write(jsonWriter, object);fieldWriter3.write(jsonWriter, object);fieldWriter4.write(jsonWriter, object);fieldWriter5.write(jsonWriter, object);fieldWriter6.write(jsonWriter, object);fieldWriter7.write(jsonWriter, object);jsonWriter.endObject();}

最终代码是执行了fieldWriter0到fieldWriter7的8个方式,这也解释了 我们的为什么我们打印的 Bytebuffer类型是这样的:

{"char":"㔳","direct":false,"double":1.4039733842967137E165,"float":2.1439479E-7,"int":1684103781,"long":7377801321278300470,"readOnly":false,"short":25653
}

image.png
这边也是刚好8个方式加起来读取了28
这边也同能解释了我第一个测试方式为什么会报错,再回头看下我第一个的 buffer 对象byteBuffer:java.nio.HeapByteBuffer[pos=0 lim=32 cap=32]
byteBuffer2:java.nio.HeapByteBuffer[pos=28 lim=32 cap=32]
为什么第二次 toJSONString 的错误就是因为,读取的位置不够了~所以在第一个测试在at java.nio.HeapByteBuffer.getDouble(HeapByteBuffer.java:531)的位置发送了错误,也应对了上面的源码的逻辑

解决方法

如果是的执行方法在使用toJSONString之前的,那是没有问题的,正常我们读取 buffer 完成后的,会重置位置,或者使用一个输出流去接收,然后再去读取,也是没有问题的,或者使用 ByteBuffer wrap = ByteBuffer.wrap(byteBuffer.array()); 也是没有问题的
当然你如果想要解决这个toJSONString的错误,也是可以的,正常我们都会将二进制使用 base64来表示的,这样就不存在问题,
上代码:

public class ByteBufferValueFilter implements ValueFilter {@Overridepublic Object process(Object object, String name, Object value) {if (value instanceof ByteBuffer) {ByteBuffer buffer = (ByteBuffer) value;byte[] bytes = new byte[buffer.remaining()];buffer.get(bytes); // 读取剩余的字节buffer.rewind(); // 重置 position 到起始位置return Base64.getEncoder().encodeToString(bytes);}return value;}
}

是可以解决的

后续

这个问题其实我在官方的issues也找到了这个问题:https://github.com/alibaba/fastjson/issues/2357
image.png

image.png

下面的回复是已经解决了这个问题的,但是我使用的2.0.26 居然还有这个问题,
我就顺便测试了1.2.83版本,这个也是2.0.26版本引用之前1.X的fastjson 的版本,
image.png

 @Test
public void testBufferObjectToJsonString2() throws Exception {String drugId = "53b8acd8694f4ddabefc76b3a6d5897653b8acd8694f4ddabefc76b3a6d5897653b8acd8694f4ddabefc76b3a6d58976";ByteBuffer byteBuffer = compressString(drugId);System.out.println("byteBuffer:" + byteBuffer);System.out.println("drugIdJSONString:" + com.alibaba.fastjson.JSON.toJSONString(byteBuffer));System.out.println("byteBuffer2:" + byteBuffer);System.out.println("drugIdJSONString2:" + JSON.toJSONString(byteBuffer));
}

输出:

byteBuffer:java.nio.HeapByteBuffer[pos=0 lim=96 cap=96]
drugIdJSONString:{"array":"NTNiOGFjZDg2OTRmNGRkYWJlZmM3NmIzYTZkNTg5NzY1M2I4YWNkODY5NGY0ZGRhYmVmYzc2YjNhNmQ1ODk3NjUzYjhhY2Q4Njk0ZjRkZGFiZWZjNzZiM2E2ZDU4OTc2","limit":96,"position":0}
byteBuffer2:java.nio.HeapByteBuffer[pos=0 lim=96 cap=96]
drugIdJSONString2:{"char":"㔳","direct":false,"double":1.4039733842967137E165,"float":2.1439479E-7,"int":1684103781,"long":7377801321278300470,"readOnly":false,"short":25653}

可以看到 在之前的做法也是将二进制去 base64表示的,不知道为什么在2.0.26 还会出现上面的问题
那我就顺便去官方 提一个issues吧,https://github.com/alibaba/fastjson2/issues/2877
后面看下是否会修复这个问题

总结

回顾上面,其实最大的一个问题就是,在记录日志的时候,不应该添加在代码执行逻辑之前,这是一个不好的代码习惯,应该将日志记录放在代码执行逻辑之后。此外,代码的测试用例要做全,排查代码的时候,定位问题的时候 更加敏锐些。

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

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

相关文章

【OpenCV_python】凸包检测 轮廓特征 直方图均衡化 模板匹配 霍夫变换

凸包特征检测 凸包就是图像的最小外接多边形&#xff0c;通过图像的轮廓点&#xff0c;找到距离最远的两个点的直线&#xff0c;根据直线找到距离最远的下一个点&#xff0c;直到所有的点被包围在多边形内 读取图像二值化找图像的轮廓获取凸包点的坐标绘制凸包点 convexHull 获…

程序员如何写PLC程序

PLC是可编程逻辑控制器的简称&#xff0c;常用的编程语言是IEC61131-3&#xff08;梯形图、结构化文本、指令表、功能块、顺序功能图&#xff09;和西门子的SCL。程序员常用的编程语言是JS、Java、Python、C/C、Go等。PLC广泛采用编程工具有codesys、博图等。程序员常用的编程工…

敏捷架构在数字时代的应用:从理论到实践的全面指南

在数字化转型和技术变革的浪潮中&#xff0c;企业面临着不断提升敏捷性和应对复杂环境的挑战。敏捷架构在数字时代的应用不仅从理论层面阐述了敏捷架构的基本原理&#xff0c;还为企业提供了详细的实践指南&#xff0c;帮助企业从理论走向实际操作。本文将从理论与实践的双重视…

STM32——CAN通讯基础知识

CAN 协议简介 CAN 是控制器局域网络 (Controller Area Network) 的简称&#xff0c;它是由研发和生产汽车电子产品著称的德国 BOSCH 公司开发的&#xff0c;并最终成为国际标准(ISO11519以及ISO11898),是国际上应用最广泛的现场总线之一。差异点如下&#xff1a; 高速CAN可以达…

YOLOv8_det/seg/pose/obb推理流程

本章将介绍目标检测、实例分割、关键点检测和旋转目标检测的推理原理,基于onnx模型推理,那么首先就需要了解onnx模型的输入和输出,对输入的图片需要进行预处理的操作,对输出的结果需要进行后处理的操作,这部分内容在我的另一个专栏《YOLOv8深度剖析》中也有介绍,如果对YO…

《机器学习》一元、多元线性回归的实现 No.4

一、一元线性回归实现 先直接看完整代码&#xff1a; import pandas as pd import matplotlib.pyplot as plt from sklearn.linear_model import LinearRegressiondate pd.read_csv(data.csv) #导入数据plt.scatter(date[广告投入],date[销售额]) # 用散点图展示数据 plt.sh…

实训第二十八天(haproxy与利用python实现mysql主从的读写分离)

1、练习 [rootnat ~]# ipvsadm -d -t 192.168.10.101:3306 -r 10.0.0.22:3306 #删除真实主机 nat: [rootnat ~]# ifconfig ens33: flags4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 10.0.0.10 netmask 255.255.255.0 broadcast 10.0.0.25…

【JVM】深入理解类加载机制(二)

深入理解类加载机制 HSDB工具的使用 Hotspot Debugger(HSDB):JDK原生自带 以Windows系统为例&#xff0c;jdk8的环境&#xff0c;在jdk的lib目录下&#xff0c;启动之前&#xff0c;你需要确保你进入的lib目录和你当前的JAVA_HOME配置的JDK是相同的&#xff0c;否则可能会出现…

2.1 文件内容差异对比方法

2.1 文件内容差异对比方法 文件内容差异对比方法2.1.1 两个字符串的差异对比2.1.2 生成美观的HTML格式文档2.1.3 对比nginx 配置文件差异代码封装 文件内容差异对比方法 介绍如何通过difflib模块实现文件内容差异对比。difflib作为Python的标准库模块无需安装&#xff0c;作用…

2024年运营技术与网络安全态势研究报告:遭遇多次网络威胁的比例暴增

随着 OT 组织不断在其业务环境中集成各种数字工具和技术&#xff0c;它们面临的安全挑战也日益变得愈加复杂和多样化。正如 NIST 指出&#xff0c; “虽然安全解决方案旨在解决典型 IT系统中的一些问题&#xff0c;但将这些相同的解决方案引入不同的 OT 环境时&#xff0c;必须…

ruoyi-vue-pro项目新建模块的接口都报404错误

目录 1. 问题所示2. 原理分析3. 解决方法1. 问题所示 新建模块之后,该模块后端的请求都是返回404,如图所示: 2. 原理分析 抛开这个项目,对于路径请求不成功,返回404 主要的步骤如下: 检查路由配置: 确保在路由配置文件中添加了新模块的路由 例如,在Spring Boot中,这…

vue3+ts 前端word文档下载文件时不预览直接下载方法(支持 doc / excel / ppt / pdf 等)

前端word文档下载文件时不预览直接下载方法支持 doc / excel / ppt / pdf 等 根据需要&#xff0c;要实现一个下载文档的需要 最简单的方法就是使用a标签 如果是相同域可以直接下载&#xff0c;但如果是不同域的&#xff0c;就会先打开一个预览页&#xff0c;在预览页再点下载…

StarRocks Lakehouse 快速入门——Apache Paimon

StarRocks Lakehouse 快速入门指南为您提供了湖仓技术概览&#xff0c;旨在帮助您迅速掌握其核心特性、独特优势和应用场景。本指南将指导您如何高效地利用 StarRocks 构建解决方案。文章末尾&#xff0c;我们集合了来自阿里云、饿了么、喜马拉雅和同程旅行等行业领导者在 Star…

Eureka原理与实践:构建高效的微服务架构

Eureka原理与实践&#xff1a;构建高效的微服务架构 Eureka的核心原理Eureka Server&#xff1a;服务注册中心Eureka Client&#xff1a;服务提供者与服务消费者 Eureka的实践应用集成Eureka到Spring Cloud项目中创建Eureka Server创建Eureka Client&#xff08;服务提供者&…

什么叫日志门面

日志门面&#xff0c;是门面模式的一个典型的应用。 门面模式&#xff08;Facade Pattern&#xff09;&#xff0c;也称之为外观模式&#xff0c;其核心为&#xff1a;外部与一个子系统的通信必须通过一个统一的外观对象进行&#xff0c;使得子系统更易于使用。 就像Log4j、Lo…

stm32智能颜色送餐小车(ESP8266WIFI模块、APP制作、物联网模型建立、MQTTFX)

大家好啊&#xff0c;我是情谊&#xff0c;今天我们来介绍一下我最近设计的stm32产品&#xff0c;我们在今年七月份的时候参加了光电设计大赛&#xff0c;我们小队使用的就是stm32的智能送餐小车&#xff0c;虽然止步于省赛&#xff0c;但是还是一次成长的经验吧&#xff0c;那…

Linux系统-通用权限管理

目录 一、文件类型 二、通用权限 1.文件的常规权限 权限类型 壹.对于文件&#xff1a; 贰.对于目录&#xff1a; 查看和修改权限 说明&#xff1a; 举例&#xff1a; 字母表示法 数字表示法 2.文件的访问控制列表&#xff08;FACL File access control list&#…

菱形继承和虚继承

菱形继承&#xff08;Diamond Inheritance&#xff09;是指在多重继承的情况下&#xff0c;某个类继承自两个类&#xff0c;而这两个类又都继承自同一个基类的情况。 在这个结构中&#xff0c;D 直接从 A 继承了 A 的所有特性&#xff0c;但通过 B 和 C 继承&#xff0c;这会导…

eNSP 华为三层交换机配置DHCP

华为三层交换机配置DHCP 华为DHCP原理&#xff1a;&#xff08;思科四个都是广播包&#xff09; 1、客户端广播发送DHCP Discover包。用于发现当前局域网中的DHCP服务器。 2、DHCP服务器单播发送DHCP Offer包给客户端。携带分配给客户端的IP地址。 3、客户端广播发送DHCP Resqe…

索引的数据结构

1.举例引出索引: 1.1.什么是索引&#xff1a; 1.2.数据查找分析&#xff1a; a.数据查找&#xff1a; 1.如上图所示&#xff0c;数据库没有索引的情况下&#xff0c;数据分布在硬盘不同的位置上面&#xff0c;读取数据时&#xff0c;摆臂需要前后摆动查找数据&#xff0c;这…