序列化
什么是序列化与反序列化
序列化是将对象转换为可传输格式的过程。是一种数据的持久化手段。一般广泛应用于网络传输,RMI和RPC等场景中。
反序列化是序列化的逆操作。
序列化是将对象的状态信息转换为可存储或传输的形式的过程,一般是以字节码或XML格式传输。而字节码或XML编码格式可以还原为完全相等的对象。这个相反的过程称为反序列化。
Java如何实现序列化和反序列化
Java对象的序列化与反序列化
在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用该对象。但是,我们创建出来的这些Java对象都是存在于JVM的堆内存中的。只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止运行,这些对象的状态也就随之而丢失了。
但是在真实的应用场景中,我们需要将这些对象持久化下来,并且能够在需要的时候把对象重新读取出来。Java的对象序列化可以帮助我们实现该功能。
对象序列化机制(object serialization) 是Java 语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。对象序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。
在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。
相关接口及类
Java为了方便开发人员将Java对象进行序列化及反序列化提供了一套方便的API支持。其中包括以下接口和类:
- java.io.Serializable
- java.io.Externalizable
- ObjectOutput
- ObjectInput
- ObjectOutputStream
- ObjectInputStream
Serializable 接口
类通过实现java.io.Serializable
接口以启动其序列化功能。未实现此接口的类将无法使用其任何状态序列化或反序列化。可序列化类的所有子类型本身都说可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。
当试图对一个对象进行序列化的时候,如果遇到不支持Serializable 接口的对象。在此情况下,将抛出NotSerializableException
。
如果要序列化的类有父类,要想同时将在父类中定义过的变量持久化下来,那么父类也应该继承java.io.Serializable
接口。
下面是一个实现了java.io.Serializable
接口的类
package com.hollischaung.serialization.SerializableDemos;
import java.io.Serializable;
/**
* Created by hollis on 16/2/17.
* 实现Serializable接口
*/
public class User1 implements Serializable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
通过下面的代码进行序列化及反序列化
package com.hollischaung.serialization.SerializableDemos;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.io.*;
/**
* Created by hollis on 16/2/17.
* SerializableDemo1 结合SerializableDemo2说明 一个类要想被序列化必须实现Serializable接口
*/
public class SerializableDemo1 {
public static void main(String[] args) {
//Initializes The Object
User1 user = new User1();
user.setName("hollis");
user.setAge(23);
System.out.println(user);
//Write Obj to File
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(oos);
}
//Read Obj from File
File file = new File("tempFile");
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
User1 newUser = (User1) ois.readObject();
System.out.println(newUser);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(ois);
try {
FileUtils.forceDelete(file);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//OutPut:
//User{name='hollis', age=23}
//User{name='hollis', age=23}
Externalizable 接口
除了Serializable 之外,java中还提供了另一个序列化接口Externalizable
为了了解Externalizable接口和Serializable接口的区别,先来看代码,我们把上面的代码改成使用Externalizable的形式。
package com.hollischaung.serialization.ExternalizableDemos;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
/**
* Created by hollis on 16/2/17.
* 实现Externalizable接口
*/
public class User1 implements Externalizable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void writeExternal(ObjectOutput out) throws IOException {
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
package com.hollischaung.serialization.ExternalizableDemos;
import java.io.*;
/**
* Created by hollis on 16/2/17.
*/
public class ExternalizableDemo1 {
//为了便于理解和节省篇幅,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
//IOException直接抛出
public static void main(String[] args) throws IOException, ClassNotFoundException {
//Write Obj to file
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
User1 user = new User1();
user.setName("hollis");
user.setAge(23);
oos.writeObject(user);
//Read Obj from file
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
User1 newInstance = (User1) ois.readObject();
//output
System.out.println(newInstance);
}
}
//OutPut:
//User{name='null', age=0}
通过上面的实例可以发现,对User1类进行序列化及反序列化之后得到的对象的所有属性的值都变成了默认值。也就是说,之前的那个对象的状态并没有被持久化下来。这就是Externalizable接口和 Serializable 接口的区别:
Externalizable 继承了Serializable,该接口中定义了两个抽象方法:writeExternal()
与 readExternal()
。当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()
与 readExternal()
方法。由于上面的代码中,并没有在这两个方法中定义序列化实现细节,所以输出的内容为空。还有一点值得注意:在使用Externalizable进行序列化的时候,在读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存的对象的字段的值分别填充到新对象中。所以,实现Externalizable接口的类必须要提供一个public 的无参构造器。
按照要求修改之后的代码如下:
package com.hollischaung.serialization.ExternalizableDemos;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
/**
* Created by hollis on 16/2/17.
* 实现Externalizable接口,并实现writeExternal和readExternal方法
*/
public class User2 implements Externalizable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
package com.hollischaung.serialization.ExternalizableDemos;
import java.io.*;
/**
* Created by hollis on 16/2/17.
*/
public class ExternalizableDemo2 {
//为了便于理解和节省篇幅,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
//IOException直接抛出
public static void main(String[] args) throws IOException, ClassNotFoundException {
//Write Obj to file
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
User2 user = new User2();
user.setName("hollis");
user.setAge(23);
oos.writeObject(user);
//Read Obj from file
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
User2 newInstance = (User2) ois.readObject();
//output
System.out.println(newInstance);
}
}
//OutPut:
//User{name='hollis', age=23}
这次,就可以把之前的对象状态持久化下来了。
如果User类中没有无参数的构造函数,在运行时会抛出异常:
java.io.InvalidClassException
Serializable 和 Externalizable 有何不同
Java 的类通过实现java.io.Serializable
接口以启用其序列化功能。未实现此接口的类将无法使用其任意状态序列化或反序列化。
可序列化的所有子类型本身都是可序列化的。
序列化接口没有方法或者字段,仅用于标识可序列化的定义。
当试图对一个对象进行序列化的时候,如果遇到不支持Serializable
接口的对象。在此情况下,将抛出NotSerializableException
。
如果要序列化的类有父类,要想同时将在父类中定义过的变量持久化下来,那么父类应该也继承 java.io.Serializable
接口。
Externalizable
继承了Serializable
,该接口定义了两个抽象方法:writeExternal()
与 readExternal()
.当使用Externalizable
接口进行序列化与反序列化的时候需要开发人员重写writerExternal()与readExternal()方法。
如果没有在这两个方法中定义序列化实现细节,那么序列化之后,对象内容为空。
实现Externalizable
接口的类必须要提供一个public
的无参构造器。
所以,实现Externalizable
,并实现writeExternal()
和 readExternal()
方法可以指定序列化哪些属性。
serialVersionUID
序列化就是将对象的状态信息转换为可存储或传输的形式的过程。
我们都知道, Java对象是保存在JVM的堆内存中的, 也就是说, 如果JVM堆不存在了, 那么对象也就跟着消失了。
⽽序列化提供了⼀种⽅案, 可以让你在即使JVM停机的情况下也能把对象保存下来的⽅案。 就像我们平时⽤的U盘⼀样。 把Java对象序列化成可存储或传输的形式( 如⼆进制流) , ⽐如保存在⽂件中。 这样, 当再次需要这个对象的时候, 从⽂件中读取出⼆进制流, 再从⼆进制流中反序列化出对象。
但是, 虚拟机是否允许反序列化, 不仅取决于类路径和功能代码是否⼀致, ⼀个⾮常重要的⼀点是两个类的序列化 ID 是否⼀致, 即serialVersionUID
要求⼀致。
在进⾏反序列化时, JVM会把传来的字节流中的serialVersionUID
与本地相应实体类的serialVersionUID
进⾏⽐较, 如果相同就认为是⼀致的, 可以进⾏反序列化, 否则就会出现序列化版本不⼀致的异常, 即是InvalidCastException
。
这样做是为了保证安全, 因为⽂件存储中的内容可能被篡改。
当实现java.io.Serializable
接口的类没有显式地定义⼀个serialVersionUID
变量时候, Java序列化机制会根据编译的Class⾃动⽣成⼀个serialVersionUID
作序列化版本⽐较⽤, 这种情况下, 如果Class⽂件没有发⽣变化, 就算再编译多 次, serialVersionUID也不会变化的。
但是, 如果发⽣了变化,那么这个⽂件对应的serialVersionUID
也就会发⽣变化。
基于以上原理, 如果我们⼀个类实现了Serializable
接口, 但是没有定义serialVersionUID
, 然后序列化。 在序列化之后, 由于某些原因, 我们对该类做了变更, 重新启动应⽤后, 我们相对之前序列化过的对象进⾏反序列化的话就会报错
为什么serialVersionUID不能随便改
关于serialVersionUID
。这个字段到底有什么用?如果不设置会怎么样?为什么《阿里巴巴Java开发手册》中有以下规定:
背景知识
Serializable 和 Externalizable
类通过实现java.io.Serializable
接口以启用其序列化功能。未实现此接口的类将无法进行序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。
如果读者看过Serializable
的源码,就会发现,他只是一个空的接口,里面什么东西都没有。Serializable接口没有方法或字段,仅用于标识可序列化的语义。但是,如果一个类没有实现这个接口,想要被序列化的话,就会抛出java.io.NotSerializableException
异常。
它是怎么保证只有实现了该接口的方法才能进行序列化与反序列化的呢?
原因是在执行序列化的过程中,会执行到以下代码:
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);
} 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());
}
}
在进行序列化操作时,会判断要被序列化是类是否是Enum
、Array
和 Serializable
类型,如果都不是则直接抛出异常NotSerializable Exception
。
Java 中还提供了Externalizable
接口,也可以实现它来提供序列化能力。
Externalizable
继承自Serializable
,该接口定义了两个抽象方法:writeExternal()
与 readExternal()
当使用Externalizable
接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()
与 readExternal()
方法。否则所有变量的值都会变成默认值。
transient
transient
关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient
变量的值被设定为初始值,如int型是0,对象型是null。
自定义序列化策略
在序列化过程中,如果被序列化的类中定义了writeObject
和 readObject
方法,虚拟机会试图调用对象类里的 writeObject
和 readObject
方法,进行用户自定义的序列化和反序列化。
如果没有这样的方法,则默认调用是ObjectOutputStream
的 defaultWriteObject
方法及ObjectInputStream
的defaultReadObject
方法
用户自定义的writeObject
和 readObject
方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。
所以,对于一些特殊字段需要定义序列化的策略的时候,可以考虑使用transient修饰,并自己重写writeObject
和readObject
方法,如java.util,ArrayList
中就有这样的实现。
我们随便找几个Java实现了序列化接口的类,如String、Integer等,我们发现一个细节,那就是这些类除了实现了Serializable
外,还定义了一个serialVersionUID
那么,到底什么是serialVersionUID
呢?为什么要设置这样一个字段呢?
什么是serialVersionUID
序列化是将对象的状态信息转换为可存储或传输的形式的过程。我们都知道,Java对象是保存在JVM的堆内存中的,也就是说,如果JVM堆不存在了,那么对象也就跟着消失了。
而序列化提供了一种方案,可以让你在即使JVM停机的情况下也能把对象保存下来的方案。就像我们平时用的U盘一样。把Java对象序列化成可存储或传输的形式(如二进制流),比如保存在文件中。这样,当再次需要这个对象的时候,从文件中读取出二进制流,再从二进制流中反序列化出对象。
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致,这个所谓的序列化ID,就是我们在代码中定义的serialVersionUID
。
如果serialVersionUID变了会怎样
我们举个例子吧,看看如果serialVersionUID
被修改了会发生什么?
public class SerializableDemo1 {
public static void main(String[] args) {
//Initializes The Object
User1 user = new User1();
user.setName("hollis");
//Write Obj to File
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(oos);
}
}
}
class User1 implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
我们先执行以上代码,把一个User1对象写入到文件中。然后我们修改一下User1类,把serialVersionUID
的值改为2L
。
class User1 implements Serializable {
private static final long serialVersionUID = 2L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
然后执行以下代码,把文件中的对象反序列化出来:
public class SerializableDemo2 {
public static void main(String[] args) {
//Read Obj from File
File file = new File("tempFile");
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
User1 newUser = (User1) ois.readObject();
System.out.println(newUser);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(ois);
try {
FileUtils.forceDelete(file);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
执行结果如下:
java.io.InvalidClassException: com.hollis.User1; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
可以发现,以上代码抛出了一个java.io.InvalidClassException
,并且指出serialVersionUID
不一致。
这是因为,在进行反序列化时,JVM会把传来的字节流中的serialVersionUID
与本地相应实体类的serialVersionUID
进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException
。
这也是《阿里巴巴Java开发手册》中规定,在兼容性升级中,在修改类的时候,不要修改serialVersionUID
的原因。除非是完全不兼容的两个版本。所以,serialVersionUID
其实是验证版本一致性的。
如果读者感兴趣,可以把各个版本的JDK代码都拿出来看一下,那些向下兼容的类的serialVersionUID
是没有变化过的。比如String类的serialVersionUID
一直都是-6849794470754667710L
。
但是,作者认为,这个规范其实还可以再严格一些,那就是规定:
如果一个类实现了Serializable
接口,就必须手动添加一个private static final long serialVersionUID
变量,并且设置初始值。
为什么要明确定一个serialVersionUID
如果我们没有在类中明确的定义一个serialVersionUID
的话,看看会发生什么。
尝试修改上面的demo代码,先使用以下类定义一个对象,该类中不定义serialVersionUID,将其写入文件。
class User1 implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
然后我们修改User1类,向其中增加一个属性。在尝试将其从文件中读取出来,并进行反序列化。
class User1 implements Serializable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
执行结果:java.io.InvalidClassException: com.hollis.User1; local class incompatible: stream classdesc serialVersionUID = -2986778152837257883, local class serialVersionUID = 7961728318907695402
同样,抛出了InvalidClassException
,并且指出两个serialVersionUID
不同,分别是-2986778152837257883
和7961728318907695402
。
从这里可以看出,系统自己添加了一个serialVersionUID
。
所以,一旦实现了Serializable
,就建议明确的定义一个serialVersionUID
。不然在修改类的时候,就会发生异常。
serialVersionUID
有两种显示的生成方式: 一是默认的1L,比如:private static final long serialVersionUID = 1L
; 二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如: private static final long serialVersionUID = xxxxL
;
后面这种方式,可以借助IDE生成,后面会介绍。
背后原理
知其然,要知其所以然,我们再来看看源码,分析一下为什么serialVersionUID
改变的时候会抛异常?在没有明确定义的情况下,默认的serialVersionUID
是怎么来的?
为了简化代码量,反序列化的调用链如下:
ObjectInputStream.readObject -> readObject0 -> readOrdinaryObject -> readClassDesc -> readNonProxyDesc -> ObjectStreamClass.initNonProxy
在initNonProxy
中 ,关键代码如下:
在反序列化过程中,对serialVersionUID
做了比较,如果发现不相等,则直接抛出异常。
深入看一下getSerialVersionUID
方法:
public long getSerialVersionUID() {
// REMIND: synchronize instead of relying on volatile?
if (suid == null) {
suid = AccessController.doPrivileged(
new PrivilegedAction<Long>() {
public Long run() {
return computeDefaultSUID(cl);
}
}
);
}
return suid.longValue();
}
在没有定义serialVersionUID
的时候,会调用computeDefaultSUID
方法,生成一个默认的serialVersionUID
。
这也就找到了以上两个问题的根源,其实是代码中做了严格的校验。
IDEA 提示
为了确保我们不会忘记定义serialVersionUID
,可以调节一下Intellij IDEA的配置,在实现Serializable
接口后,如果没定义serialVersionUID
的话,IDEA(eclipse一样)会进行提示:
并且可以一键生成一个:
当然,这个配置并不是默认生效的,需要手动到IDEA中设置一下:
在图中标号3的地方(Serializable class without serialVersionUID的配置),打上勾,保存即可。
总结
serialVersionUID
是用来验证版本一致性的。所以在做兼容性升级的时候,不要改变类中serialVersionUID
的值。
如果一个类实现了Serializable接口,一定要记得定义serialVersionUID
,否则会发生异常。可以在IDE中通过设置,让他帮忙提示,并且可以一键快速生成一个serialVersionUID
。
之所以会发生异常,是因为反序列化过程中做了校验,并且如果没有明确定义的话,会根据类的属性自动生成一个。
序列化如何破坏单例模式
本文将通过实例+阅读Java源码的方式介绍序列化是如何破坏单例模式的,以及如何避免序列化对单例的破坏。
单例模式,是设计模式中最简单的一种。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
但是,单例模式真的能够实现实例的唯一性吗?
答案是否定的,很多人都知道使用反射可以破坏单例模式,除了反射以外,使用序列化与反序列化也同样会破坏单例。
序列化堆单例的破坏
首先来写一个单例的类:
code 1
package com.hollis;
import java.io.Serializable;
/**
* Created by hollis on 16/2/5.
* 使用双重校验锁方式实现单例
*/
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
接下来是一个测试类:
code 2
package com.hollis;
import java.io.*;
/**
* Created by hollis on 16/2/5.
*/
public class SerializableDemo1 {
//为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
//Exception直接抛出
public static void main(String[] args) throws IOException, ClassNotFoundException {
//Write Obj to file
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(Singleton.getSingleton());
//Read Obj from file
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
//判断是否是同一个对象
System.out.println(newInstance == Singleton.getSingleton());
}
}
//false
输出结构为false,说明:
通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。
这里,在介绍如何解决这个问题之前,我们先来深入分析一下,为什么会这样?在反序列化的过程中到底发生了什么。
ObjectInputStream
对象的序列化过程通过ObjectOutputStream
和ObjectInputputStream
来实现的,那么带着刚刚的问题,分析一下ObjectInputputStream
的readObject
方法执行情况到底是怎样的。
为了节省篇幅,这里给出ObjectInputStream的readObject
的调用栈:
这里看一下重点代码,readOrdinaryObject
方法的代码片段: code 3
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
//此处省略部分代码
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
//此处省略部分代码
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
code 3 中主要贴出两部分代码。先分析第一部分:
code 3.1
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(desc.forClass().getName(),"unable to create instance").initCause(ex);
}
这里创建的这个obj对象,就是本方法要返回的对象,也可以暂时理解为是ObjectInputStream
的readObject
返回的对象。
isInstantiable
:如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。针对serializable和externalizable我会在其他文章中介绍。
desc.newInstance
:该方法通过反射的方式调用无参构造方法新建一个对象。
所以。到目前为止,也就可以解释,为什么序列化可以破坏单例了?
答:序列化会通过反射调用无参数的构造方法创建一个新的对象。
那么,接下来我们再看刚开始留下的问题,如何防止序列化/反序列化破坏单例模式。
防止序列化破坏单例模式
先给出解决方案,然后再具体分析原理:
只要在Singleton类中定义readResolve
就可以解决该问题:
code 4
package com.hollis;
import java.io.Serializable;
/**
* Created by hollis on 16/2/5.
* 使用双重校验锁方式实现单例
*/
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
private Object readResolve() {
return singleton;
}
}
还是运行以下测试类:
package com.hollis;
import java.io.*;
/**
* Created by hollis on 16/2/5.
*/
public class SerializableDemo1 {
//为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
//Exception直接抛出
public static void main(String[] args) throws IOException, ClassNotFoundException {
//Write Obj to file
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(Singleton.getSingleton());
//Read Obj from file
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
//判断是否是同一个对象
System.out.println(newInstance == Singleton.getSingleton());
}
}
//true
package com.hollis;
import java.io.*;
/**
* Created by hollis on 16/2/5.
*/
public class SerializableDemo1 {
//为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
//Exception直接抛出
public static void main(String[] args) throws IOException, ClassNotFoundException {
//Write Obj to file
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(Singleton.getSingleton());
//Read Obj from file
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
//判断是否是同一个对象
System.out.println(newInstance == Singleton.getSingleton());
}
}
//true
本次输出结果为true。具体原理,我们回过头继续分析code 3中的第二段代码:
code 3.2
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}
hasReadResolveMethod
:如果实现了serializable 或者 externalizable 接口的类包含readResolve
,则返回true
invokeReadResolve
:通过反射调用要被反序列化的类的readResolve方法。
所以,原理也就清楚了,主要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。
总结
在涉及到序列化的场景时,要格外注意他对单例的破坏。
protobuf
Protocol Buffer(简称Protobuf) 是Google 出品的性能优异,跨语言、跨平台的序列化库。
2001年初,Protobuf首先在Google内部创建, 我们把它称之为 proto1,一直以来在Google的内部使用,其中也不断的演化,根据使用者的需求也添加很多新的功能,一些内部库依赖它。几乎每个Google的开发者都会使用到它。
Google开始开源它的内部项目时,因为依赖的关系,所以他们决定首先把Protobuf开源出去。
目前Protobuf的稳定版本是3.9.2,于2019年9月23日发布。由于很多公司很早的就采用了Protobuf,所以很多项目还在使用proto2协议,目前是proto2和proto3同时在使用的状态。