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

2023-12-28 13:03:42

背景:

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

排查:

????????初步排查了用户和商品的基本信息,发现没有问题。然后根据上游的异常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>

文章来源:https://blog.csdn.net/a314774167/article/details/135265923
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。