Protostuff序列化问题

  最近在开发中遇到一个Protostuff序列化问题,在这记录一下问题的根源;分析一下Protostuff序列化和反序列化原理;以及怎么样避免改bug。

1. 问题描述

  有一个push业务用到了mq,mq的生产者和消费者实体序列化我们用的是Protostuff方式实现的。由于业务需要,我们要在一个已有的枚举类添加一种类型,比如:

 1 public enum LimitTimeUnit {
 2     NATURAL_DAY {
 3         @Override
 4         public long getRemainingMillis() {
 5             Date dayEnd = DateUtils.getDayEnd();
 6             return dayEnd.getTime() - System.currentTimeMillis();
 7         }
 8     };
18     /**
19      * 距离当前单位时间结束剩余毫秒数.
20      * @return
21      */
22     public abstract long getRemainingMillis();
23
24 }

中添加一个类型 NATURAL_MINUTE :

 1 public enum LimitTimeUnit {
 2     NATURAL_MINUTE {
 3         @Override
 4         public long getRemainingMillis() {
 5             return 1000 * 60;
 6         }
 7     },
 8
 9     NATURAL_DAY {
10         @Override
11         public long getRemainingMillis() {
12             Date dayEnd = DateUtils.getDayEnd();
13             return dayEnd.getTime() - System.currentTimeMillis();
14         }
15     };
25     /**
26      * 距离当前单位时间结束剩余毫秒数.
27      * @return
28      */
29     public abstract long getRemainingMillis();
30
31 }

消费端项目添加了这个字段升级了版本,但是消费者在有些项目中没有升级,测试的时候看日志没有报错,所以就很happy上线了回家睡个好觉。第二天测试找到我问:为什么昨晚我收到那么多push...不是限制每天限制只能收到...?我:哦,这是以前的逻辑吗?...好的,我看看!佛系开发没办法!

2. 定位问题

  打开app快速(一分钟内)按测试所说的流程给自己搞几个push,发现没有问题啊!然后开始跟测试磨嘴皮,让他给我重现,哈哈,他也重现不了!就这样我继续撸代码...安静的过了五分钟。测试又来了...后面发送的事大家自己YY一下。

  快速找到对应生产者代码,封装的确实是 NATURAL_DAY,那只能debug消费者这边接收的代码。发现消费者接收到是 NATURAL_MINUTE!看到这里测试是对的,本来限制一天现在变成一分钟!!!是什么改变这个值呢?mq只是一个队列,保存的是字节码,一个对象需要序列化成字节码保存到mq,从mq获取对象需要把字节码反序列化成对象。那么问题根源找到了,是序列化和反序列化时出了问题。

3. Protostuff序列化过程

  该问题是Protostuff序列化引起的,那么解决这个问题还得弄懂Protostuff序列化和反序列化原理。弄懂原理最好的办法就是看源码:

 1 public class ProtoStuffSerializer implements Serializer {
 2
 3     private static final Objenesis objenesis = new ObjenesisStd(true);
 4     private static final ConcurrentMap<Class<?>, Schema<?>> schemaCache = new ConcurrentHashMap<>();
 5     private ThreadLocal<LinkedBuffer> bufferThreadLocal = ThreadLocal.withInitial(() -> LinkedBuffer.allocate());
 6
 7     @Override
 8     public <T> byte[] serialize(T obj) {
 9         Schema<T> schema = getSchema((Class<T>) obj.getClass());
10
11         LinkedBuffer buf = bufferThreadLocal.get();
12         try {
13             // 实现object->byte[]
14             return ProtostuffIOUtil.toByteArray(obj, schema, buf);
15         } finally {
16             buf.clear();
17         }
18     }
19
20     @Override
21     public <T> T deserialize(byte[] bytes, Class<T> clazz) {
22         T object = objenesis.newInstance(clazz);    // java原生实例化必须调用constructor. 故使用objenesis
23         Schema<T> schema = getSchema(clazz);
24         ProtostuffIOUtil.mergeFrom(bytes, object, schema); // 反序列化源码跟踪入口
25         return object;
26     }
27
28     private <T> Schema<T> getSchema(Class<T> clazz) {
29         Schema<T> schema = (Schema<T>) schemaCache.get(clazz);
30         if (schema == null) {
31             // 把可序列化的字段封装到Schema
32             Schema<T> newSchema = RuntimeSchema.createFrom(clazz);
33             schema = (Schema<T>) schemaCache.putIfAbsent(clazz, newSchema);
34             if (schema == null) {
35                 schema = newSchema;
36             }
37         }
38         return schema;
39     }

这是我们实现Protostuff序列化工具类。接下来看一下 ProtostuffIOUtil.toByteArray(obj, schema, buf) 这个方法里面重要代码:

 1 public static <T> byte[] toByteArray(T message, Schema<T> schema, LinkedBuffer buffer)
 2     {
 3         if (buffer.start != buffer.offset)
 4             throw new IllegalArgumentException("Buffer previously used and had not been reset.");
 5
 6         final ProtostuffOutput output = new ProtostuffOutput(buffer);
 7         try
 8         {
 9            // 继续跟进去
10             schema.writeTo(output, message);
11         }
12         catch (IOException e)
13         {
14             throw new RuntimeException("Serializing to a byte array threw an IOException " +
15                     "(should never happen).", e);
16         }
17         return output.toByteArray();
18     }
1 public final void writeTo(Output output, T message) throws IOException
2     {
3         for (Field<T> f : getFields())
4             // 秘密即将揭晓
5             f.writeTo(output, message);
6     }

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) 里面重要的代码:

