前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Protostuff序列化问题

Protostuff序列化问题

作者头像
GreizLiao
发布2019-09-23 15:23:57
2K0
发布2019-09-23 15:23:57
举报
文章被收录于专栏:足球是圆的足球是圆的

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

1. 问题描述

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

代码语言:javascript
复制
 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 :

代码语言:javascript
复制
 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序列化和反序列化原理。弄懂原理最好的办法就是看源码:

代码语言:javascript
复制
 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) 这个方法里面重要代码:

代码语言:javascript
复制
 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     }
代码语言:javascript
复制
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这里面才是关键:

代码语言:javascript
复制
@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) 里面重要的代码:

代码语言:javascript
复制
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 }
代码语言:javascript
复制
 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     }
代码语言:javascript
复制
 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     }
代码语言:javascript
复制
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} -->

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018-07-05 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 问题描述
  • 2. 定位问题
  • 3. Protostuff序列化过程
  • 4. Protostuff反序列化过程
  • 5. 总结
相关产品与服务
NAT 网关
NAT 网关(NAT Gateway)提供 IP 地址转换服务,为腾讯云内资源提供高性能的 Internet 访问服务。通过 NAT 网关,在腾讯云上的资源可以更安全的访问 Internet,保护私有网络信息不直接暴露公网;您也可以通过 NAT 网关实现海量的公网访问,最大支持1000万以上的并发连接数;NAT 网关还支持 IP 级流量管控,可实时查看流量数据,帮助您快速定位异常流量,排查网络故障。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档