序列化使用与理解
关于序列化,你是否有思考过以下问题
- 序列化之后,存了那些东西,是什么结构?
- serialVersionUID 有什么用,没有会怎么样?
- 字段增加减少后是否有兼容性问题?
本节 图解序列化的存储结构, 了解 serialVersionUID 的生成方式, 对序列化兼容性有一个更加全面的说明
基本使用
public class SerialModel implements Serializable {
private String name;
// 忽略 getter setter
}
byte [] objData;
// 使用了java7 提供的 try-with-resource 关闭流
try(ByteArrayOutputStream byteArrayInputStream = new ByteArrayOutputStream();
ObjectOutputStream objectInputStream =new ObjectOutputStream(byteArrayInputStream)){
SerialModel serialModel = new SerialModel();
serialModel.setName("Karen");
objectInputStream.writeObject(serialModel);
objData = byteArrayInputStream.toByteArray();
}
这是一个简单的小例子,将对象SerialModel
写入到ObjectOutputStream
图解序列化结构
String结构
@Test
public void testSerialString() throws IOException {
String name = "Karen Aya";
try(ByteArrayOutputStream byteArrayInputStream = new ByteArrayOutputStream();
ObjectOutputStream objectInputStream =new ObjectOutputStream(byteArrayInputStream)){
objectInputStream.writeObject(name);
byte [] result = byteArrayInputStream.toByteArray();
FileOutputStream fileOutputStream = new FileOutputStream(new File("d:/stringSerial.txt"));
fileOutputStream.write(result, 0, result.length);
fileOutputStream.close();
}
}
文件内容如下:
00000000h: AC ED 00 05 74 00 09 4B 61 72 65 6E 20 41 79 61 ; ..t..Karen Aya
ObjectOutputStream 默认写出内容
String 序列化图
- 魔数: 0xaced
- 版本: 0x0005
- 类型: (Short String)0x74,(Long String) 0x7C
- 长度: 0x0009
- 内容: Karen Aya,
4B 61 72 65 6E 20 41 79 61
基本结构非常简单,魔术和版本是由 ObjectOutputStream 对象创建时提供,与具体数据内容、类型无关
后面的类型、内容、长度 就是String类型序列化后的具体内容了
类结构
序列化实体类
public class SerialModel implements Serializable {
private String name;
// 省略 getter setter
}
序列化测试方法
@Test
public void testDecimalSimple() throws IOException {
byte [] objData;
try(ByteArrayOutputStream byteArrayInputStream = new ByteArrayOutputStream();
ObjectOutputStream objectInputStream =new ObjectOutputStream(byteArrayInputStream)){
SerialModel serialModel = new SerialModel();
serialModel.setName("Karen");
objectInputStream.writeObject(serialModel);
objData = byteArrayInputStream.toByteArray();
}
FileOutputStream fileOutputStream = new FileOutputStream(new File("d:/buf.txt"));
fileOutputStream.write(objData,0,objData.length);
fileOutputStream.close();
}
buf.txt 内容如下
偏移 16进制 字符串
00000000h: AC ED 00 05 73 72 00 13 63 6F 6D 2E 61 79 61 2E ; ..sr..com.aya.
00000010h: 53 65 72 69 61 6C 4D 6F 64 65 6C 09 9D B0 59 D6 ; SerialModel.澃Y?
00000020h: 38 AD 0C 02 00 01 4C 00 04 6E 61 6D 65 74 00 12 ; 8?...L..namet..
00000030h: 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E ; Ljava/lang/Strin
00000040h: 67 3B 78 70 74 00 05 4B 61 72 65 6E ; g;xpt..Karen
这些二进制的内容图形表达如下,具体细节分析参见序列化类结构
序列化id的生成与作用
serialVersionUID 由 ObjectStreamClass.lookup(clazz) 生成,若类中已经定义了 serialVersionUID 则会覆盖
影响 serialVersionUID 的内容:
- 类名
- 类描述符
- 类实现的接口
- 私有字段(排除static,transient)
- 类静态方法
- 类构造方法
- 非私有方法
通过上述内容生成 SHA-1 值,做如下操作生成最终的serialVersionUID值
long hash = ((sha[0] >>> 24) & 0xFF) |
((sha[0] >>> 16) & 0xFF) << 8 |
((sha[0] >>> 8) & 0xFF) << 16 |
((sha[0] >>> 0) & 0xFF) << 24 |
((sha[1] >>> 24) & 0xFF) << 32 |
((sha[1] >>> 16) & 0xFF) << 40 |
((sha[1] >>> 8) & 0xFF) << 48 |
((sha[1] >>> 0) & 0xFF) << 56;
作者尝试根据官方文档的方式,做以下操作,并未生成出默认的 serialVersionUID 完全一致的值
- 将上述内容写入DataOutputStream
- 对内容进行SHA-1加密
- 将加密的内容转换成int数组,然后进行hash计算
尝试寻找 open-jdk 的源码也未找到如何生成与jdk一致的 serialVersionUID 值。
生成类的序列化id: ObjectStreamClass.lookup(SerialModel.class).getSerialVersionUID()
oracle官方文档:oralce生成序列化id介绍
兼容性
默认
枚举,动态代理,接口的 serialVersionUID = 0L;
不定义serialVersionUID
还是以 SerialModel 为例, 若类中不定义 serialVersionUID
, 发生 包名
,类
,方法
,字段
,构造函数
改动时,完全不兼容。(仅方法内容改动不影响)
定义serialVersionUID
定义下列模型,并说明兼容情况
序列化 SerialModel 版本A 的内容到文件
- 版本B 兼容 版本A,不受增加字段影响
- 版本C 兼容 版本A,不受减少字段影响
- 版本D 不兼容 版本A,受 serialVersionUID 影响
SerialModel 版本A
public class SerialModel implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String id;
}
SerialModel 版本B,删除字段
public class SerialModel implements Serializable {
private static final long serialVersionUID = 1L;
private Integer age;
private String name;
private String id;
}
SerialModel 版本C,新增字段
public class SerialModel implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
}
SerialModel 版本D 修改serialVersionUID
public class SerialModel implements Serializable {
private static final long serialVersionUID = 2L;
private String name;
}
最佳实践
当类的改动不向前兼容时,修改版本号,并附上注释
/**
* serialVersionUID=1L,第一版本
* serialVersionUID=2L,第二版本,改动name字段
* serialVersionUID=3L,第三版本,删除字段xx,新增字段
* serialVersionUID=3L,第四版本,新增字段yy,逻辑不冲突,向前兼容
*/
private static final long serialVersionUID = 3L;
serialVersionUID 可以由IDE自动生成,也可以写成具体的数值(最小值为1)
扩展
序列化类结构
类结构中序列化数据与结构的对应内容
魔数: 0xaced
版本: 0x5
类型:Object:0x73
类型描述符
类标识符号: 0x72
全路径名: com.aya.SerialModel (String) 长度:`00 13`,内容:`63 6F 6D 2E 61 79 61 2E 53 65 72 69 61 6C 4D 6F 64 65 6C`
序列化id(serialVersionUID) 自动生成的serialVersionUID `09 9D B0 59 D6 38 AD 0C`
序列化标识位(byte): 2:SC_SERIALIZABLE (4:SC_EXTERNALIZABLE 2:SC_SERIALIZABLE 1:SC_WRITE_METHOD 16:SC_ENUM)
字段
字段长度 (short)`00 01`
遍历字段
字段类型 (byte) L: 字符串 `4C`
字段名称 (String) 字段长度:`00 04` 字段内容:`6E 61 6D 65`
字段类型全称 (String) Ljava/lang/String; `74` 字符串标识,`00 12` 字符串长度,字符串内容`4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B`
序列化结束符 `78`
父类描述符 NULL:`70` ,(内容与类型描述符一致,本例的父类是Object,所以内心描述符写入NULL)
字段内容: `74` 字符串标识,`00 05` 字符串长度,字符串内容`4B 61 72 65 6E`
写入序列化数据
基本类型直接写入,其他类型地柜调用序列化写入
类型: TC_REFERENCE 0x71
写入处理器: 0x7e0000+3