1 public static <T> void mergeFrom(byte[] data, T message, Schema<T> schema)
2 {
3     IOUtil.mergeFrom(data, 0, data.length, message, schema, true);
4 }
 1 static <T> void mergeFrom(byte[] data, int offset, int length, T message,
 2             Schema<T> schema, boolean decodeNestedMessageAsGroup)
 3     {
 4         try
 5         {
 6             final ByteArrayInput input = new ByteArrayInput(data, offset, length,
 7                     decodeNestedMessageAsGroup);
 8             // 继续跟进
 9             schema.mergeFrom(input, message);
10             input.checkLastTagWas(0);
11         }
12         catch (ArrayIndexOutOfBoundsException ae)
13         {
14             throw new RuntimeException("Truncated.", ProtobufException.truncatedMessage(ae));
15         }
16         catch (IOException e)
17         {
18             throw new RuntimeException("Reading from a byte array threw an IOException (should " +
19                     "never happen).", e);
20         }
21     }
 1 @Override
 2     public final void mergeFrom(Input input, T message) throws IOException
 3     {
 4         // 按顺序获取字段
 5         for (int n = input.readFieldNumber(this); n != 0; n = input.readFieldNumber(this))
 6         {
 7             final Field<T> field = getFieldByNumber(n);
 8             if (field == null)
 9             {
10                 input.handleUnknownField(n, this);
11             }
12             else
13             {
14                 field.mergeFrom(input, message);
15             }
16         }
17     }
1     public void mergeFrom(Input input, T message)
2             throws IOException
3     {
4         // 负载给字段
5         us.putObject(message, offset, input.readString());
6     }

5. 总结

  通过protostuff的序列化和反序列化源码知道一个对象序列化时是按照可序列化字段顺序把值序列化到字节码中,反序列化时也是按照当前对象可序列化字段顺序赋值。所以会出现 NATURAL_DAY 经过序列化和反序列化后变成 NATURAL_MINUTE。由于这两个字段类型是一样的,反序列化没有报错,如果序列化前的对象和反序列化接收对象对应顺序字段类型不一样时会出现反序列失败报错。为了避免以上问题,在使用protostuff序列化时,对已有的实体中添加字段放到最后去就可以了。

p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Helvetica; color: #454545 }

原文地址:https://www.cnblogs.com/wolf-bin/p/9269987.html

时间: 2024-10-11 05:16:43

Protostuff序列化问题的相关文章

protostuff序列化/反序列化

Protostuff是基于Google protobuff技术的Java版本,直接使用原生的protobuff是需要数据结构的预编译过程,需要编写.proto格式的配置文件,再通过protobuff提供的工具翻译成目标语言代码,而Protostuff动态支持了protobuff的预编译的过程,可以直接使用普通java POJO进行序列化,简化编码. 经过实测序列化性能相对原生protpbuff没有影响. 由于Protostuff只支持Java实现,不过并未对序列化格式有做任何修改,所以Proto

