序列化代理类
有时候我们去看JDK源码,会发现一些类实现了Serializable接口,同时类中还创建了静态内部类SerializationProxy(或者以此结尾、开头的,总归一个意思和相同的用途)。当我们第一次看见时,会有些迷惑,这些类的编码都非常的简单,似乎没做太多事情,那么它们到底有什么作用,设计者的意图是什么,为了解决什么问题呢。
序列化代理类写法
先来简单看几个JDK中自带的:在 JDK 中的多个并发工具类(例如 DoubleAdder、LongAdder 等)中都能看到类似的设计
private static class SerializationProxy implements Serializable {
private static final long serialVersionUID = 7249069246863182397L;
private final double value;
SerializationProxy(DoubleAdder a) {
value = a.sum();
}
private Object readResolve() {
DoubleAdder a = new DoubleAdder();
a.base = Double.doubleToRawLongBits(value);
return a;
}
}
主要用途:
- 序列化代理只保存对象的逻辑状态,也就是说只保存值,比如上面这种不直接引用内部的实现细节(例如这里的包类Striped64)。这样,在序列化时不暴露内部复杂或者可能变化的结构
- 保证安全性,通过外部类的readResolve()方法配合,创建一个新的对象并初始化器状态,可以确保反序列化后的对象满足类的不变性要求,避免恶意构造的字节流破坏对象状态
以下另一个是EnumSet类中的序列化代理类,从类的注释说明就能看到这样设计的目的:
这个类用于序列化所有EnumSet实例,而不考虑实现类型。它捕获它们的“逻辑内容”,并使用公共静态工厂对它们进行重构。这对于确保特定实现类型的存在是实现细节是必要的。
其实和上一个序列化代理类的功能用途都是一样的,为了保持状态一致性,序列化和反序列化之后的对象保持一致,且不被外部破坏状态(比如设置不正确的属性值、引用不正确的类型等操作)。EnumSet是个抽象类,他的实现有两个,是根据装载的枚举数量来决定使用哪一个实现,那么意味着如果不做特殊处理,一个类在序列化之前是JumboEnumSet、然后通过序列化手段修改元素数量,可能反序列化回来就变成RegularEnumSet,这样一来类型就不匹配了。所以这里使用序列化代理也就同时解决了这个问题,序列化后的类型依然由类的内部来决定。
private static class SerializationProxy <E extends Enum<E>>
implements java.io.Serializable
{
private final Class<E> elementType;
private final Enum<?>[] elements;
SerializationProxy(EnumSet<E> set) {
elementType = set.elementType;
elements = set.toArray(ZERO_LENGTH_ENUM_ARRAY);
}
// instead of cast to E, we should perhaps use elementType.cast()
// to avoid injection of forged stream, but it will slow the implementation
@SuppressWarnings("unchecked")
private Object readResolve() {
EnumSet<E> result = EnumSet.noneOf(elementType);
for (Enum<?> e : elements)
result.add((E)e);
return result;
}
private static final long serialVersionUID = 362491234563181265L;
}
这是guava包中,cache初始化的。类的注释说明如下:
序列化LocalCache的配置,在反序列化时使用CacheBuilder将其重构为LoadingCache。这个类的一个实例适合由LocalLoadingCache的writeReplace使用。不幸的是,当存在循环依赖时,readResolve()不会被调用,因此代理必须能够像缓存本身一样运行。
static final class LoadingSerializationProxy<K, V> extends ManualSerializationProxy<K, V>
implements LoadingCache<K, V>, Serializable {
private static final long serialVersionUID = 1;
transient LoadingCache<K, V> autoDelegate;
LoadingSerializationProxy(LocalCache<K, V> cache) {
super(cache);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
CacheBuilder<K, V> builder = recreateCacheBuilder();
this.autoDelegate = builder.build(loader);
}
@Override
public V get(K key) throws ExecutionException {
return autoDelegate.get(key);
}
@Override
public V getUnchecked(K key) {
return autoDelegate.getUnchecked(key);
}
@Override
public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException {
return autoDelegate.getAll(keys);
}
@Override
public final V apply(K key) {
return autoDelegate.apply(key);
}
@Override
public void refresh(K key) {
autoDelegate.refresh(key);
}
private Object readResolve() {
return autoDelegate;
}
}
主要用途
-
隔离内部实现
Guava 的缓存内部实现(比如 LocalCache 等)通常比较复杂、并且包含许多内部状态或锁等细节,不适宜直接暴露在序列化的字节流中。LoadingSerializationProxy 只保存了缓存的逻辑状态和构造参数(例如 CacheBuilder 的配置、加载器 loader 等),而不直接序列化内部的实现细节。 -
安全地重建缓存
在反序列化过程中,它在 readObject() 方法中调用了 in.defaultReadObject(),然后利用保存的配置信息(通过调用 recreateCacheBuilder())和缓存加载器重新构建一个新的 LoadingCache 实例,并将结果赋值给 autoDelegate。这样确保了反序列化后得到的缓存实例是通过公有 API 正确构造的,而不是直接恢复内部状态,从而保证了不变性和安全性。 -
方法委托与透明替换
LoadingSerializationProxy 实现了 LoadingCache 接口,所有 LoadingCache 的方法(如 get、getUnchecked、refresh 等)都简单地委托给了 autoDelegate(新构造的缓存实例)。最后,readResolve() 返回 autoDelegate,从而使得整个反序列化过程对调用者透明,最终得到的对象就是一个正常工作的 LoadingCache。
核心要点
- 既然是序列化代理模式,其作用无容置疑就是为了Java的序列化和反序列化而设计和实现的,不做它用。那么Java中一个类要支持序列化,当然是必须实现Seriaizable接口
- 无论是代理类、被代理类都要实现Serializable接口
- 代理类中要实现private Object readResolve() 方法。注意方法签名不能变,因为它是属于Serializable接口的特殊方法。在 Java 序列化过程中,当一个对象被反序列化后,JVM 会检查该类是否定义了一个名为 readResolve 的特殊方法(通常是 private 且无参数,返回 Object)。如果定义了这个方法,JVM 会在反序列化完成后自动调用它,并用 readResolve 方法返回的对象替换刚刚反序列化生成的对象。
- 被代理类要实现Serializable的Object writeReplace() private void readObject(ObjectInputStream in) 方法。writeReplace方法允许一个对象在序列胡之前替换为另一个对象。也就是说,当序列化系统发现当前对象定义了这个方法时,会调用它,并将该方法返回的对象实际写入到序列化流中,而不是当前对象本身。(用来保证单例或者不变性)。readObject()方法用于定制对象的反序列化过程。当对象被反序列化时,如果类中定义了这个私有方法,系统会调用它来读取对象的状态。通常,这个方法会先调用 in.defaultReadObject() 来执行默认的反序列化操作,然后可以进行额外的操作。(用来保证不变性检查、防御性拷贝和阻止直接反序列化。~~想想单例模式的实现,为了防止序列化攻击产生多个对象,会实现此方法并作处理)
关于序列化
在《Effective Java》中,作者用了专门一个章节来讲Java序列化,从Java的历史遗留问题到现在的序列化方式,做了全面的分析和总结。最终给大家的建议就是,不要随意实现Serializable,尽可能避免使用Java自带的序列化机制,如果非要用,对于有状态的类,为确保安全、一致性要求那么久使用序列化代理类,而最好的选择是使用现代序列化机制比如常见的json、xml、protobuff等机制替代Java自带序列化。

浙公网安备 33010602011771号