深入探究Protostuff枚举类型的序列化

背景:

        有一天突然被一个群组@排查线上问题,说是一个场景划线价和商品原价一模一样。看到问题时,我的内心毫无波澜,因为经常处理线上类似的问题,但了解业务后发现是上个版本经我手对接的新客弹窗商品算价,内心有一丝小慌,但表面看还是稳的一匹。

排查:

        初步排查了用户和商品的基本信息,发现没有问题。然后根据上游的异常trace检查日志,发现server端接收的场景RECALL_VENUE,不是之前约定的 NEW_USER_POP_UP,而RECALL_VENUE 场景会少算一个虚拟优惠,才导致优惠价和原价一致。

接口入参的大致结构如下:

@Data
public class Demo implements Serializable {private static final long serialVersionUID = 90410024120541517423L;@Tag(1)private Long userId;@Tag(2)private CalcSceneEnum scene;。。。
}

        反馈给上游说场景传递错了,上游立马甩过来一个日志截图,显示的是NEW_USER_POP_UP。同一个请求在client端和server端入参日志竟然不一样,这就有点超出认知了。不过如果是这么明显的问题,在联调和测试阶段肯定会发现的,那么没有暴露出来,大概率是测试环境没有问题。然后还有一个点比较奇怪,算价场景有几十多个,就算映射错为什么挑中了 RECALL_VENUE。然后又看了代码中的枚举,发现这2个场景刚好是紧挨着的,NEW_USER_POP_UP在前,RECALL_VENUE在后,而且代码提交的日期只查了1天,那么代码就是同一个版本上线的。

        然后就有了一个大胆的猜想,会不会 Protostuff 序列化是根据角标顺序映射的呢,如果是的话,那么上游的jar包肯定有问题。

        果然,询问发现上游的jar包使用的是测试环境的SNAPSHOT包,而SNAPSHOT包中是RECALL_VENUE在前,NEW_USER_POP_UP在后。

解决:

        然后根据猜测在测试环境server端使用RELEASE包,client端使用SNAPSHOT包,复现了线上的问题。然后让上游升级了RELEASE包之后,server端入参日志打印就恢复正常了,新客弹窗的算价也正常了。

根因:

        问题解决了之后,又琢磨了一下源码,发现 Enum类型的对象会隐式继承 java.lang.Enum,公司使用的rpc序列化协议是 Protostuff,在序列化和反序列化过程中使用的是 java.lang.Enum#ordinal 映射(类似数组的角标)。如果client端的jar包和服务端的中的枚举顺序不一致,那么ordinal值就也不一样了,就会出现入参不一致的问题。

 
public abstract class EnumIO<E extends Enum<E>> implementsPolymorphicSchema.Factory{.........public void writeTo(Output output, int number, boolean repeated,Enum<?> e) throws IOException{if (0 == (IdStrategy.ENUMS_BY_NAME & strategy.flags))// e是EnumTest.DemoReq#myEnumoutput.writeEnum(number, getTag(e), repeated);elseoutput.writeString(number, getAlias(e), repeated);}...public int getTag(Enum<?> element){return tag[element.ordinal()];}
}

可以根据如下demo验证:

import com.alibaba.fastjson.JSON;
import io.protostuff.Tag;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.apache.dubbo.common.serialize.protostuff.ProtostuffSerialization;import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Arrays;public class EnumTest {public enum MyEnum {ONE,TWO,THREE,FOUR,FIVE}//    public enum MyEnum {
//        ONE,
//        TWO,
//        FIVE, //调整位置
//        FOUR,
//        THREE //调整位置
//    }@AllArgsConstructor@Getter@Setterstatic class DemoReq implements Serializable {private static final long serialVersionUID = 5085649228215276199L;@Tag(3)MyEnum myEnum;}/*** 1、先执行main方法,得到原始序列化的值 dataArrays* 2、注释掉第一个 MyEnum ,放开第二个MyEnum* 3、把第一步生成的dataArrays 赋值给 changeArrays,重新执行main,打印的changeDemoReq的值就会变为 FIVE */public static void main(String[] args) throws IOException, ClassNotFoundException {DemoReq demoReq = new DemoReq(MyEnum.THREE);byte[] dataArrays = getBytes(demoReq);System.out.println("原始序列化:" + Arrays.toString(dataArrays));//  --------------------------------------------- 分割线  ---------------------------------------------
//        byte[] changeArrays = new byte[]{
//                0, 0, 0, 62, // 类绝对路径编码后的长度 62
//                0, 0, 0, 2, // 入参属性编码后的长度 2
//                // 类绝对路径编码,总共62个元素
//                99, 111, 109, 46, 115, 104, 105, 122, 104, 117, 97, 110, 103, 46, 100, 117, 97, 112, 112, 46, 100, 105, 115, 99, 111, 117, 110, 116, 46, 105, 110, 116, 101, 114, 102, 97, 99, 101, 115, 46, 118, 97, 108, 105, 100, 46, 69, 110, 117, 109, 84, 101, 115, 116, 36, 68, 101, 109, 111, 82, 101, 113,
//                // 2个元素,对应的是myEnum属性
//                // 24对应的是 @tag(3),
//                // 4对应的是 MyEnum.ONE.ordinal=1值,
//                24, 4
//        };
//        Object changeModel = changeModel(changeArrays);
//        System.out.println("changeDemoReq:"+JSON.toJSONString(changeModel));}private static byte[] getBytes(DemoReq demoReq) throws IOException {ProtostuffSerialization serialization = new ProtostuffSerialization();ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();// 位置1serialization.serialize(null, byteArrayOutputStream).writeObject(demoReq);byte[] serializedData = byteArrayOutputStream.toByteArray();return serializedData;}public static Object changeModel(byte[] changeArrays) throws IOException, ClassNotFoundException {ProtostuffSerialization serialization = new ProtostuffSerialization();ByteArrayInputStream changeStream = new ByteArrayInputStream(changeArrays);// 位置2Object changeDemoReq = serialization.deserialize(null, changeStream).readObject();return changeDemoReq;}
}

核心代码:

