今日考题 JAVA序列化方面
题目共三题:
- Serializable 与 Externalizable的区别;
- 说明 Serializable 接口 WriteReplace、ReadResolve 机制及用途
- 编程题, 写一个单例模式可序列化类,并保证其不能被反射重复创建
上述问题涉及到java序列化(Serializability)过程的相关知识,以下逐层以源码方式进行相关说明。以下源码以 java 8 进行描述。
问题1
首先要说明 什么叫序列化(Serializability)及序列化的用途。简单的来说序列化(Serializability)就是将一个对象的状态(各个属性量)保存起来,然后在适当的时候再获得。对应的序列化过程就会有 序列化(serialization) 和 反序列化(deserialization) 两个过程。
序列化(serialization)过程则是将对象Object 通过ObjectOutputStream 进行IO流输出,反序列化(deserialization)则是通过 ObjectInputStream 将对应的IO流 转化为 Object对象。通过序列化操作,就可以把对应的对象以流的方式保存为文件,推送给其他外部系统等等,所以序列化可以认为是一个对象可以支持分布式计算的标志。
Serializable 是一个 可序列化标识接口,用于表示某类的实例化对象可以被序列化。它和Cloneable一样是标记型接口(tagging interface),是一个空接口,但对应的有若干特殊签名方法(special methods with exact signatures),包括以下内容
//负责写入特定类的对象的状态
private void writeObject(java.io.ObjectOutputStream out) throws IOException
//负责从流中读取并恢复类字段
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
//非正常情况下进行恢复类字段
private void readObjectNoData() throws ObjectStreamException;
//将对象写入流前需要指定要使用的替代对象的可序列化类
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
//从流中读取类的一个实例,进行返回时需要指定替代的类
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
上述的前三个方法是用于控制对象进行特殊化序列化/反序列化操作。而对应的writeReplace 则是序列化写入前进行相应的替换,如将对象的一个加密字段替换为一个固定标识;对应的readResolve 则是反序列化返回对应的对象前来进行相应解决处理(含意不同于solve),如将对象的某一个字段进行修改或返回指定单例模式对象等。
此外Serializable 类 需要设置静态 (static)、最终 (final) 、 long类型的 serializableVersionUID字段,用于标识本类的版本,确认相应的兼容性。若未设置,则会在序列化过程中重新计算一个对应的serialVersionUID,且由于不同版本的编译器(compiler)差异问题,反而会导致出现 InvalidClassException 异常,所以强烈建议指定或生成一个对应的serialVersionUID。
ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;
Externalizable 则继承了 Serializable 接口,并附加writeExternal,readExternal 方法用来实现对对象序列化/反序列化的完全控制。即要存储的每个对象都需要检测是否支持 Externalizable 接口。如果对象支持 Externalizable,则调用 writeExternal 方法。如果对象不支持 Externalizable 但实现了 Serializable,则使用 ObjectOutputStream 保存该对象。在重构 Externalizable 对象时,先使用无参数的公共构造方法创建一个实例,然后调用 readExternal 方法。通过从 ObjectInputStream 中读取 Serializable 对象可以恢复这些对象。
以上内容可以通过对应的 ObjectOutputStream /ObjectInputStream 来看出相关端倪,如下:
//ObjectOutputStream 部分代码
private void writeOrdinaryObject(Object obj,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
//此处省略部分代码....
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);
} else {
writeSerialData(obj, desc);
}
//此处省略部分代码....
//ObjectInputStream 部分代码
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
//此处省略部分代码....
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
//此处省略部分代码....
问题 2
关于问题2 其实在 问题1 已经给出了相应的答案,其实质还是在于对序列化/反序列化进行过程的相关控制。实际序列化操作都会使用到对应的ObjectStreamClass 类,在该类中构造器则会通过反射的方式来判断是否可序列化、是否有对应的Method等,参考如下代码
private ObjectStreamClass(final Class<?> cl) {
this.cl = cl;
name = cl.getName();
isProxy = Proxy.isProxyClass(cl);
isEnum = Enum.class.isAssignableFrom(cl);
serializable = Serializable.class.isAssignableFrom(cl);
externalizable = Externalizable.class.isAssignableFrom(cl);
//此处省略部分代码....
if (serializable) {
//此处省略部分代码....
if (externalizable) {
cons = getExternalizableConstructor(cl);
} else {
cons = getSerializableConstructor(cl);
writeObjectMethod = getPrivateMethod(cl, "writeObject",
new Class<?>[] { ObjectOutputStream.class },
Void.TYPE);
readObjectMethod = getPrivateMethod(cl, "readObject",
new Class<?>[] { ObjectInputStream.class },
Void.TYPE);
readObjectNoDataMethod = getPrivateMethod(
cl, "readObjectNoData", null, Void.TYPE);
hasWriteObjectData = (writeObjectMethod != null);
}
domains = getProtectionDomains(cons, cl);
writeReplaceMethod = getInheritableMethod(
cl, "writeReplace", null, Object.class);
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
//此处省略部分代码....
而在对应的ObjectInputStream 类 readOrdinaryObject方法就可以获知到实际运行反序列化过程中,同样以反射的方式运行了readResolve方法。
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
//此处省略部分代码....
ObjectStreamClass desc = readClassDesc(false);
//此处省略部分代码....
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);
}
}
return obj;
同理 ObjectOutputStream 也有对应 invokeWriteReplace 的操作,不过相应的源码解析要相对复杂一点,因为会涉及到类的逐层继承问题,需要迭代地进行相关判断及处理。
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
//此处省略部分代码....
for (;;) {
// REMIND: skip this check for strings/arrays?
Class<?> repCl;
desc = ObjectStreamClass.lookup(cl, true);
if (!desc.hasWriteReplaceMethod() ||
(obj = desc.invokeWriteReplace(obj)) == null ||
(repCl = obj.getClass()) == cl)
{
break;
}
cl = repCl;
}
//此处省略部分代码....
问题3
有了前面 问题2 的基础就可以利用readResolve来解决,这个比较容易写,以下以静态内部类的方式来说明及进行相关验证。
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
/**
* 可序列化的单例类
* @author zhangb
*
*/
public class MySingletonDemo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 静态内部类
*
* @author zhangb
*
*/
private static class SingletonClassInstance {
private static final MySingletonDemo instance = new MySingletonDemo();
}
private MySingletonDemo() {
//以下禁止反射
if(SingletonClassInstance.instance!=null) {
throw new RuntimeException();
}
}
public static MySingletonDemo getInstance() {
return SingletonClassInstance.instance;
}
// 利用 readResolve来强制返回当前的实例
private Object readResolve() {
return getInstance();
}
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
MySingletonDemo demo = MySingletonDemo.getInstance();
System.out.println(demo);
// 以下测试 反序列化
Path demoFile = FileSystems.getDefault().getPath(System.getProperty("java.io.tmpdir"),"demo.demo");
System.out.println(demoFile.toString());
ObjectOutputStream objectOutputStream = new ObjectOutputStream(
Files.newOutputStream(demoFile, StandardOpenOption.CREATE));
objectOutputStream.writeObject(demo);
if(demoFile.toFile().canRead()) {
System.out.println("size:"+demoFile.toFile().length());
}
ObjectInputStream objectInputStream = new ObjectInputStream(
Files.newInputStream(demoFile));
MySingletonDemo demo1 = (MySingletonDemo) objectInputStream.readObject();
System.out.println(demo1.toString());
System.out.println(demo1 == demo);
//以下测试反射
Class<MySingletonDemo> clazz = MySingletonDemo.class;
Constructor<MySingletonDemo> constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
MySingletonDemo demo2 = constructor.newInstance(null);
System.out.println(demo2);
System.out.println(demo2 == demo);
}
}
测试时可以注释对应的readResolve方法来进行测试。