Protostuff 序列化

protostuff 是简化protobuf开发的java的操作工具jar . 先看看 protobuf 的使用流程: 1.先编写proto文件格式,例如 message Person {     required int32 id = 1;     required string name = 2;     optional string email = 3;   } 2.运行编译程序,生成实体类Person.javaprotoc  --java_out=./src   ./person.pr

Protostuff序列化工具类

源代码 package org.wit.ff.util; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; import com.dyuproject.protostuff.LinkedBuffer; import com.dyuproject.protostuff.ProtostuffIOUti

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

原文:http://blog.csdn.net/zhglance/article/details/56017926 google原生的protobuffer使用起来相当麻烦,首先要写.proto文件,然后编译.proto文件,生成对应的.Java文件,鄙人试了一次,发现真的很麻烦.而protostuff的官方网站(http://www.protostuff.io/documentation/runtime-schema/),对于智商比较低的小编来说也略显生涩,于是鄙人就根据项目中用到的proto

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

目录 1.背景 2.测试 2.1.环境 2.2.工具 2.3.说明 2.4.结果 2.5.结论 3.xml简单教程 3.1.准备 3.2.代码 4.protobuf简单教程 4.1.快速入门 1.下载.exe编译器 2.编写.proto文件 3.利用编译器编译.proto文件生成javabean 4.引用jar包 5.直接使用javabean自带的序列化.反序列化.提取属性等方法 5.protostuff简单教程 5.1.快速入门 1.引用jar包 2.直接使用相关序列化.反序列化语法 1.背景

基于protostuff的序列化工具类开发

[toc] 基于protostuff的序列化工具类开发 前言 前面在介绍protostuff的基本使用时(可以参考文章protostuff基本使用),都是针对某个类写的序列化和反序列化方法,显然这样不具有通用性,例如在进行远程过程调用时,传输的对象并不唯一,这时就需要开发具有通用性的序列化工具类,即不管序列化的对象是什么类型,都可以使用该工具类进行序列化.下面就来开发这样的工具类. 基于这个需要,下面会开发两个序列化工具类,一个是不具有缓存功能的SerializationUtil,一个是具有缓存

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

上一篇文章 通讯协议序列化解读(一):http://www.cnblogs.com/tohxyblog/p/8974641.html  前言:上一面文章我们介绍了java序列化,以及谷歌protobuf,但是由于protobuf的使用起来并不像其他序列化那么简单(首先要写.proto文件,然后编译.proto文件,生成对应的.java文件),所以即使他是如何的优秀,也还是没能抢占json的份额.这篇文章我们要介绍的是一款基于protobuf的java序列化协议--prorostuff,在java

java各种序列化性能测试

最近在做redis封装的过程中,需要使用序列化进行数据的传输,索性就把各种序列化方案都拿出来做了一个对比. 序列化有两种用途: 1)把对象的字节序列永久的保存在硬盘中 2)在网络上传输对象的字节序列 场景:通过模拟一个普通的POJO类进行序列化和反序列化的过程 每种情况进行1000万次的循环 不同版本JDK的模拟 序列化方案: 1.fst 2.jdk 3.kryo 4.hession 5.protostuff 6.jackson 7.fastjson 测试结果: 总结: 1.java版本升级到1

SSM框架学习之高并发秒杀业务--笔记5-- 并发优化

前几节终于实现了这个高并发秒杀业务,现在问题是如何优化这个业务使其能扛住一定程度的并发量. 一. 优化分析 对于整个业务来说,首先是分析哪些地方会出现高并发,以及哪些地方会影响到了业务的性能.可能会出现高并发的地方:详情页,获取系统时间,地址暴露接口,执行秒杀操作. 这个业务为什么要单独获取时间呢?用户会在详情页大量刷新,为了优化这里,将detal.jsp详情页和一些静态资源(css,js等)部署在CDN的节点上(至于这个CDN是什么,下面会说),也就是说用户访问详情页是不需要访问我们的系统的,