深入探究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>> implements
        PolymorphicSchema.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#myEnum
            output.writeEnum(number, getTag(e), repeated);
        else
            output.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
    @Setter
    static 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();
        // 位置1
        serialization.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);
        // 位置2
        Object changeDemoReq = serialization.deserialize(null, changeStream).readObject();
        return changeDemoReq;
    }
}

核心代码:

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

public abstract class EnumIO<E extends Enum<E>> implements
        PolymorphicSchema.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#myEnum
            output.writeEnum(number, getTag(e), repeated);
        else
            output.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
    @Override
    public 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)
        // 外层再写 ordinal
            tail = sink.writeVarInt32(
                    value,
                    this,
                    sink.writeVarInt32(
                            makeTag(fieldNumber, WIRETYPE_VARINT),
                            this,
                            tail));
        }
    }
}

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

public class ProtostuffObjectInput implements ObjectInput {
    ...
    ...
    ...
    @Override
    public 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)
{
    @Override
    public <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))
        {
            @Override
            public void mergeFrom(Input input, T message)
                    throws IOException
            {
                // message是 model对象
                // offset 是@tag(3)
                // input是 对象的属性值 [24,2]
                // eio.valueByTagMap维护 ordinal&枚举 的关系
                // eio.readFrom(input) 返回的是具体的枚举 FIVE
                us.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://www.mfbz.cn/a/274884.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C# MVC +Layui侧边导航栏的收缩及展开

目录 1、头部代码 2、侧边栏&#xff08;例子只写了一级导航&#xff0c;需要多级可自行添加&#xff09; 3、body内容填充 4、 JS 1、头部代码 <div class"layui-layout layui-layout-admin"> <div class"layui-header"> …

MySQL 核心模块揭秘 |《发刊词》

1. 为什么要写专栏&#xff1f; 我还在做业务系统研发的时候&#xff0c;有一段时间&#xff0c;系统不稳定&#xff0c;慢 SQL 很多。我们团队花了很长时间持续优化 SQL。 我们有一个表格&#xff0c;从慢查询日志里整理出了很多慢 SQL。其中一些 SQL&#xff0c;按照我们的…

大华NVR和IPC通过主动注册协议方式接入AS-V1000视频监控平台的步骤

最近有人经常用到有的型号的大华网路摄像机&#xff0c;不支持国标GB28181标准&#xff0c;问我们能否接入到在公网的AS-V1000平台 &#xff1f; 我们早期就开发了大华的主动注册协议SDK&#xff0c;能够支持大华的NVR和IPC接入到AS-V1000平台。 今天就直接讲解如何一步步的把局…

人工智能 机器学习 深度学习:概念,关系,及区别说明

如果过去几年&#xff0c;您读过科技主题的文章&#xff0c;您可能会遇到一些新词汇&#xff0c;如人工智能&#xff08;Artificial Intelligence&#xff09;、机器学习&#xff08;Machine Learning&#xff09;和深度学习&#xff08;Deep Learning&#xff09;等。这三个词…

关于使用Selenium获取网页控制台的数据

背景&#xff1a; 需要获取网页的控制台的数据&#xff0c;如下图 在此文章将使用到 Pycharm 和 Selenium4 Pycharm安装 Selenium安装 from selenium import webdriver from selenium.webdriver.common.by import By import time# 创建浏览器对象 browser webdriver.Chro…

普中STM32-PZ6806L开发板(使用过程中的问题收集)

Keil使用ST-Link 报错 Internal command error 描述: 在某一次使用过程中&#xff0c;前面都是正常使用, Keil在烧录时报错Internal command error, 试了网上的诸多方式, 例如 升级固件;ST-Link Utility 清除;Keil升级到最新版本;甚至笔者板子的Micro头也换了&#xff0c;因为坏…

docker学习笔记02-安装mysql

1.安装mysql8 下载MySQL镜像 docker pull mysql:8.0创建并启动容器 docker run -itd --name mysqltest -p 9999:3306 -e MYSQL_ROOT_PASSWORD123456 mysql其中-it是交互界面 -d是后台执行 -name 指定容器名称 -p指定映射端口 -e设置环境变量 最后mysql是镜像名或者用镜像id如…

消防数据监测可视化大屏:守护城市安全的智慧之眼

在数字化时代&#xff0c;数据已经成为决策的关键。特别是在消防领域&#xff0c;快速、准确的数据分析对于及时应对火情、挽救生命财产具有不可估量的价值。为此&#xff0c;消防数据监测可视化大屏应运而生&#xff0c;成为城市安全的守护者。 一、什么是消防数据监测可视化大…

Qt 中使用 MySQL 数据库保姆级教程(下)

作者&#xff1a;billy 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 前言 上篇中我们安装好了 MySQL 数据库和 Navicat 软件&#xff0c;下面在 Qt 中尝试使用数据库 1. 在 Qt 中连接 MySQL 数据库&#…

VuePress、VuePress-theme-hope 搭建个人博客 1【快速上手】 —— 防止踩坑篇

vuePress官网地址 &#x1f449; 首页 | VuePress 手动安装 这一章节会帮助你从头搭建一个简单的 VuePress 文档网站。如果你想在一个现有项目中使用 VuePress 管理文档&#xff0c;从步骤 3 开始。 步骤 1: 创建并进入一个新目录 mkdir vuepress-starter cd vuepress-star…

Unity 代码控制Text自适应文本高度

在使用代码给Text赋值时&#xff0c;且文本有多段&#xff0c;并需要根据实际文本高度适配Text组件的高度时&#xff0c;可以使用以下方法&#xff1a; //Text文本 public TextMeshProUGUI text;void Start() {//代码赋值文本text.text "好!\n很好!\n非常好!";//获…

加强->servlet->tomcat

0什么是servlet jsp也是servlet 细细体会 Servlet 是 JavaEE 的规范之一&#xff0c;通俗的来说就是 Java 接口&#xff0c;将来我们可以定义 Java 类来实现这个接口&#xff0c;并由 Web 服务器运行 Servlet &#xff0c;所以 TomCat 又被称作 Servlet 容器。 Servlet 提供了…

ebay如何发货到国外?ebay发货规则是什么?——站斧浏览器

​ebay如何发货到国外&#xff1f; 对于卖家来说&#xff0c;在eBay上向海外买家发货可能会面临一些额外的挑战。以下是一些建议&#xff0c;帮助卖家顺利地将商品发货给海外买家&#xff1a; 1、选择合适的物流服务&#xff1a;eBay合作的物流服务通常提供可靠和经济实惠的国…

【ARMv8M Cortex-M33 系列 2.1 -- Cortex-M33 使用 .hex 文件介绍】

文章目录 HEX 文件介绍英特尔十六进制文件格式记录类型hex 示例Cortex-M 系列hex 文件的使用 HEX 文件介绍 .hex 文件通常用于微控制器编程&#xff0c;包括 ARM Cortex-M 系列微控制器。这种文件格式是一种文本记录&#xff0c;用于在编程时传递二进制信息。.hex 文件格式最常…

【Java EE初阶三 】线程的状态与安全(上)

1. join方法与多线程 1.1 初识多线程 为了提高cpu得利用率&#xff0c;因此就引入了多个线程的概念&#xff1b;即每个线程负责完成整个程序的一部分工作即可。 写一个代码&#xff0c;让主线程&#xff0c;创建一个新的线程&#xff0c;由新线程负责完成运算&#xff08;12。…

人工智能时代,如何借助新技术实现突破?| 圆桌对话

继上篇介绍完干货满满的议题分享后&#xff0c;更精彩的圆桌论坛衔尾相随。本次圆桌对话以“人工智能时代&#xff0c;如何借助新技术实现突破&#xff1f;”为主题&#xff0c;由华锐技术机构市场团队负责人-高媛主持&#xff0c;邀请了AMD中国区数据中心事业部资深架构师-梁朝…

轻松实现不同类型文件,统一重命名与隐藏编号!

你是否经常需要处理大量的文件&#xff0c;却为如何进行高效的文件管理而苦恼&#xff1f;现在&#xff0c;我们为你提供了一种全新的解决方案——轻松实现文件统一重命名与隐藏编号&#xff01; 第一步&#xff0c;首先我们要进入文件批量改名高手主页面&#xff0c;并在板块栏…

优思学院|掌握TPM的全貌:8大支柱及其优势

TPM&#xff08;全面生产维护&#xff09;是一种旨在通过组织内人员的参与&#xff0c;持续改进生产设备或制造过程的有效性的方法。它的目标是实现“零机器故障”或“零停机”。 TPM的八大支柱 自主保养&#xff08;JISHU HOZEN&#xff09; 操作员执行日常的CLIT活动&#…

【c++、数据结构课设】拓扑序列的应用

再贡献一篇课设&#xff0c;希望能帮助到正在做课设的小伙伴。 屏幕录制2023-12-27 22.28.48 课设要求 题目描述 大学的每个专业都要制定教学计划。假设任何专业都有固定的学习年限&#xff0c;每学年含两学期&#xff0c;每学期的时间长度和学分上限值均相等。每个专业开设的…

使用pandas处理数据的一些总结

1、替换换行符等特殊符号 df df.replace({None: "", np.nan: "", "\t": "", "\n": "", "\x08": ""}, regexTrue) 2、清除DataFrame中所有数据的左右空格&#xff0c;字符串中间空格不会清…