Protostuff序列化问题
最近在开发中遇到一个Protostuff序列化问题,在这记录一下问题的根源;分析一下Protostuff序列化和反序列化原理;以及怎么样避免改bug。
1. 问题描述
有一个push业务用到了mq,mq的生产者和消费者实体序列化我们用的是Protostuff方式实现的。由于业务需要,我们要在一个已有的枚举类添加一种类型,比如:
public enum LimitTimeUnit {
NATURAL_DAY {
@Override
public long getRemainingMillis() {
Date dayEnd = DateUtils.getDayEnd();
return dayEnd.getTime() - System.currentTimeMillis();
}
};
/**
* 距离当前单位时间结束剩余毫秒数.
* @return
*/
public abstract long getRemainingMillis();
}
中添加一个类型 NATURAL_MINUTE :
public enum LimitTimeUnit {
NATURAL_MINUTE {
@Override
public long getRemainingMillis() {
return 1000 * 60;
}
},
NATURAL_DAY {
@Override
public long getRemainingMillis() {
Date dayEnd = DateUtils.getDayEnd();
return dayEnd.getTime() - System.currentTimeMillis();
}
};
/**
* 距离当前单位时间结束剩余毫秒数.
* @return
*/
public abstract long getRemainingMillis();
}
消费端项目添加了这个字段升级了版本,但是消费者在有些项目中没有升级,测试的时候看日志没有报错,所以就很happy上线了回家睡个好觉。第二天测试找到我问:为什么昨晚我收到那么多push...不是限制每天限制只能收到...?我:哦,这是以前的逻辑吗?...好的,我看看!佛系开发没办法!
2. 定位问题
打开app快速(一分钟内)按测试所说的流程给自己搞几个push,发现没有问题啊!然后开始跟测试磨嘴皮,让他给我重现,哈哈,他也重现不了!就这样我继续撸代码...安静的过了五分钟。测试又来了...后面发送的事大家自己YY一下。
快速找到对应生产者代码,封装的确实是 NATURAL_DAY,那只能debug消费者这边接收的代码。发现消费者接收到是 NATURAL_MINUTE!看到这里测试是对的,本来限制一天现在变成一分钟!!!是什么改变这个值呢?mq只是一个队列,保存的是字节码,一个对象需要序列化成字节码保存到mq,从mq获取对象需要把字节码反序列化成对象。那么问题根源找到了,是序列化和反序列化时出了问题。
3. Protostuff序列化过程
该问题是Protostuff序列化引起的,那么解决这个问题还得弄懂Protostuff序列化和反序列化原理。弄懂原理最好的办法就是看源码:
public class ProtoStuffSerializer implements Serializer {
private static final Objenesis objenesis = new ObjenesisStd(true);
private static final ConcurrentMap<Class<?>, Schema<?>> schemaCache = new ConcurrentHashMap<>();
private ThreadLocal<LinkedBuffer> bufferThreadLocal = ThreadLocal.withInitial(() -> LinkedBuffer.allocate());
@Override
public <T> byte[] serialize(T obj) {
Schema<T> schema = getSchema((Class<T>) obj.getClass());
LinkedBuffer buf = bufferThreadLocal.get();
try {
// 实现object->byte[]
return ProtostuffIOUtil.toByteArray(obj, schema, buf);
} finally {
buf.clear();
}
}
@Override
public <T> T deserialize(byte[] bytes, Class<T> clazz) {
T object = objenesis.newInstance(clazz); // java原生实例化必须调用constructor. 故使用objenesis
Schema<T> schema = getSchema(clazz);
ProtostuffIOUtil.mergeFrom(bytes, object, schema); // 反序列化源码跟踪入口
return object;
}
private <T> Schema<T> getSchema(Class<T> clazz) {
Schema<T> schema = (Schema<T>) schemaCache.get(clazz);
if (schema == null) {
// 把可序列化的字段封装到Schema
Schema<T> newSchema = RuntimeSchema.createFrom(clazz);
schema = (Schema<T>) schemaCache.putIfAbsent(clazz, newSchema);
if (schema == null) {
schema = newSchema;
}
}
return schema;
}
这是我们实现Protostuff序列化工具类。接下来看一下 ProtostuffIOUtil.toByteArray(obj, schema, buf) 这个方法里面重要代码:
public static <T> byte[] toByteArray(T message, Schema<T> schema, LinkedBuffer buffer)
{
if (buffer.start != buffer.offset)
throw new IllegalArgumentException("Buffer previously used and had not been reset."); final ProtostuffOutput output = new ProtostuffOutput(buffer);
try
{
// 继续跟进去
schema.writeTo(output, message);
}
catch (IOException e)
{
throw new RuntimeException("Serializing to a byte array threw an IOException " +
"(should never happen).", e);
}
return output.toByteArray();
}
public final void writeTo(Output output, T message) throws IOException
{
for (Field<T> f : getFields())
// 秘密即将揭晓
f.writeTo(output, message);
}
RuntimeUnsafeFieldFactory这里面才是关键:
@Override
public void writeTo(Output output, T message) throws IOException
{
CharSequence value = (CharSequence)us.getObject(message, offset);
if (value != null)
// 看这里
output.writeString(number, value, false);
}
跟踪到这里,我们把一切谜题都解开了。原来Protostuff序列化时是按可序列化字段顺序只把value保存到字节码中。
4. Protostuff反序列化过程
以下是反序列化源码的跟踪:ProtostuffIOUtil.mergeFrom(bytes, object, schema) 里面重要的代码:
public static <T> void mergeFrom(byte[] data, T message, Schema<T> schema)
{
IOUtil.mergeFrom(data, 0, data.length, message, schema, true);
}
static <T> void mergeFrom(byte[] data, int offset, int length, T message,
Schema<T> schema, boolean decodeNestedMessageAsGroup)
{
try
{
final ByteArrayInput input = new ByteArrayInput(data, offset, length,
decodeNestedMessageAsGroup);
// 继续跟进
schema.mergeFrom(input, message);
input.checkLastTagWas(0);
}
catch (ArrayIndexOutOfBoundsException ae)
{
throw new RuntimeException("Truncated.", ProtobufException.truncatedMessage(ae));
}
catch (IOException e)
{
throw new RuntimeException("Reading from a byte array threw an IOException (should " +
"never happen).", e);
}
}
@Override
public final void mergeFrom(Input input, T message) throws IOException
{
// 按顺序获取字段
for (int n = input.readFieldNumber(this); n != 0; n = input.readFieldNumber(this))
{
final Field<T> field = getFieldByNumber(n);
if (field == null)
{
input.handleUnknownField(n, this);
}
else
{
field.mergeFrom(input, message);
}
}
}
public void mergeFrom(Input input, T message)
throws IOException
{
// 负载给字段
us.putObject(message, offset, input.readString());
}
5. 总结
通过protostuff的序列化和反序列化源码知道一个对象序列化时是按照可序列化字段顺序把值序列化到字节码中,反序列化时也是按照当前对象可序列化字段顺序赋值。所以会出现 NATURAL_DAY 经过序列化和反序列化后变成 NATURAL_MINUTE。由于这两个字段类型是一样的,反序列化没有报错,如果序列化前的对象和反序列化接收对象对应顺序字段类型不一样时会出现反序列失败报错。为了避免以上问题,在使用protostuff序列化时,对已有的实体中添加字段放到最后去就可以了。
Protostuff序列化问题的更多相关文章
- Protostuff序列化分析
前言最近项目中需要将业务对象直接序列化,然后存数据库:考虑到序列化.反序列化的时间以及生产文件的大小觉得Protobuf是一个很好的选择,但是Protobuf有的问题就是需要有一个.proto的描述文 ...
- Protostuff序列化
前言: Java序列化是Java技术体系当中的一个重要议题,序列化的意义在于信息的交换和存储,通常会和io.持久化.rmi技术有关(eg:一些orm框架会要求持久化的对象类型实现Serializabl ...
- Protostuff序列化工具类
源代码 package org.wit.ff.util; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStre ...
- protostuff序列化使用
背景 最近在做项目的时候需要使用持久化功能,1.0版本中使用的akka自带的持久化功能,使用的是akka persist支持的redis插件,在使用的过程中踩了一些坑.因此,在而2.0版本中考虑自己往 ...
- Protostuff序列化和反序列化
序列化和反序列化是在应对网络编程最常遇到的问题之一. 序列化就是将Java Object转成byte[]:反序列化就是将byte[]转成Java Object. 这里不介绍JDK serializab ...
- java protostuff 序列化反序列化工具
protostuff是由谷歌开发的一个非常优秀的序列化反序列化工具 maven导入包: <dependency> <groupId>io.protostuff</grou ...
- Protostuff序列化和反序列化使用说明
原文:http://blog.csdn.net/zhglance/article/details/56017926 google原生的protobuffer使用起来相当麻烦,首先要写.proto文件, ...
- 通讯协议序列化解读(二) protostuff详解教程
上一篇文章 通讯协议序列化解读(一):http://www.cnblogs.com/tohxyblog/p/8974641.html 前言:上一面文章我们介绍了java序列化,以及谷歌protobu ...
- java序列化/反序列化之xstream、protobuf、protostuff 的比较与使用例子
目录 背景 测试 环境 工具 说明 结果 结论 xstream简单教程 准备 代码 protobuf简单教程 快速入门 下载.exe编译器 编写.proto文件 利用编译器编译.proto文件生成ja ...
随机推荐
- eclipse Maven配置以及使用方法
简述: 现需要在Eclipse中配置Maven插件,同时安装maven应用,配置Maven环境变量,建立Maven管理的工程,并用Maven导入Gson包, 编写简易Json输出程序 步骤: 1. 首 ...
- Java实现ZooKeeper的zNode监控
上一篇文章已经完成了ZooKeeper的基本搭建和使用的介绍,现在开始用代码说话.参考 https://zookeeper.apache.org/doc/current/javaExample.htm ...
- 洛谷 P3870 [TJOI2009]开关
题意简述 有n盏灯,默认为关,有两个操作: 1.改变l~r的灯的状态(把开着的灯关上,关着的灯打开) 2.查询l~r开着的灯的数量 题解思路 维护一个线段树,支持区间修改,区间查询 懒标记每次^1 代 ...
- Windows Server 2008创建域环境
介绍一下域环境搭建,域主要用于中大型企业,小型企业计算机数量不多,而中大型企业计算机比较多,使用域可以方便管理,安全性也比在工作组中安全 1.安装完操作系统默认都属于WORKGROUP工作组. 2.安 ...
- hadoop2.7之作业提交详解(下)
接着作业提交详解(上)继续写:在上一篇(hadoop2.7之作业提交详解(上))中已经讲到了YARNRunner.submitJob() [WordCount.main() -> Job.wai ...
- 拼写单词[哈希表]----leetcode周赛150_1001
题目描述: 给你一份『词汇表』(字符串数组) words 和一张『字母表』(字符串) chars. 假如你可以用 chars 中的『字母』(字符)拼写出 words 中的某个『单词』(字符串),那么我 ...
- 自然语言处理(NLP)的一般处理流程!
1. 什么是NLP 自然语言处理 (Natural Language Processing) 是人工智能(AI)的一个子领域.自然语言处理是研究在人与人交互中以及在人与计算机交互中的语言问题的一门学科 ...
- 性能测试学习第一天-----概念、环境、LR录制&参数化
1.性能测试的概念: 通过一定的手段,在多并发情况下,获取被测系统的各项性能指标, 验证被测系统在高并发下的处理能力.响应能力.稳定性等,能否满足预期.定位性能瓶颈,排查性能隐患,保障系统的质量,提升 ...
- EOS源码分析:transaction的一生
最近在处理智能合约的事务上链问题,发现其中仍旧有知识盲点.原有的认识是一个事务请求会从客户端设备打包签名,然后通过RPC传到非出块节点,广播给超级节点,校验打包到可逆区块,共识确认最后变为不可逆区块. ...
- npm钉钉脚手架,支持考勤信息获取
钉钉官方并未提供nodejs包,第一次调用接口的时候非常费事,而且尝试去寻找相关的钉钉考勤数据模块的时候只找到了一些消息啊,只能办公啊,免登啊之类的模块,有关考勤数据的似乎没有 关于dd的npm包中一 ...