项目中间用到了 Java 序列化,但碰到了几个问题,可以说道说道。
由于涉及到问题以及解决方法有些繁杂,所以用一套例子来演示各种情况,项目以 [Ant] 编译运行。目录文件如下:
-
CustomObject.java // 就是我们的主角了,它本身是在缺省的类路径,但其子类都是单独的 jar
-
|-- SubCustomObject.java // 第一个子类
-
|-- plugin1.properties // 第一个子类所对应的信息,让加载器读取
-
|-- AnotherSubCustomObject.java // 第二个子类
-
|-- plugin2.properties // 同上
-
-
// 主程序
-
SerializationTest.java
-
-
// 单例类
-
CustomObjectUtils.java
-
// 自定义的类加载器
-
CustomClassLoader.java
-
-
// 最简单的对象容器
-
SimpleCustomObjectHolder.java
-
// 将包含的对象在序列化时转换成 String,并在逆序列化时再转换回来。
-
CustomObjectHolder.java
-
// 列表容器,演示 Collection
-
CustomObjectArrayHolder.java
-
-
// 使用自定义类加载器的 ObjectInputStream
-
ClassLoaderObjectInputStream.java
-
-
// 对象包装器以及相应的 ObjectInputStream 和 OutputStream
-
WrapperCustomObject.java
-
WrapperObjectInputStream.java
-
WrapperObjectOutputStream.java
第一个问题有关类加载器(Class Loader)。有些类使用了自定义的类加载器,但也需要进行序列化。写出去应该没什么问题,但读入就有问题了。缺省的情况下,找到的类加载器是最近调用 readObject() 的类所用的类加载器。一般情况下找到的都是缺省的类加载器,那对这些使用自定义类加载器的类进行逆序列化的时候就会报 ClassNotFoundException 异常了。在我们的例子中,"Wrong" 和 SimpleCustomObjectHolder 体现了这点。如下例所示,可以调用 ant run-classloader-wrong 或者 ant run-classloader-wrong 来运行查看结果:
-
o1 = utils.stringToObject(s1);
-
-
public void classloaderWrong(boolean save) {
-
if (save) {
-
try {
-
out.writeObject(o1);
-
-
out.close();
-
e.printStackTrace();
-
}
-
} else {
-
try {
-
CustomObject obj = (CustomObject) in.readObject();
-
-
-
in.close();
-
e.printStackTrace();
-
}
-
}
-
}
那需要自己的类加载器怎么办呢?一种是直接的方法,继承 java.io.ObjectInputStream 重写 Class resolveClass(ObjectStreamClass) 方法,在其中调用自己的类加载器来加载正确的类。这个没什么好说的,但是不符合我的要求。在项目中,在这个地方是直接拿不到原类加载器的引用的,而且也没有必要暴露出来,还有就是哪些类需要自定义的类加载器也是没有办法预先判断,就不得不寻找其他办法。
这个方法其实是一种变通(workaround)方法。项目中的这些类具体是哪些是未知的,但可以和 JSON 对象互相转换,而 JSON 又可以和 String 互相转换,所以可以在其包含类中的 writeObject() 中将其转换成 String 来序列化,然后在 readObject() 中以 String 读入,再转换成相应的对象。当然,我这里 String 到 JSON 再到自定义类的对象是有别的方法来控制,并且能够利用自定义的类加载器来加载相应的类。在这里的例子中,在 CustomObjectUtils 提供了相对应的方法作为示例。如下例所示,使用 ant run-classloader-custom-holder 查看结果:
-
/**
-
* Custom object holder with converting.
-
*
-
* @author SuperMMX
-
*/
-
private transient CustomObject obj;
-
-
this.value = value;
-
this.obj = obj;
-
}
-
-
out.defaultWriteObject();
-
-
out.writeObject(objValue);
-
}
-
-
in.defaultReadObject();
-
-
obj = CustomObjectUtils.stringToObject(objValue);
-
}
-
-
StringBuilder sb = new StringBuilder();
-
sb.append("Holder Value: ").append(value);
-
sb.append(", CustomObject: ").append(obj);
-
-
return sb.toString();
-
}
-
}
-
o2 = utils.stringToObject(s2);
-
-
customHolder = new CustomObjectHolder("CustomHolder", o2);
-
-
public void classloaderCustomHolder(boolean save, boolean withWrapper) {
-
if (withWrapper) {
-
file = "build/classloader-custom-holder-wrapper.ser";
-
} else {
-
file = "build/classloader-custom-holder.ser";
-
}
-
if (save) {
-
try {
-
out.writeObject(customHolder);
-
-
out.close();
-
e.printStackTrace();
-
}
-
} else {
-
try {
-
CustomObjectHolder obj = (CustomObjectHolder) in.readObject();
-
-
-
in.close();
-
e.printStackTrace();
-
}
-
}
-
}
这个方法还存在一个问题,比如自定类叫 ClassA,在别的一个对象 B 中有一个 ClassA 的对象,对其进行序列化和逆序列化可以用上述方法来做到,毕竟 B 是自己写的,可以在里面进行控制。但如果说对一个集合容器(Collection)比如 ArrayList、Set、Map 之中的 ClassA 就无法这样做到,总不可能在所有用到这些类的地方都自己写一套转换的代码吧。如果在 ClassA 内转换成 String 写入,那读出的时候就是 String 而非 ClassA,更不可行。所以只好加了个包装,叫做 ClassAWrapper 中包含一个 ClassA 对象的引用,在 writeObject() 和 readObject() 进行转换,然后序列化与逆序列化针对的都是 ClassAWrapper。如下代码:
-
/**
-
* Wrapper for custom object.
-
*
-
* @author SuperMMX
-
*/
-
private transient CustomObject obj;
-
-
public WrapperCustomObject(CustomObject obj) {
-
this.obj = obj;
-
}
-
-
public CustomObject getObject() {
-
return obj;
-
}
-
-
out.defaultWriteObject();
-
-
out.writeObject(value);
-
}
-
-
in.defaultReadObject();
-
-
obj = CustomObjectUtils.getInstance().stringToObject(value);
-
}
-
}
但问题没有完全解决,就是时机,什么时候才能让我们将 CustomObject 转换成 ClassAWrapper?幸好 Java 序列化的机制比较完整,在 ObjectInputStream 中有个方法 Object resolveObject(Object),就是将一个对象用另一个对象来替换掉,恰恰就是这里所需要的。但这之前必须要调用 ObjectInputStream.enableResolveObject(true) 启用这个特性。那么问题就解决了。下面是完整的代码,其他部分跟以前的代码基本是相同的,可以调用 ant run-classloader-simple-holder-wrapper run-classloader-custom-holder-wrapper run-classloader-array-holder-wrapper 查看结果:
-
/**
-
* Wrapper Object output stream.
-
*
-
* @author SuperMMX
-
*/
-
super(out);
-
-
enableReplaceObject(true);
-
}
-
-
if (obj instanceof CustomObject) {
-
obj = new WrapperCustomObject((CustomObject)obj);
-
}
-
return obj;
-
}
-
}
-
-
/**
-
* ObjectInputStream that wraps the custom objects.
-
*
-
* @author SuperMMX
-
*/
-
super(in);
-
-
enableResolveObject(true);
-
}
-
-
if (obj instanceof WrapperCustomObject) {
-
obj = ((WrapperCustomObject)obj).getObject();
-
}
-
return obj;
-
}
-
}
另:后来再仔细想想,还是有办法拿到所有能控制的子类列表的,在系统启动的时候某个模块会使用自定义的类加载器把所有可用的子类都会加载进来,这样在 resolveClass() 中可以跟这个类的列表进行比较了。但这样会有性能损失吗?每个进行逆序列化的对象都要和列表进行比较,开销会不会很大?答案是不会。
我们看一看 [Java Object Serialization Specification]:
12. For regular objects, the ObjectStreamClass for the class of the object is written by recursively calling writeObject. It will appear in the stream only the first time it is referenced. A handle is assigned for the object.
所以,一个类相应的 ObjectStreamClass 首先会写出去,然后
7. If the object is an ObjectStreamClass, a handle is assigned to the object, after which it is written to the stream...
5. If the object has already been written to the stream, its handle is written to the stream and writeObject returns.
也就是说在进行序列化的时候,同一个对象只会在第一次碰到的时候全部存下来,其他时候都保存的是一个引用的句柄(handle)。
然后再看看[逆序列化]:
The resolveClass method is called while a class is being deserialized, and after the class descriptor has been read.
也就是说只有一个类(Class)进行逆序列化的时候才会调用 resolveClass(),而非每个对象(Object)。所以不管你要进行逆序列化的对象有多少,其所对应的类只要进行一次就可以了。所以不会对性能有很大的影响。
至此,此问题完全并且漂亮地解决,代码量很少。
所有的代码可以下载:serialization.tar.gz
BTW,刚开始想到 resolveClass() 的时候,确实没有仔细考虑性能问题,就简单地认为每个对象都会比较一遍,所以才采用了后面的包装器方法,走了些弯路。直到写这篇文章的时候才仔细研究,得出现在的结论。




Post new comment