起因
书接上回:【问题排查】【⭐⭐】简约而不简单的StringIndexOutOfBoundsException
其实当时排查已经接近正确答案了,但仍然没有查清楚是具体哪个序列化环节出错,所以本文继续深入探索到底问题出在哪。
排查
kryo序列化流程
当我们知道是序列化框架问题的时候就好办了。
先来梳理一下kryo的调用顺序是什么样的(其实一开始是不知道的,是调试的时候一点一点看出来的:cry:):

模拟环境
由于上回排查问题时已经知道问题会出现在版本差异比较大的jar包之间,所以这次也是选用了差异比较大的两个版本。
为了展示例子足够简单和尽量不涉及敏感信息,本文对结构进行了简化。
旧版本Class结构
class Outer {
String name;
List<Inner> innerList;
}
class Inner {
Integer count;
String userType;
String idNo;
// String agreementType; 对此字段进行注释,相当于旧版本没有这个字段
}
复制代码
新版本Class结构
class Outer {
String name;
List<Inner> innerList;
}
class Inner {
Integer count;
String userType;
String idNo;
String agreementType;
}
复制代码
模拟新版本Class序列化,旧版本Class反序列化
序列化操作代码
private static String encode() {
Kryo kryo = new Kryo();
kryo.setDefaultSerializer(CompatibleFieldSerializer.class);
Output output = new Output(256, -1);
kryo.writeClassAndObject(output, newInstance());
return Base64.getEncoder().encodeToString(output.toBytes());
}
private static Outer newInstance() {
List<Inner> innerList = new LinkedList<>();
for (int i = 0; i < 10; i++) {
Inner inner = new Inner();
inner.count = i;
inner.agreementType = "1";
inner.idNo = "350103";
inner.userType = "01";
innerList.add(inner);
}
Outer outer = new Outer();
outer.name = "out";
outer.innerList = innerList;
return outer;
}
复制代码
调用上面encode得到一串base64字符串
AQBjb20uZXNvdGVyaWNzb2Z0d2FyZS5rcnlvLnNlcmlhbGl6ZXJzLk91dGXyAQJpbm5lckxpc/RuYW3laQEBamF2YS51dGlsLkxpbmtlZExpc/QBCgECY29tLmVzb3Rlcmljc29mdHdhcmUua3J5by5zZXJpYWxpemVycy5Jbm5l8gEEYWdyZWVtZW50VHlw5WNvdW70aWRO73VzZXJUeXDlAwGCMQQAAgEACQAHATM1MDEwswUAAwEwsQYAAQIBAQUEAAIBAgMAAQYDAAEHBgABAgEBBQQAAgEEAwABBgMAAQcGAAECAQEFBAACAQYDAAEGAwABBwYAAQIBAQUEAAIBCAMAAQYDAAEHBgABAgEBBQQAAgEKAwABBgMAAQcGAAECAQEFBAACAQwDAAEGAwABBwYAAQIBAQUEAAIBDgMAAQYDAAEHBgABAgEBBQQAAgEQAwABBgMAAQcGAAECAQEFBAACARIDAAEGAwABBwEAAAQBb3X0AA==
复制代码
因为这个框架都是直接写入或读出bytes,所以乍一看根本看不出这字符串有问题。接着我们拿着这串字符串反序列化。
反序列化操作代码
private static Object decode(String base64) {
byte[] decode = Base64.getDecoder().decode(base64);
Kryo kryo = new Kryo();
kryo.setDefaultSerializer(CompatibleFieldSerializer.class);
Input input = new Input(decode);
kryo.setClassLoader(Thread.currentThread().getContextClassLoader());
Object o = kryo.readClassAndObject(input);
return o;
}
复制代码
得到的对象toString后
Outer{name='out', innerList=[Inner{count=0, userType='01', idNo='350103'}, Inner{count=1, userType='', idNo='01'}, Inner{count=2, userType='', idNo='01'}, Inner{count=3, userType='', idNo='01'}, Inner{count=4, userType='', idNo='01'}, Inner{count=5, userType='', idNo='01'}, Inner{count=6, userType='', idNo='01'}, Inner{count=7, userType='', idNo='01'}, Inner{count=8, userType='', idNo='01'}, Inner{count=9, userType='', idNo='01'}]}
复制代码
发现问题了吗?
list中从第1位开始往后的对象里字段userType和idno的值错了。我们刚刚让所有对象的idNo=350103的。这怎么回事?
调试
我们是知道我们自己使用的是什么序列化器的,那么直接将断点打在read方法里
github地址:github.com/EsotericSof…
上面链接是read方法,方法内部完整代码太长就不贴了,我将反序列化过程用文字描述:
- 实例化我们要反序列化的对象
- 找到byte数据中所有此类的字段名
- 根据字段名找到字段包装类(如果字段太多,会启用二分搜索)
- 使用字段包装类还原字段值
- 如果字段值是原始类型类的数据(不可继续深度遍历的),反射设置到对象的字段中
- 如果字段值是包装类(里面还有其他字段的),递归1~4步骤
根据上面模拟测试结果发现idNo=’01’,而01是我们设置给userType的。再加上我们是减少了个字段,初步判断是值读错位了!
反序列化
首先跟断点至innerList[0].userType,也就是第一次反序列化Inner.userType。
经历了以下步骤:
- 初始化准备工作(校验、初始化等等)
- 判断是否是引用,是
- 尝试读取引用中的数据,如读到则直接返回对象
- 读不到引用中的数据时,从原始数据中反序列化后放入引用中
- 返回对象
引用在这里的作用相当于一个“缓存”,默认有两种实现ListReferenceResolver和MapReferenceResolver。如果相同对象相同值,kryo偷懒地将引用中的值直接返回,间接地节省了重复反序列化的耗时。

