最近在开发中遇到一个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序列化问题的更多相关文章

  1. Protostuff序列化分析

    前言最近项目中需要将业务对象直接序列化,然后存数据库:考虑到序列化.反序列化的时间以及生产文件的大小觉得Protobuf是一个很好的选择,但是Protobuf有的问题就是需要有一个.proto的描述文 ...

  2. Protostuff序列化

    前言: Java序列化是Java技术体系当中的一个重要议题,序列化的意义在于信息的交换和存储,通常会和io.持久化.rmi技术有关(eg:一些orm框架会要求持久化的对象类型实现Serializabl ...

  3. Protostuff序列化工具类

    源代码 package org.wit.ff.util; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStre ...

  4. protostuff序列化使用

    背景 最近在做项目的时候需要使用持久化功能,1.0版本中使用的akka自带的持久化功能,使用的是akka persist支持的redis插件,在使用的过程中踩了一些坑.因此,在而2.0版本中考虑自己往 ...

  5. Protostuff序列化和反序列化

    序列化和反序列化是在应对网络编程最常遇到的问题之一. 序列化就是将Java Object转成byte[]:反序列化就是将byte[]转成Java Object. 这里不介绍JDK serializab ...

  6. java protostuff 序列化反序列化工具

    protostuff是由谷歌开发的一个非常优秀的序列化反序列化工具 maven导入包: <dependency> <groupId>io.protostuff</grou ...

  7. Protostuff序列化和反序列化使用说明

    原文:http://blog.csdn.net/zhglance/article/details/56017926 google原生的protobuffer使用起来相当麻烦,首先要写.proto文件, ...

  8. 通讯协议序列化解读(二) protostuff详解教程

    上一篇文章 通讯协议序列化解读(一):http://www.cnblogs.com/tohxyblog/p/8974641.html  前言:上一面文章我们介绍了java序列化,以及谷歌protobu ...

  9. java序列化/反序列化之xstream、protobuf、protostuff 的比较与使用例子

    目录 背景 测试 环境 工具 说明 结果 结论 xstream简单教程 准备 代码 protobuf简单教程 快速入门 下载.exe编译器 编写.proto文件 利用编译器编译.proto文件生成ja ...

随机推荐

  1. Gin + Vue全栈开发实战(一)

    Gin入门 本章概要 Gin简介 开发第一个Gin程序 1.1 Gin简介 Gin是用Go语言编写的一个轻量级Web应用框架,现在在各个公司包括字节跳动.bilibili等大互联网公司都得到了广泛的应 ...

  2. Z算法

    Z算法 Z算法是一种用于字符串匹配的算法.此算法的核心在于\(z\)数组以及它的求法. (以下约定字符串下标从\(1\)开始) \(\bm z\)数组和Z-box 定义\(z\)数组:\(z_{a,i ...

  3. 《机器学习技法》---AdaBoost算法

    1 AdaBoost的推导 首先,直接给出AdaBoost算法的核心思想是:在原数据集上经过取样,来生成不同的弱分类器,最终再把这些弱分类器聚合起来. 关键问题有如下几个: (1)取样怎样用数学方式表 ...

  4. 【数学+思维】ZZULIOJ 1531: 小L的区间求和

    题目链接 题目描述 在给定的一个整数序列中,小L希望找到一个连续的区间,这个区间的和能够被k整除,请你帮小L算一下满足条件的最长的区间长度是多少. 输入 第一行输入两个整数n.k.(1 <= n ...

  5. react父组件调用子组件中方法

  6. 既然synchronized是"万能"的,为什么还需要volatile呢?

    在我的博客和公众号中,发表过很多篇关于并发编程的文章,之前的文章中我们介绍过了两个在Java并发编程中比较重要的两个关键字:synchronized和volatile 我们简单回顾一下相关内容: 1. ...

  7. 建立apk定时自动打包系统第三篇——代码自动更新、APP自动打包系统

    我们的思路是每天下班后团队各成员在指定的时间(例如下午18:30)之前把各自的代码上传到SVN,然后服务器在指定的时间(例如下午18:30)更新代码.执行ant 打包命令.最后将apk包存放在指定目录 ...

  8. 多线程之NSOperation

    关于多线程会有一系列如下:多线程之概念解析 多线程之pthread, NSThread, NSOperation, GCD 多线程之NSThread 多线程之NSOperation 多线程之GCD

  9. 基础部分之System、Object、Calendar、StringBuffer

    System.out.println(obj)和 System.out.println(obj.toString())区别 public static void main(String[] args) ...

  10. springboot + jedisCluster

    如果使用的是redis2.x,在项目中使用客户端分片(Shard)机制. 如果使用的是redis3.x中的集群,在项目中使用jedisCluster. 1.项目结构 2.pom.xml 1 <? ...