前言
就在昨天和同事聊天聊起了序列化,我们熟知并且使用最方便的就是Serializable。
那么为什么要序列化呢?
有些朋友会说:序列化主要是为了数据持久化。
我们都知道Serializable是一个空接口,不需要我们实现任何的方法,设置可以不设置serialVersionUID,那他存在的意义是什么呢,直接让所有的类都可以序列化不是更简单吗,带着这样的思考,我开始研究Serializable的相关的源码。
正文
首先我们看一下Serializable的注释,非常的长,我简单的概括为一下几点:
- 慎用Serializable
- Serializable接口仅仅是为了标识哪些类可以被序列化。
- 如果想要序列化一个类,必须要实现Serializable接口,包括他的属性。
- 如果父类实现了Serializable,那么他的子类也可以被序列化。
- 子类只可以序列化父类的可见属性,例如public,protected,或者其他情况,并且必须提供一个无参构造方法,否则会在运行时报错。
- serialVersionUID可以理解为版本号,如果发生了变化,会抛出InvalidClassException。
- 强烈建议手动设置serialVersionUID,兼容类发生的变化,如果没有手动设置serialVersionUID,会根据系统算法默认生成一个serialVersionUID。
- 系统生成的serialVersionUID会因为各种原因发生变化,例如类的属性或方法变化,SDK版本的变化等等,所以请注意处理异常。
- 强烈建议使用JSON,简洁性,可读性,效率都会有所提高。
通过注释,我们可以理解Serializable的作用是为了帮助我们标识哪些类的实例可以被序列化,并且提供了serialVersionUID作为版本号帮助我们兼容类的改变。
之前我们提到了一个问题:如果去掉Serializable,所有的类默认可以直接序列化不行吗?
我个人是这么理解的:
1、从设计模式的六大准则之一,单一职责原则。一个类做越少的事就越好管理,如果所有的类都可以直接序列化,当我们想知道程序中到底序列化了哪些类,就没有办法进行查找和筛选。
2、Java只有单继承,导致很多情况下不够灵活,接口的出现大大弥补的了这一空缺,通过接口进行类型区分是非常方便的做法。
3、最关键的注释已经写的很清楚了,使用Serializable进行序列化是一个非常谨慎的行为(各种异常),限制类的序列化是为了保证程序的安全运行。
经过思考后,我们对Serializable的起源有了新的升华,接下来我们看看Serializable的具体使用源码。
ObjectOutputStream
ObjectOutputStream通过IO流的方式把对象写入到指定的位置,例如以下代码:
// 写入Student对象到指定路径的文件中
ObjectOutputStream(FileOutputStream(path))
.apply {
writeObject(Student("1", "zhangsan"))
flush()
close()
}
我们先看看文件中到底写了什么东西:
这是从内存读出出来的Student对象相关的信息,我们可以看到Class类名,属性类型和名称以及对应的值。
接下来我们看看ObjectOutputStream是怎么具体写入文件的,因为源码非常的多,我们只截取部分关键的代码。写入对象到IO流肯定要看writeObject方法,根据代码定位最终我们会在writeObject0()方法中看到第一个关键代码:
// 你会看到通过对象的Class创建了ObjectStreamClass
// ObjectStreamClass会保存类相关的信息,包括获取serialVersionUID
// 之后也会调用ObjectStreamClass的write方法进行写入
ObjectStreamClass desc = ObjectStreamClass.lookup(cl, true);
...
// 此处代码判断对象的类型是否可以被序列化
// Class
if (obj instanceof Class) {
writeClass((Class) obj, unshared);
}
// ObjectStreamClass
else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
}
// String
else if (obj instanceof String) {
writeString((String) obj, unshared);
}
// 数组
else if (cl.isArray()) {
writeArray(obj, desc, unshared);
}
// 枚举
else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
}
// Serializable实现序列化接口
else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
}
// 其他情况都会抛出异常
else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
以刚才Student为例,他的类型是Object(而不是Class),如果我们没有实现Serializable接口,这里就会抛出异常。所以这里会执行writeOrdinaryObject方法,在writeOrdinaryObject方法中,就会调用ObjectStreamClass的写入方法:
private void writeOrdinaryObject(Object obj,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
...
try {
desc.checkSerialize();
// 开始标记位
bout.writeByte(TC_OBJECT);
// 调用ObjectStreamClass的写入方法
writeClassDesc(desc, false);
handles.assign(unshared ? null : obj);
// 判断是否实现了Externalizable接口,我们没有实现,所以不会走这里
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);
} else {
// 写入序列化数据
writeSerialData(obj, desc);
}
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
从上面的方法里,已经包含了所有的写入流程:
1、写入开始标记位
2、开始写入ObjectStreamClass中的内容
3、写入序列化数据
我们看一下第二步的所有核心内容:
// 首先会执行方法writeNonProxyDesc方法
private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
throws IOException
{
// 描述符,表示这个一个类的描述
bout.writeByte(TC_CLASSDESC);
...
// 虽然这里有一个判断,实际上执行的还是 desc.writeNonProxy(this);
// 应该是一个等待兼容修改的方法
if (protocol == PROTOCOL_VERSION_1) {
// do not invoke class descriptor write hook with old protocol
desc.writeNonProxy(this);
} else {
writeClassDescriptor(desc);
}
...
// 写入结束block data
bout.writeByte(TC_ENDBLOCKDATA);
//
writeClassDesc(desc.getSuperDesc(), false);
}
// 写入ObjectOutputStream,此处用到了serialVersionUID
void writeNonProxy(ObjectOutputStream out) throws IOException {
// 写入类的名称
out.writeUTF(name);
// 写入SerialVersionUID
out.writeLong(getSerialVersionUID());
// 写入其他标记,这里就直接省略了
...
// 开始写入属性
out.writeShort(fields.length);
for (int i = 0; i < fields.length; i++) {
ObjectStreamField f = fields[i];
out.writeByte(f.getTypeCode());
out.writeUTF(f.getName());
if (!f.isPrimitive()) {
out.writeTypeString(f.getTypeString());
}
}
}
如果我们没有设置serialVersionUID怎么办,系统会为我们自动生成serialVersionUID,具体请查看ObjectStreamClass.computeDefaultSUID(),算法中与类的名称、属性、方法都有关系,所以我们随意的修改都可能导致计算出不同的serialVersionUID。
最后我们回到之前的第三步:
// 写入序列化方法
private void writeSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
// 得到继承结构,开始遍历
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
// 如果我们在类中自己定义了writeObject,会被调用,并进入到下面的代码
if (slotDesc.hasWriteObjectMethod()) {
...
}
// 如果没有自定义writeObject,会进入else
else {
// 此方法中会把所有的属性都写入进去,属性的类型也会重新调用最开始的writeObject放方法
defaultWriteFields(obj, slotDesc);
}
}
}
到这里我们的序列化写入就结束了,我们以示例代码序列化Student到文件,回顾一下整个流程:
ObjectInputStream
弄懂了输出流,我们再分析一下输入流,之前我们已经弄懂了序列化的流程,所以我们可以推断,反序列化的过程应该是相反的。
// 读出文件出的对象
ObjectInputStream(FileInputStream(path))
.apply {
text.text = readObject().toString()
close()
}
首先看一下readObject()方法,首先我们在readObject0()方法中看到了反序列化的类型分支:
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}
case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
我们写入的类型是TC_OBJECT,最终会跟踪到readOrdinaryObject()方法:
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
// 读取序列化中的ObjectStreamClass
ObjectStreamClass desc = readClassDesc(false);
...
// 通过反射,创建对象
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
...
}
// 判断是否实现了Externalizable接口
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
}
// 我们并没有实现Externalizable接口,会进入到else判断
else {
// 读取序列化的数据
readSerialData(obj, desc);
}
// 判断是否自定义了readObject方法,如果有会调用,如果没有直接返回obj
// 此处省略
...
return obj;
}
上面方法是反序列化的全部流程,我重点看一下读取ObjectStreamClass的readClassDesc()方法和读取序列化数据的readSerialData()方法。
private ObjectStreamClass readNonProxyDesc(boolean unshared)
throws IOException
{
...
// 创建ObjectStreamClass
ObjectStreamClass desc = new ObjectStreamClass();
// 读取ObjectStreamClass
ObjectStreamClass readDesc = null;
try {
readDesc = readClassDescriptor();
} catch (ClassNotFoundException ex) {
throw (IOException) new InvalidClassException(
"failed to read class descriptor").initCause(ex);
}
// 各种异常处理
...
// 通过读取到ObjectStreamClass初始化desc
desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
...
// 返回desc
return desc;
}
读取ObjectStreamClass最终会定位到readNonProxy()方法:
void readNonProxy(ObjectInputStream in)
throws IOException, ClassNotFoundException
{
// 类名
name = in.readUTF();
// suid
suid = Long.valueOf(in.readLong());
// 各种flag
...
// 循环保存属性到数组中
for (int i = 0; i < numFields; i++) {
char tcode = (char) in.readByte();
String fname = in.readUTF();
String signature = ((tcode == 'L') || (tcode == '[')) ?
in.readTypeString() : new String(new char[] { tcode });
try {
fields[i] = new ObjectStreamField(fname, signature, false);
} catch (RuntimeException e) {
...
}
}
computeFieldOffsets();
}
现在已经把所有的属性保存到数组中了,接下来调用initNonProxy()方法,这个方法中主要做了很多的判断,用现有的Class和读取到的Class进行对比,例如判断serialVersionUID是否一致,否则会抛出InvalidClassException异常。
if (model.serializable == osc.serializable && !cl.isArray() && suid != osc.getSerialVersionUID()) {
throw new InvalidClassException(osc.name,
"local class incompatible: " +
"stream classdesc serialVersionUID = " + suid +
", local class serialVersionUID = " +
osc.getSerialVersionUID());
}
经过读取ObjectStreamClass,我们已经得到了初始化对象的所有类型信息,接下来是如何把对对象的属性赋值。
private void readSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
// 遍历继承列表,ClassDataSlot中保存的每个层级父类的信息
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
// 判断这个属性是否有值
if (slots[i].hasData) {
// 忽略这个属性
if (obj == null || handles.lookupException(passHandle) != null) {
defaultReadFields(null, slotDesc); // skip field values
}
// 是否自定义了readObject方法,此处忽略
else if (slotDesc.hasReadObjectMethod()) {
...
}
// 默认会进入到这里
else {
defaultReadFields(obj, slotDesc);
}
...
}
// 判断在无值的情况下,是否自定了readObjectNoData方法
// 此处忽略
else {
...
}
}
}
// 对属性进行赋值操作
private void defaultReadFields(Object obj, ObjectStreamClass desc)
throws IOException
{
// 异常检查等操作
...
// 开始遍历类的属性列表
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
for (int i = 0; i < objVals.length; i++) {
ObjectStreamField f = fields[numPrimFields + i];
// 注意此处会回到最开始的反序列化位置,完成属性值的反序列化
objVals[i] = readObject0(f.isUnshared());
if (f.getField() != null) {
handles.markDependency(objHandle, passHandle);
}
}
// 通过反射进行赋值
if (obj != null) {
desc.setObjFieldValues(obj, objVals);
}
passHandle = objHandle;
}
到这里反序列化操作到此结束,我们整理一张流程图,梳理一下反序列化的整个流程:
总结
今天我们一起讨论了序列化接口Serializable的起源和设计理念,然后通过分析源码了解了Serializable的在序列化和反序列化中的作用。我们也看到了Serializable接口的不足:
- 太多的循环和递归遍历,读写效率确实有隐患。
- 不能有选择的对属性序列化和反序列,灵活性差。
- 兼容性差,不指定serialVersionUID很容易出现反序列的崩溃问题。
正如Serializable开头所说的:慎用Serializable,推荐使用JSON。
补充
评论里叶落清秋提到了@Transient注解,他可以在序列化中禁止某些属性被序列化,我重新查看了一遍源码,之前我只重点看了write和read的过程,所以并没有看到这个注解,重新整理发现,在ObjectStreamClass的构造函数中就已经完成了@Transient的功能,代码如下:
// 私有构造函数
if (serializable) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
...
// 得到可以序列化的属性的集合
fields = getSerialFields(cl);
...
}
}
private static ObjectStreamField[] getSerialFields(Class<?> cl) throws InvalidClassException
{
ObjectStreamField[] fields;
...
// 进入到此方法中
fields = getDefaultSerialFields(cl);
...
return fields;
}
private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
Field[] clFields = cl.getDeclaredFields();
ArrayList<ObjectStreamField> list = new ArrayList<>();
int mask = Modifier.STATIC | Modifier.TRANSIENT;
for (int i = 0; i < clFields.length; i++) {
// 遍历所有的属性,过滤掉static和transient
if ((clFields[i].getModifiers() & mask) == 0) {
list.add(new ObjectStreamField(clFields[i], false, true));
}
}
int size = list.size();
return (size == 0) ? NO_FIELDS :
list.toArray(new ObjectStreamField[size]);
}
经过这次整理,我们知道了Serializable是不会序列化静态属性和@Transient注解的属性,感谢叶落清秋的评论指正。