        在示例代码中的位置1,会序列化入参,底层会调用到 EnumIO.writeTo 方法,然后会把入参的属性存储到outPut的缓冲数组(tail)中。

public abstract class EnumIO<E extends Enum<E>> implementsPolymorphicSchema.Factory{.........public void writeTo(Output output, int number, boolean repeated,Enum<?> e) throws IOException{if (0 == (IdStrategy.ENUMS_BY_NAME & strategy.flags))// number是tag值// e是EnumTest.DemoReq#myEnumoutput.writeEnum(number, getTag(e), repeated);elseoutput.writeString(number, getAlias(e), repeated);}...public int getTag(Enum<?> element){return tag[element.ordinal()];// 获取父类的ordinal值}
}
--------------------------分割线---------------------------
public final class ProtostuffOutput extends WriteSession implements Output{// fieldNumber tag值// value 枚举的ordinal@Overridepublic void writeInt32(int fieldNumber, int value, boolean repeated) throws IOException{if (value < 0){tail = sink.writeVarInt64(value,this,sink.writeVarInt32(makeTag(fieldNumber, WIRETYPE_VARINT),this,tail));}else{// 内层先写 tag(3)// 外层再写 ordinaltail = sink.writeVarInt32(value,this,sink.writeVarInt32(makeTag(fieldNumber, WIRETYPE_VARINT),this,tail));}}
}

