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 ...
随机推荐
- JAVA MQ API方式通信采用Binding MQ Server方式
package com.mqapi; /** * @modified by actorai E-mail:actorai@163.com * @version 创建时间:2010-9-15 * ...
- Prometheus 集成 Node Exporter
文章首发于公众号<程序员果果> 地址:https://mp.weixin.qq.com/s/40ULB9UWbXVA21MxqnjBxw 简介 Prometheus 官方和一些第三方,已经 ...
- 天天都用消息队列,却不知道为啥要用MQ,这就有点尴尬了
1.为什么要使用消息队列? 分析:一个用消息队列的人,不知道为啥用,有点尴尬.没有复习这点,很容易被问蒙,然后就开始胡扯了. 回答:这个问题,咱只答三个最主要的应用场景(不可否认还有其他的,但是只答三 ...
- Shiro权限注解原理
概述 前不久刚学会使用权限注解(),开始思索了一番.最开始猜测实现方式是注解@Aspect,具体实现方式类似如下所示(切面记录审计日志).后来发现并非如此,所以特地分析一下源码. @Component ...
- 手撕ThreadPoolExecutor线程池源码
这篇文章对ThreadPoolExecutor创建的线程池如何操作线程的生命周期通过源码的方式进行详细解析.通过对execute方法.addWorker方法.Worker类.runWorker方法.g ...
- c中自增自减的妙用
#include <stdio.h> int main() { ; printf("%d,%d,%d,%d",i++;i--;++i;--i); /*运算从右往左运算 ...
- Linux下复位USB设备
有时候USB设备出错,这时我们希望通过软件复位一下USB设备,可以参考下面这段代码: #include <stdio.h> #include <unistd.h> #inclu ...
- JVM(十二):方法调用
JVM(十二):方法调用 在 JVM(七):JVM内存结构 中,我们说到了方法执行在何种内存结构上执行:Java 方法活动在虚拟机栈中的栈帧上,栈帧的具体结构在内存结构中已经详细讲解过了,下面就让我们 ...
- JS中 isNaN() 方法解析
1. isNaN() 存在的意义 由于 NaN 是唯一一个不等于自身的值,不像其他的值,可以用相等操作符来判断是否等于自身,NaN == NaN和NaN === NaN都会返回false,所以isNa ...
- js获取Cookie,获取url参数
function getCookie(name) { var strCookie = document.cookie; var arrCookie = strCookie.split("; ...