最近在开发中遇到一个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. 【Kubernetes 系列一】Kubernetes 概述

    以下内容还可以通过 Google Slide 查看:https://docs.google.com/presentation/d/1eYP4bkVBojI_e6PqdpxIf0hvWO-JwAf-fy ...

  2. alluxio源码解析-层次化存储(4)

    层次化存储-特性介绍: https://www.alluxio.org/docs/1.6/cn/Tiered-Storage-on-Alluxio.html 引入分层存储后,Alluxio管理的数据块 ...

  3. 从源码看Flask框架配置管理

    1 引言 Flask作为Python语言web开发的三大顶梁柱框架之一,对于配置的管理当然必不可少.一个应用从开发到测试到最后的产品发布,往往都需要多种不同的配置,例如是否开启调试模式.使用哪个数据库 ...

  4. 8.9 day30 并发编程 进程理论 进程方法 守护进程 互斥锁

    多道技术 1.空间上的复用 多个程序共用一套计算机硬件 多道技术原理 2.时间上的复用 ​ 切换+保存状态 ​ 1.当一个程序遇到IO操作 操作系统会剥夺该程序的CPU执行权限( 提高了CPU的利用率 ...

  5. 自己实现spring核心功能 三

    前言 前两篇已经基本实现了spring的核心功能,下面讲到的参数绑定是属于springMvc的范畴了.本篇主要将请求到servlet后怎么去做映射和处理.首先来看一看dispatherServlet的 ...

  6. 数据仓库系列之ETL过程和ETL工具

    上周因为在处理很多数据源集成的事情一直没有更新系列文章,在这周后开始规律更新.在维度建模中我们已经了解数据仓库中的维度建模方法以及基本要素,在这篇文章中我们将学习了解数据仓库的ETL过程以及实用的ET ...

  7. tracert和traceroute介绍

    一.tracert和traceroute简介 相同点:都是用来跟踪路由,帮助排查问题,关注的是过程,而ping关注的是结果 不同点:tracert请求是icmp echo报文 traceroute请求 ...

  8. idea打开eclipse项目并部署至tomcat

    前言:因为遇到了用idea打开eclipse项目并配置外置tomact的场景,经过实验也成功了,所以特地来记录一下. 导入eclipse项目 这里我们导入的eclipse下开发的maven项目,直接点 ...

  9. hdu6703_array

    题意 给定一个1到\(n\)的全排列,两种操作,将\(a_{pos}\)修改为\(a_{pos}+1000000\),询问第一个大于等于\(k\)的且不在\(a_1...a_r\)的数. 分析 由于\ ...

  10. RSA加密的java实现

    首先科普一波: RSA的1024位是指公钥及私钥分别是1024bit,也就是1024/8=128 Bytes RSA算法密钥长度的选择是安全性和程序性能平衡的结果,密钥长度越长,安全性越好,加密解密所 ...