背景:
有一天突然被一个群组@排查线上问题,说是一个场景划线价和商品原价一模一样。看到问题时,我的内心毫无波澜,因为经常处理线上类似的问题,但了解业务后发现是上个版本经我手对接的新客弹窗商品算价,内心有一丝小慌,但表面看还是稳的一匹。
排查:
初步排查了用户和商品的基本信息,发现没有问题。然后根据上游的异常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>