单例模式-序列化问题
在前面文章 ,提到了对象序列化会破坏单例模式,同时也做了试验,今天我们来借着这个问题了解一下序列化的过程。
序列化的作用起源这里就不详细赘述了,本文主要解决序列化破坏单例的问题。如果想详细了解序列化,反序列化过程,推荐一篇文章,写得清清楚楚,包括底层实现流程。
https://cloud.tencent.com/developer/article/1125165)
问题原因
回顾之前序列化的测试代码,重点在于反序列化,我们需要探究为什么它返回了一个新对象。
// 将对象从文件当中反序列化
try{
fis = new FileInputStream("Singleton.obj");
ois = new ObjectInputStream(fis);
s1 = (Singleton) ois.readObject();
}finally{
ois.close();
fis.close();
}
反序列化调用的是 ObjectInputStream 类的 readObject() 方法,继续往下定位问题所在。
源码的过程有些曲折,这边省略了中间过程。
最终走到了readOrdinaryObject() 这个方法,返回值就是反序列化的结果,发现它还是通过反射创建了一个新的对象。。。。。
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
// 获取类的描述信息
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
// 通过描述信息获取 Class 对象
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
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);
}
解决方案
当然官方提供了一个『小后门』,单例类可以通过实现 readResolve() 这个方法,解决序列化破坏单例的问题,代码如下,可以看到 readResolve() 方法中我们返回了之前创建的单例对象。
class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
String name;
private Singleton() {
}
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Object readResolve() {
return instance;
}
}
再次运行之前的测试代码:
public static void main(String[] args) throws Exception {
Singleton s1 = null;
Singleton s = Singleton.getInstance();
FileOutputStream fos = null;
ObjectOutputStream oos = null;
FileInputStream fis = null;
ObjectInputStream ois = null;
try {
fos = new FileOutputStream("Singleton.obj");
oos = new ObjectOutputStream(fos);
oos.writeObject(s);
} finally {
oos.flush();
oos.close();
fos.close();
}
try{
fis = new FileInputStream("Singleton.obj");
ois = new ObjectInputStream(fis);
s1 = (Singleton) ois.readObject();
}finally{
ois.close();
fis.close();
}
System.out.println(s.hashCode());
System.out.println(s1.hashCode());
System.out.println(s == s1);
}
结果如下:
回到上面提到的反序列化方法 readOrdinaryObject() ,看它的后半部分代码,可以发现这个『小后门』。
// 判断类中是否定义了 readResolved() 方法
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) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
// 最终返回的还是单例对象
handles.setObject(passHandle, obj = rep);
}
}
当然也可以通过枚举来实现单例模式,就不存在被序列化破坏单例的问题,这是因为在反序列化的过程中对枚举类型进行了特殊处理。
我们看到 readEnum() 这个方法,用来对枚举类型进行反序列化。
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
if (!desc.isEnum()) {
throw new InvalidClassException("non-enum class: " + desc);
}
int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(enumHandle, resolveEx);
}
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
重点在这,最终是直接调用枚举类提供的 Api 来获取对象,参数是枚举类的 Class 对象,以及枚举变量名称,返回值就是之前定义的枚举对象。
Enum<?> en = Enum.valueOf((Class)cl, name);
总结
通过这篇文章,我们了解了:
反序列化破坏单例模式的原因
通过在单例类中实现 readResolve() 解决问题
通过枚举实现单例,可以避免序列化的问题
如果觉得文章对你有帮助,欢迎留言点赞。
长按扫码可关注