  在示例代码中的位置2,会反序列化changeArrays,把value写入提前构建好的result 对象。

public class ProtostuffObjectInput implements ObjectInput {.........@Overridepublic Object readObject() throws IOException, ClassNotFoundException {int classNameLength = dis.readInt();int bytesLength = dis.readInt();if (classNameLength < 0 || bytesLength < 0) {throw new IOException();}byte[] classNameBytes = new byte[classNameLength];// dis是读取数组的输入流// 填充类名数组dis.readFully(classNameBytes, 0, classNameLength);byte[] bytes = new byte[bytesLength];// 填充属性数组dis.readFully(bytes, 0, bytesLength);String className = new String(classNameBytes);Class clazz = Class.forName(className);Object result;if (WrapperUtils.needWrapper(clazz)) {Schema<Wrapper> schema = RuntimeSchema.getSchema(Wrapper.class);Wrapper wrapper = schema.newMessage();GraphIOUtil.mergeFrom(bytes, wrapper, schema);result = wrapper.getData();} else {Schema schema = RuntimeSchema.getSchema(clazz);result = schema.newMessage();// schema有类相关信息,可以通过tag映射具体的属性// 将属性数组值填充给result对象GraphIOUtil.mergeFrom(bytes, result, schema);}return result;}...
}
--------------------------分割线---------------------------...
public static final RuntimeFieldFactory<Integer> ENUM = new RuntimeFieldFactory<Integer>(ID_ENUM)
{@Overridepublic <T> Field<T> create(int number, java.lang.String name,final java.lang.reflect.Field f, final IdStrategy strategy){final EnumIO<? extends Enum<?>> eio = strategy.getEnumIO(f.getType());final long offset = us.objectFieldOffset(f);return new Field<T>(FieldType.ENUM, number, name,f.getAnnotation(Tag.class)){@Overridepublic void mergeFrom(Input input, T message)throws IOException{// message是 model对象// offset 是@tag(3)// input是 对象的属性值 [24,2]// eio.valueByTagMap维护 ordinal&枚举 的关系// eio.readFrom(input) 返回的是具体的枚举 FIVEus.putObject(message, offset, eio.readFrom(input));}}}
}    

扩展:

dubbo支持其他序列化协议,下面也做了测评,感兴趣的也可以通过上面的示例代码玩一把 ,更改示例代码中的序列化协议即可(Fst和Kryo需要添加额外的包,pom见附录)

协议

映射方式

Protostuff

枚举ordinal

FastJson

枚举name

Gson

枚举name

Hessian2

枚举name

Fst

枚举ordinal

Kryo

枚举name

附录:

<dependency><groupId>com.esotericsoftware</groupId><artifactId>kryo</artifactId><version>3.0.0</version>
</dependency>
<dependency><groupId>de.javakaffee</groupId><artifactId>kryo-serializers</artifactId><version>0.45</version>
</dependency><dependency><groupId>de.ruedigermoeller</groupId><artifactId>fst</artifactId><version>2.57</version>
</dependency>

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

案例237:基于微信小程序的医院挂号预约系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

如何学习TS?

文章目录 一. 8种内置基础类型.ts二. void、never、any、unknown类型void类型never类型any类型unknown类型总结&#xff1a;void和any在项目中是比较常见的&#xff0c;never和unknown不常用。 三. 数组和函数类型定义.ts 一. 8种内置基础类型.ts /* eslint-disable typescrip…

vivado 路径分段

路径分段 与其他XDC约束不同&#xff0c;set_max_delay命令和set_min_delay在-from和-to选项的情况下&#xff0c;命令可以接受无效起始点列表或端点。当指定了无效的起始点时&#xff0c;正时发动机停止正时的传播通过该节点&#xff0c;使该节点成为有效的起点。 在以下示例…

65.乐理基础-打拍子-前附点、后附点

内容来源于&#xff1a;三分钟音乐社 上一个内容&#xff1a;前八后十六、前十六后八拍子-CSDN博客 前附点指的是一个附点八分音符加一个十六分音符的节奏型&#xff0c;如图1。 后附点指的是一个十六分音符加一个附点八分音符的节奏型&#xff0c;如图2。 前附点、后附点这两…

QLabelQPushButton和QLineEdit

QLabel 设置文件格式字体颜色背景 源码 设置图片 源码 设置gif 设置文本 源码 富文本 (Rich Text): 格式化选项&#xff1a;富文本支持各种格式化选项&#xff0c;如字体样式&#xff08;粗体、斜体&#xff09;、字体大小、颜色、超链接、图片插入、列表、表格等。文件格式&a…

基于ssm的企业在线培训系统论文

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装企业在线培训系统软件来发挥其高效地信息处理的作用&#x…

TransNeXt:稳健的注视感知ViT学习笔记

论文地址&#xff1a;https://arxiv.org/pdf/2311.17132.pdf 代码地址&#xff1a; GitHub - DaiShiResearch/TransNeXt: Code release for TransNeXt model 可以直接在ImageNet上训练的分类代码&#xff1a;GitHub - athrunsunny/TransNext-classify 代码中读取数据的部分修改…

低信噪比环境下的语音端点检测

端点检测技术 是 语音信号处理 的关键技术之一为提高低信噪比环境下端点检测的准确率和稳健性&#xff0c;提出了一种非平稳噪声抑制和调制域谱减结合功率 归一化 倒谱距离的端点检测算法 1 端点检测 1-1 定义 定义&#xff1a;在 存在背景噪声 的情况下检测出 语音的起始点和…

关于Java并发、JVM面试题

前言 之前为了准备面试&#xff0c;收集整理了一些面试题。 本篇文章更新时间2023年12月27日。 最新的内容可以看我的原文&#xff1a;https://www.yuque.com/wfzx/ninzck/cbf0cxkrr6s1kniv 并发 进程与线程的区别 线程属于进程&#xff0c;进程可以拥有多个线程。进程独享…

TDengine 公布 2023 年发展“成绩”,六大亮点引人瞩目

今天&#xff0c;我们进行了 2023 年重大成就和发展成绩盘点&#xff0c;主要归纳为产品创新、市场发展、开源社区、生态建设、活动布道与奖项荣誉六大维度。在元旦前夕&#xff0c;我们也想把这份“2023 年成绩单”分享给所有关注 TDengine 的朋友们。 在今年&#xff0c;最值…

第八章 javascript字符(string)的介绍和使用

文章目录 一、什么是字符二、什么是包装数据类型&#xff08;了解&#xff09;三、创建字符串四、字符串操作五、字符编码和字符集ASCII&#xff08;as key&#xff09; 字符集&#xff08;了解&#xff09;unicode 编码 ***国标编码GBK&#xff08;汉字内码扩展规范&#xff0…

UEFI模拟环境搭建——windows+EDKII

目录 0 说明 1 安装软件 1.1 VS2019的安装 1.2 Python的安装 1.3 IASL的安装 1.4 NASM的安装 1.5 git的下载 2 EDKII的下载 3 配置环境 0 说明 个人感觉UEFI的环境搭建非常复杂&#xff0c;在经过很长一段折磨后&#xff0c;终于还是搭建成功&#xff0c;写下来记录一…

YOLOv8训练自定义数据集和运行参数解读

1、YOLOv8深度学习环境搭建及安装 1.1. Yolov8介绍 设置操作类型 YOLOv8模型可用于各种任务&#xff0c;包括检测、分割和分类。这些任务的不同之处在于它们产生的输出类型和它们要解决的特定问题。 **检测:**检测任务涉及识别和定位图像或视频中感兴趣的对象或区域。YOLO模…

0基础学习VR全景平台篇第132篇:曝光三要素—快门速度

上课&#xff01;全体起立~ 大家好&#xff0c;欢迎观看蛙色官方系列全景摄影课程&#xff01; 经过前面两节课的学习我们认识了曝光三要素中的感光度和光圈&#xff0c;这节课我们将一同去了解影响曝光的最后一个要素——快门速度。 (曝光三要素&#xff1a;感光度、光圈、…

所有逐个位置相加的方法

989. 【加法模板】秒杀所有逐位相加 参考教程

算法设计与分析 | 矩阵连乘

题目描述 一个n*m矩阵由n行m列共n*m个数排列而成。两个矩阵A和B可以相乘当且仅当A的列数等于B的行数。一个N*M的矩阵乘以一个M*P的矩阵等于一个N*P的矩阵&#xff0c;运算量为nmp。 矩阵乘法满足结合律&#xff0c;A*B*C可以表示成(A*B)*C或者是A*(B*C)&#xff0c;两者的运算…

CSS 纵向扩展动画

上干货 <template><!-- mouseenter"startAnimation" 表示在鼠标进入元素时触发 startAnimation 方法。mouseleave"stopAnimation" 表示在鼠标离开元素时触发 stopAnimation 方法。 --><!-- 容器元素 --><div class"container&q…

JavaSE50题:26. (数组练习题)使奇数位于偶数之前

概述 调整数组顺序使得奇数位于偶数之前&#xff0c;调整之后&#xff0c;不关心大小顺序。 如数组&#xff1a;{1,2,3,4,5,6} 调整后可能是&#xff1a;{1&#xff0c;5&#xff0c;3&#xff0c;4&#xff0c;2&#xff0c;6} 方法 定义 left 和 right&#xff0c;二者分别…

K8S结合Prometheus构建监控系统

一、Prometheus简介 Prometheus 是一个开源的系统监控和警报工具&#xff0c;用于收集、存储和查询时间序列数据。它专注于监控应用程序和基础设施的性能和状态&#xff0c;并提供丰富的查询语言和灵活的告警机制1、Prometheus基本介绍 数据模型&#xff1a;Prometheus 使用时…

48道Linux面试题

本博客将汇总 Linux 面试中常见的题目&#xff0c;并提供详细的解答。 文章目录 1、绝对路径用什么[符号表](https://so.csdn.net/so/search?q符号表&spm1001.2101.3001.7020)示&#xff1f;当前目录、上层目录用什么表示&#xff1f;主目录用什么表示? 切换目录用什么命…