第二次跟踪端点至innerList[1].userType
经历了以下步骤:
- 初始化准备工作(校验、初始化等等)
- 判断是否是引用,是
- 尝试读取引用中的数据,读到后返回对象

这流程看起来没问题啊?但请看下此时从引用中读到的对象是什么

readObject=Inner对象,而userType实际类型是String。把Inner对象塞给String,这简直在乱来!
读取引用

id是引用的坐标,可以看成是key
referenceResolver是维护引用的类,可以看成是map(实际上里面的实现是list)
方法的大致意思:
- 获取key
- value = map.get(key)
- return value
错位就发生在第一步
看代码831行,id = input.readVarInt(true)。 此操作从原始数据中读取id,意味着id是序列化时生成的。 但我们的原始数据是用新Class生成的,新Class是有agreementType字段的,而旧Class没有该字段。如果以list作为引用维护对象,相当于字段增加或缺少N个,后续的索引会整体向左或向右移动N位。
以下图为例(实际代码和这个很像,但不完全一样,比如下标的生成,这里只是举个例子):

我们将inner.c字段删除。inner.d和inner.e顺位向左移动1格。那么在反序列化inner.d时,按照序列化时的id应当是4。接着按照id=4在旧Class生成的引用数组寻找id=4的值,值为”ee”。将”ee”赋值inner.d,从而发生了错位。
结论
触发条件
当开启引用时,引用对象在新旧版本发生增减则会导致兼容性问题。
kryo4在单独使用CompatibleFieldSerializer时没有问题,比如这样的结构:
{
"user": {
"name": "zhangsan",
"son": {
"name": "zhangsi",
"son": {
"name": "zhangwu"
}
}
}
}
复制代码
但是CompatibleFieldSerializer结合CollectionSerializer使用时,新旧版本Class有新增或减少字段,就会出现字段读取错位的情况,比如这样的结构:
{
"list": [
{
"name": "zhangsan",
"age": 20,
},
{
"name": "lisi",
"age": 25,
}
]
}
复制代码
解决方法
关闭引用
Kryo kryo = new Kryo();
kryo.setReferences(false);
复制代码
结果正常
Outer{name='out', innerList=[Inner{count=0, userType='01', idNo='350103'}, Inner{count=1, userType='01', idNo='350103'}, Inner{count=2, userType='01', idNo='350103'}, Inner{count=3, userType='01', idNo='350103'}, Inner{count=4, userType='01', idNo='350103'}, Inner{count=5, userType='01', idNo='350103'}, Inner{count=6, userType='01', idNo='350103'}, Inner{count=7, userType='01', idNo='350103'}, Inner{count=8, userType='01', idNo='350103'}, Inner{count=9, userType='01', idNo='350103'}]}
复制代码
升级至kryo5
由于kryo5在kryo4的基础上做了破坏性的改动,比如:
com.esotericsoftware.kryo.io.Output#writeInt
- 调换写入顺序(完全不想兼容旧版本了属于是)
- 减少posistion赋值动作
// kryo4: com.esotericsoftware.kryo.io.Output#writeInt
public void writeInt (int value) throws KryoException {
require(4);
byte[] buffer = this.buffer;
buffer[position++] = (byte)(value >> 24);
buffer[position++] = (byte)(value >> 16);
buffer[position++] = (byte)(value >> 8);
buffer[position++] = (byte)value;
}
// kryo5: com.esotericsoftware.kryo.io.Output#writeInt
public void writeInt (int value) throws KryoException {
require(4);
byte[] buffer = this.buffer;
int p = position;
position = p + 4;
buffer[p] = (byte)value;
buffer[p + 1] = (byte)(value >> 8);
buffer[p + 2] = (byte)(value >> 16);
buffer[p + 3] = (byte)(value >> 24);
}
复制代码
两个版本间生成的数据大概率是不能兼容的,原话:
Moving from v2 to v5 or from v4 to v5 is about the same: the newer Kryo version is unlikely to be able to read bytes written with the older version.
对于不能向下兼容的升级是很痛苦的。而且dubbo3已经把kryo从序列化支持里移除了。
参考
- 迁移文档 github.com/EsotericSof…
- Buffer underflow while deserializing with Collection type field removed (with reproducible test case) github.com/EsotericSof…























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)