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