JDK序列化
实现Serializable接口
被序列化的Object中的 Object属性必须实现 Serializable接口,否则报异常, 基本类型默认可以序列化。static 和transient关键字修饰的变量不会被实例化
父类A没有实现Serializable 接口,子类B实现了Serializable 接口, A a=new B() 此时a可以序列化
A a=new A() 此时 a不可以序列化,也就是 被序列化的是指向的是实现了 Serializable 接口的实例才可以。
父类A没有实现Serializable 接口,子类B实现了Serializable 接口 序列化类B的实例,不会序列化父类A,就是 父类实例的数据不会被写入,反序列化后 获取不到父类的数据。 反序列化会调用 父类无参的构造方法。
序列化格式
先写类描述 包含类,序列化id,所有字段的描述,然后写数据
类描述
格式:类型标识1个byte+类描述开始标识1个byte + 类全名+SerialVersionUID+flags+字段个数2byte+字段列表描述+类描述结束标识
字段描述:类型简写1个byte+ 字段名+类型描述 (Object类型需要写类型描述,如Ljava/lang/String)
Object类型标识:TC_OBJECT=115 类描述开始标识:TC_CLASSDESC=114
flags: 用未标识 实现了Serializable接口和标识是枚举
范例如下
115 114 com.changqun.demo.serialization.User 1 2 2 I age L name Ljava/lang/String; 120
说明:User 后面的1是SerialVersionUID long类型。 I 和L 都是类型简写,非基本类型要写类型名, 120 是 类描述结束标识。
然后 没有父类写入空标识 112,如果有父类,写父类描述,迭代调用直到找不父类,如此完成所有类描述的写入。
总结: 写入完整的类描述信息,包含类名 ,字段名,字段类型。先写子类,后写父类。
写数据
写数据部分就是 写字段值列表
有父子类关系的 先写父类数据,后写子类数据
数据部分 先写 基本类型,后写Object类型。
计算出所有基本类型字段的总长度,单位 byte, 创建一个这个长度的byte数组,将这些字段的值存入。 按照字段在Object类中出现的顺序。
(ObjectStreamClass.lookup(Class<?>, boolean) 解析类结构,
创建代表类结构的工具类)
Object 如果是String,isArray,Enum按照固定格式写入,否则必须实现Serializable ,调用写入Object方法(相当于迭代调用)。
ObjectOutputStream.writeObject0(Object, boolean)
//String 类型 先写长度再写值
writeString((String) obj, unshared);
集合类型字段的写入
把集合类型当做Object 类型 迭代调用ObjectOutputStream.writeObject0(Object, boolean) 以HashMap 为例,
使用反射调用 了它的writeObject方法 ,ArrayList也是如此。
ObjectStreamClass.lookup(Class<?>, boolean) line: 357
//这里创建并缓存了 类结构辅助类。
try {
entry = new ObjectStreamClass(cl);
} catch (Throwable th) {
entry = th;
}
if (future.set(entry)) {
Caches.localDescs.put(key, new SoftReference<Object>(entry));
}
总结: 先写父类数据,后写子类数据,先写所有基本类型,后写入所有object类型,Object 如果是String,isArray,Enum按照固定格式写入,否则必须实现Serializable ,调用写入Object方法(相当于迭代调用),把集合类型当做Object 类型。以HashMap 为例,使用反射调用 了它的writeObject方法 ,ArrayList也是如此。
写入和解析基本类型
共同步骤:解析出 字段个数,每个字段在对象内位移值,每个字段相对应存值数组的开始位置,每个字段的类型。把集合类型当做Object 类型 迭代调用ObjectOutputStream.writeObject0(Object, boolean)
然后使用反射调用 了HashMap,ArrayList 的 writeObject方法
写入基本类型字段过程
写入的时候根据固定顺序读出字段在对象内位移值,字段相对应存值数组的开始位置,字段的类型 ,根据不同的类型 unsafe.getXXX获取值,Bits.putXXX写入
以int和long类型为例
case 'I':
Bits.putInt(buf, off, unsafe.getInt(obj, key));
case 'J':
Bits.putLong(buf, off, unsafe.getLong(obj, key));
break;
off 代表字段在对象内位移值
解析基本类型字段过程
解析的时候根据固定顺序读出字段在对象内位移值,字段相对应存值数组的开始位置,字段的类型 ,根据不同的类型 Bits.getXXX获取值 Unsafe.putXXX赋值。
以int和long类型为例
case 'I':
unsafe.putInt(obj, key, Bits.getInt(buf, off));
break;
case 'J':
unsafe.putLong(obj, key, Bits.getLong(buf, off));
break;
off 同时也代表对应类型在 基本类型字节数组的偏移量
创建对象调用 class.getInstance()
反序列化
先解析描述,后解析数据
解析描述时,先解析 子类,后解析父类。
解析数据时,先解析 父类,后解析子类
流程基本与序列化顺序一致,不同的是 代表类结构信息的 ObjectStreamClass是从序列化的后的数据读出并解析后创建。,而序列化过程中ObjectStreamClass是解析对象类创建的
注意JDK 的反序列化 只有 result= input.readObject(); 没有可以传入参数类型的
如果是对象字段,迭代调用。
Object是主要的读取流程, 如果是String,Array,Enum按照固定格式读取。
Hessian序列化
序列化
序列化结果如下
格式:MT+ 0+ 类全名长度 +类全名 + [字段列表]+ 结束标志 z(小写)
字段列表:字段名+字段类型+字段值
字段类型+字段值 是单独解析的,不同类型,实现不同,以Int和String类型为例
Int类型:字段类型 I + 字段值 int类型 32位,拆分为4个字节存储, 每个字节存储8位。
String类型 : 字段类型 S+ 长度9-16位+长度后8位 + 字符数组。
UnsafeSerializer.writeObject10(Object, AbstractHessianOutput) line: 187
protected void writeObject10(Object obj, AbstractHessianOutput out)
throws IOException
{
for (int i = 0; i < _fields.length; i++) {
Field field = _fields[i];
out.writeString(field.getName());//字段名
_fieldSerializers[i].serialize(out, obj);//字段值
}
hessian1中字段名 字段类型 字段值是个联系并循环的顺序。
序列化流程如下图
第一次serializer.writeObject(object, this) serializer 类型是UnsafeSerializer(执行Object 所有字段的序列化,其中Object 类型的字段 迭代调用 out.writeObject(value);)。
过程中会创建_fieldSerializers,其中的每一个元素都是FieldSerializer的子类,解析 字段生成的 。这个数组非常重要,字段的写入就是完全调用其write方法 完成的。
以后写Object 类型会创建不同的Serializer子类,如MapSerializer。
创建 get set 都使用了unsafe.
protected Object instantiate()
throws Exception
{
return _unsafe.allocateInstance(_type);
}
每一个类型都有独立的序列化类,都实现了FieldSerializer
包括基本类型 Boolean Byte Char Double Float Int Long Short
Object类型:Object String Date
其中 ObjectFieldSerializer serialize 调用 out.writeObject(value);
Date类型实现: 存储时间戳
out.writeUTCDate(value.getTime());
public void writeUTCDate(long time)
throws IOException
{
os.write('d');
os.write((byte) (time >> 56));
os.write((byte) (time >> 48));
os.write((byte) (time >> 40));
os.write((byte) (time >> 32));
os.write((byte) (time >> 24));
os.write((byte) (time >> 16));
os.write((byte) (time >> 8));
os.write((byte) (time));
}
注意如果有多个 Object字段 会创建多个 ObjectFieldSerializer。
有些类型使用了比较大的类型存储,如char 调用writeString short调用WriteInt float 调用writeDouble
不知原因。
生成的Serializer子类会被缓存。
Map类型:MapSerializer 执行serializer.writeObject(object, this); 格式: M T null [key value]列表
List类型:CollectionSerializer执行serializer.writeObject(object, this); 格式:V l 4字节长度 [value]列表
写入的类名 读出后 用于创建 类结构解析器Deserializer,字段解析器, 创建 Object实例
创建Objects使用了_unsafe.allocateInstance
protected Object instantiate()
throws Exception
{
return _unsafe.allocateInstance(_type);
}
反序列化
创建对象 set 各个类型value 都使用了unsafe.
与序列化相同 多个类型都实现了FieldDeserializer2,还多了 SqlDate SqlTimeStamp类型实现.
创建FieldDeserializer2子类的逻辑
当前类作为初始值,循环后获取父类作为当前类
获取当前类的declaredFields,循环创建 FieldDeserializer2子类,如果是静态或者是transient 类型则跳过。(declaredFields数组的顺序按照字段在对象内书写顺序,创建的子类数组中,前面是子类的字段,后面是父类的字段)
细节: 传入field获取字段offset.
以IntFieldDeserializer为例
IntFieldDeserializer
IntFieldDeserializer(Field field)
{
_field = field;//save field
_offset = _unsafe.objectFieldOffset(_field);//获取字段offset
}
public void deserialize(AbstractHessianInput in, Object obj)
throws IOException
{
int value = 0;
try {
value = in.readInt();//读取 特定类型值
_unsafe.putInt(obj, _offset, value);// 为对象字段赋值。
} catch (Exception e) {
logDeserializeError(_field, obj, value, e);
}
}
}
序列化和反序列化创建的Serializer,Deserializer子类都会被缓存。
hessian2序列化
格式:C0 类全名长度 类全名 字段长度 字段名列表 [字段值列表] 结束标志 z(小写)
字段值列表: 字段类型 字段值
byte范围:-128 到 127
相比hessian改进点
- 序列化后的byte值先缓存起来,后面一次性写入
- 优化数值类型的存储格式,尽量使用少的byte存储。
数值类型中为实现尽量使用少的byte存储,使用了阀值。
如果不使用阀值 当读取一个byte后无法是否还需要再读取后面的byte来拼接数值。
写字段定义
字段个数 [字段字符串列表]
字段字符串: 为空 写N,长度,字符数组。
长度:int 类型 根据大小 使用 1~3个字节存储 长度小于等于31,占一个字节。 长度小于等于1023,占2个字节,长度大于1023 第一个字节写 大写字母 S 值,后面2字节写长度
字符: 根据字符值大小 使用 1~3个字节存储
ASCII:定义了 包含英文字母,数字,各种常用符号 128个字符。
字符值在 128以内(不含128)都用1个字节存储。
有些语言一个字符值范围不够 如法语,汉字(汉字占2字节)
序列化方式JDK比 hessian性能差的原因
- JDK序列化生成的byte数组较长,每个非基本类型字段描述都存了完整名 如Ljava/util/Map ,Ljava/lang/String。 hessian的类型描述使用了标识符
- JDK序列化集合类型使用反射调用writeObject方法