最近在项目中发现一个很奇怪的问题,将ProductDraftDO对象传输到远程服务上,远程服务获取的ProductDraftDO对象的ActionTrace为null。而在传输之前明明是有值的。ActionTrace类已经实现了序列化接口,它的所有属性都是可序列化的。
最后查明了原因,是序列化的问题。由于项目中的远程服务用dubbo实现,Hessian是dubbo的默认序列化协议,它比java的序列化性能要高很多。当hessian序列化一个对象时,默认的序列化类是com.caucho.hessian.io. JavaSerializer。
JavaSerializer的writeObject方法代码片段如下:
try {
out.writeMapBegin(cl.getName());
for (int i = 0; i < _fields.length; i++) {
Field field = _fields[i];
out.writeString(field.getName());
out.writeObject(field.get(obj));
}
out.writeMapEnd();
} catch (IllegalAccessException e) {
throw new IOException(String.valueOf(e));
}
将属性和属性值都写入到流中,这里看起来似乎没有什么问题,可是这些Field是怎么来的呢?看看JavaSerializer的构造函数:
public JavaSerializer(Class cl) {
_writeReplace = getWriteReplace(cl);
if (_writeReplace != null) _writeReplace.setAccessible(true);
ArrayList primitiveFields = new ArrayList();
ArrayList compoundFields = new ArrayList();
for (; cl != null; cl = cl.getSuperclass()) {
Field[] fields = cl.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
if (Modifier.isTransient(field.getModifiers()) || Modifier.isStatic(field.getModifiers())) continue;
// XXX: could parameterize the handler to only deal with public
field.setAccessible(true);
if (field.getType().isPrimitive() || field.getType().getName().startsWith("java.lang.")
&& !field.getType().equals(Object.class)) primitiveFields.add(field);
else compoundFields.add(field);
}
}
ArrayList fields = new ArrayList();
fields.addAll(primitiveFields);
fields.addAll(compoundFields);
_fields = new Field[fields.size()];
fields.toArray(_fields);
}
获取当前class的所有字段,接着获取父类的所有字段。序列化的时候,所有字段都放在一个ArrayList里,然后依次写入到二进制流中,反序列化的时候,所有字段放在了一个HashMap里,HashMap的key不能重复,悲剧就出现了,如果子类和父类有同名的字段就会有问题,父类的值会把子类的值覆盖掉。
看看反序列化时,JavaDeserializer的getFieldMap方法,父类字段会把子类字段覆盖掉。
/**
* Creates a map of the classes fields.
*/
protected HashMap getFieldMap(Class cl)
{
HashMap fieldMap = new HashMap();
for (; cl != null; cl = cl.getSuperclass()) {
Field []fields = cl.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
if (Modifier.isTransient(field.getModifiers()) ||
Modifier.isStatic(field.getModifiers()))
continue;
else if (fieldMap.get(field.getName()) != null)
continue;
// XXX: could parameterize the handler to only deal with public
try {
field.setAccessible(true);
} catch (Throwable e) {
e.printStackTrace();
}
fieldMap.put(field.getName(), field);
}
}
return fieldMap;
}
然后根据字段接收值,看看JavaDeserializer的readMap方法的代码片段
while (!in.isEnd()) {
Object key = in.readObject();
Field field = (Field) _fieldMap.get(key);
if (field != null) {
Object value = in.readObject(field.getType());
try {
field.set(obj, value);
} catch (Throwable e) {
IOException e1 = new IOException("Failed setting: " + field + " with " + value + "\n"
+ e.toString());
e1.initCause(e);
throw e1;
}
} else {
Object value = in.readObject();
}
}
发送二进制数据时,子类数据在前,父类数据在后。
接收二进制数据时,子类数据在前,父类数据在后。
所以对于同名字段,子类的该字段值会被赋值两次,总是被父类的值覆盖,导致子类的字段值丢失。分析完了,再回过头来看看,ProductDraftDO有一个ActionTrace类型字段actionTrace,父类ProductDO也有一个ActionTrace类型字段actionTrace。由于反序列化时,ProductDraftDO的actionTrace一直被ProductDO的actionTrace覆盖,所以ProductDraftDO的actionTrace总是空的。至于为什么父类和子类都有一个ActionTrace,这是历史原因,这里不讨论。这个问题最初由海滔发现,我对代码做了一个完整的分析。
所以,使用hessian序列化时,一定要注意子类和父类不能有同名字段。在使用dubbo时如果没有指定序列化协议,则也要注意这个问题。
建议dubbo对这个做强制检查。
分享到:
相关推荐
NULL 博文链接:https://qinghua0208.iteye.com/blog/493516
hessian序列化.pdf
介绍自己不会查吗?这里有一个点就是Hessian的序列化与反序列化与原生序列化与反序列化不同,就以使用的Resin链而言,其中的javax.naming.spi
NULL 博文链接:https://inter12.iteye.com/blog/1555678
主要通过对二者简单的实现方式的对比,介绍了Java序列化和hessian序列化的差异,具有一定参考价值,需要的朋友可以了解下。
NULL 博文链接:https://san-yun.iteye.com/blog/1688510
Hessian 2.0序列化协议规范
hessian轻量级 rpc实现
java hessian-3.0.38.jar。修改了原生的jar包,解决了hessian 序列化BigDecimal的精度问题。注意,请在hessian服务端和客户端中分别替换此jar包哦!! 只替换服务端hessian jar包还是会有问题。
hessian.jar,Hessian的序列化输出 ,
默认就是⾛ dubbo 协议,单⼀⻓连接,进⾏的是 NIO 异步通信,基于 hessian 作为序列化协议。使⽤的场景是:传输数据量⼩ (每次请求在 100kb 以内),但是并发量很⾼。 为了要⽀持⾼并发场景,⼀般是服务提供者就⼏...
a --args gadget入参,多个参数使用多次该命令传入,例-a -a Calc-p --protocol [dubbo|http] 通讯协议名称,默认缺省dubbo-s --serialization [hessian|java] 序列化类型,默认缺省hessian-t --target 目标,例:...
SOFA-Hessian 基于原生...包括:增加泛化序列化。增加 ClassNameResolver 和 ClassNameFilter 用于类名的映射、转换、过滤等。增加序列化黑名单(来自蚂蚁金服安全团队)。改进 SerializerFactory 内缓存的锁机制。
远程调用方法就是HttpInvoker:他也是将参数和返回值通过Java的序列化机制进行编组和反编组,它具有RMI的支持所有可序列化对象的优点。试使用Http协议传输二进制流的,同时又具有Hessian、Burlap(传输xml文本)的...
蚂蚁金服对 Hessian 序列化的定制版本 sofa-hessian-node 对应的 Java Hessian 版本是 v3.1.3。它对 模块做了进一步封装,通过预编译来提高性能。 安装 $ npm install sofa-hessian-node --save 示例 'use strict'; ...
-a:生成exploit下的所有payload(例如:hessian下的SpringPartiallyComparableAdvisorHolder, SpringAbstractBeanFactoryPointcutAdvisor, Rome, XBean, Resin) -t:对生成的payloads进行解码测试 -v:verbose mode...
传输协议 TCP,异步,Hessian 序列化; rmi: 采用 JDK 标准的 rmi 协议实现,传输参数和返回参数对象需要实现 Serializable 接口,使用 java 标准序列化机制,使用阻塞式短连接,传输数 据包大小混合,消费者...
NULL 博文链接:https://hbxflihua.iteye.com/blog/2231765
Hessian 2.0序列化协议规范 翻译: Edison peng 目录 1.概述 4 2.设计