Effective Java 第三版—— 90.考虑序列化代理替代序列化实例
Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。
90. 考虑序列化代理替代序列化实例
正如在条目 85和 条目86中提到并贯穿本章的讨论,实现Serializable接口的决定,增加了出现bug和安全问题的可能性,因为它允许使用一种语言之外的机制来创建实例,而不是使用普通的构造方法。然而,有一种技术可以大大降低这些风险。这种技术称为序列化代理模式(serialization proxy pattern)。
序列化代理模式相当简单。首先,设计一个私有静态嵌套类,它简洁地表示外围类实例的逻辑状态。这个嵌套类称为外围类的序列化代理。它应该有一个构造方法,其参数类型是外围类。这个构造方法只是从它的参数拷贝数据:它不需要做任何一致性检查或防御性拷贝。按照设计,序列化代理的默认序列化形式是外围类的最好的序列化形式。外围类及其序列化代理都必须声明以实现Serializable。
例如,考虑在条目 50中编写的不可变Period类,并在条目 88中进行序列化。以下是该类的序列化代理。 Period非常简单,其序列化代理与该属性具有完全相同的属性:
// Serialization proxy for Period class
private static class SerializationProxy implements Serializable {
private final Date start;
private final Date end;
SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
private static final long serialVersionUID =
234098243823485285L; // Any number will do (Item 87)
}
接下来,将以下writeReplace方法添加到外围类中。可以将此方法逐字复制到具有序列化代理的任何类中:
// writeReplace method for the serialization proxy pattern
private Object writeReplace() {
return new SerializationProxy(this);
}
该方法在外围类上的存在,导致序列化系统发出SerializationProxy实例,而不是外围类的实例。换句话说,writeReplace方法在序列化之前将外围类的实例转换为它的序列化代理。
使用此writeReplace方法,序列化系统永远不会生成外围类的序列化实例,但攻击者可能会构造一个实例,试图违反类的不变性。 要确保此类攻击失败,只需把readObject方法添加到外围类中:
// readObject method for the serialization proxy pattern
private void readObject(ObjectInputStream stream)
throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
最后,在SerializationProxy类上提供一个readResolve方法,该方法返回外围类逻辑等效的实例。此方法的存在导致序列化系统在反序列化时把序列化代理转换回外围类的实例。
这个readResolve方法只使用其公共API创建了一个外围类的实例,这就是该模式的美妙之处。它在很大程度上消除了序列化的语言外特性,因为反序列化实例是使用与任何其他实例相同的构造方法、静态工厂和方法创建的。这使你不必单独确保反序列化的实例遵从类的不变量。如果类的静态工厂或构造方法确立了这些不变性,而它的实例方法维护它们,那么就确保了这些不变性也将通过序列化来维护。
以下是Period.SerializationProxy
的readResolve
方法:
// readResolve method for Period.SerializationProxy
private Object readResolve() {
return new Period(start, end); // Uses public constructor
}
与防御性拷贝方法(第357页)一样,序列化代理方法可以阻止伪造的字节流攻击(条目 88,第354页)和内部属性盗用攻击(条目 88, 第356页)。 与前两种方法不同,这一方法允许Period
类的属性为final,这是Period类成为真正不可变所必需的(条目 17)。 与之前的两种方法不同,这个方法并没有涉及很多想法。 不你必弄清楚哪些属性可能会被狡猾的序列化攻击所破坏,也不必显示地进行有效性检查,作为反序列化的一部分。
还有另一种方法,序列化代理模式比readObject中的防御性拷贝更为强大。 序列化代理模式允许反序列化实例具有与最初序列化实例不同的类。 你可能认为这在实践中没有有用,但并非如此。
考虑EnumSet
类的情况(条目 36)。 这个类没有公共构造方法,只有静态工厂。 从客户端的角度来看,它们返回EnumSet实例,但在当前的OpenJDK实现中,它们返回两个子类中的一个,具体取决于底层枚举类型的大小。 如果底层枚举类型包含64个或更少的元素,则静态工厂返回RegularEnumSet
; 否则,他们返回一个JumboEnumSet
。
现在考虑,如果你序列化一个枚举集合,集合枚举类型有60个元素,然后将五个元素添加到这个枚举类型,再反序列化枚举集合。序列化时,这是一个RegularEnumSet
实例,但一旦反序列化,最好是JumboEnumSet
实例。事实上正是这样,因为EnumSet
使用序列化代理模式。如果好奇,如下是EnumSet
的序列化代理。其实很简单:
// EnumSet's serialization proxy
private static class SerializationProxy <E extends Enum<E>>
implements Serializable {
// The element type of this enum set.
private final Class<E> elementType;
// The elements contained in this enum set.
private final Enum<?>[] elements;
SerializationProxy(EnumSet<E> set) {
elementType = set.elementType;
elements = set.toArray(new Enum<?>[0]);
}
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;
}
序列化代理模式有两个限制。它与用户可扩展的类不兼容(条目 19)。而且,它与一些对象图包含循环的类不兼容:如果试图从对象的序列化代理的readResolve方法中调用对象上的方法,得到一个ClassCastException
异常,因为你还没有对象,只有该对象的序列化代理。
最后,序列化代理模式增强的功能和安全性并不是免费的。 在我的机器上,使用序列化代理序列化和反序列化Period实例,比使用防御性拷贝多出14%的昂贵开销。
总之,只要发现自己必须在不能由客户端扩展的类上编写readObject或writeObject方法时,请考虑序列化代理模式。 使用重要不变性来健壮序列化对象时,这种模式可能是最简单方法。
Effective Java 第三版—— 90.考虑序列化代理替代序列化实例的更多相关文章
- Effective Java 第三版——34. 使用枚举类型替代整型常量
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- Effective Java 第三版——9. 使用try-with-resources语句替代try-finally语句
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- 《Effective Java 第三版》目录汇总
经过反复不断的拖延和坚持,所有条目已经翻译完成,供大家分享学习.时间有限,个别地方翻译得比较仓促,希望有疑虑的地方指出批评改正. 第一章简介 忽略 第二章 创建和销毁对象 1. 考虑使用静态工厂方法替 ...
- 《Effective Java 第三版》新条目介绍
版权声明:本文为博主原创文章,可以随意转载,不过请加上原文链接. https://blog.csdn.net/u014717036/article/details/80588806前言 从去年的3月份 ...
- Effective Java 第三版—— 87. 考虑使用自定义序列化形式
Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...
- Effective Java 第三版——3. 使用私有构造方法或枚类实现Singleton属性
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- Effective Java第三版(一) ——用静态工厂代替构造器
此文做为<Effective Java>系列的第一篇,所以有必要大概说下此书的特点,当然很多人可能都看过,毕竟是有着Java四大名著之一的大名在外,不过总会有萌新不了解,例如我!<E ...
- effective java(第三版)---读书笔记
第一章 引言 < Effective Java>这本书并不厚,而且并不适合初学者,适合有一定的工作经验的java攻城狮.这本书不是百科全书式的JAVA 手册,而是试图在讲述如何正确.高效地 ...
- Effective Java 第三版——1. 考虑使用静态工厂方法替代构造方法
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
随机推荐
- MD5_Util工具类代码
package com.yby.mall.utils; import java.math.BigInteger; import java.security.MessageDigest; public ...
- 【CRM】Microsoft CRM-QueryExpression 成员
名称 ColumnSet 获取或设置要包含的列. Criteria 获取或设置过滤查询结果的复杂条件和逻辑过滤器表达式. Distinct 获取或设置查询的结果是否包含重复的实体实例. Entit ...
- 连接到docker 指定的一个容器中
1.docker run -itd ubuntu 2.sudo docker ps 3.PID=$(docker-pid containerID) 返回一个p_id 4.nsenter --targ ...
- [c++] opencv加载png
1.cvloadimage载入png文件时,默认的第2个参数是1,即CV_LOAD_IMAGE_COLOR,生成的iplimage对象的channel数是3,而不是4,丢失了第4通道.需要改为cvlo ...
- bzo1606: [Usaco2008 Dec]Hay For Sale 购买干草
1606: [Usaco2008 Dec]Hay For Sale 购买干草 Time Limit: 5 Sec Memory Limit: 64 MBSubmit: 1338 Solved: 9 ...
- token和盐
// 盐,加密后密码获取 Map<String, String> map = new HashMap<String, String>(); map.put(&quo ...
- Java 多线程 临界区
临界区表示一种公共资源或共享数据,可以被多个线程使用.但是每一次只能有一个线程使用它.一旦临界区资源被占用,想使用该资源的其他线程必须等待. 例如,办公室里的打印机一次只能执行一个任务.当小王和小明同 ...
- java第二周的作业
package java第二周学习; import javax.swing.JOptionPane; public class 数学题 { private int a; private int b; ...
- Python直接控制鼠标键盘
Python直接控制鼠标键盘 之前因为期末的原因已经很久没写博客了,今天博主发现一个好玩的模块PyAutoGUI,借助它可以使用Python脚本直接控制键盘鼠标,感觉可以解决很多无聊的机械运动.这里记 ...
- GDB快速入门
GDB快速入门 GDB(GNU DeBugger)是Linux下强大的C/C++调试器,纯命令行操作 启动 以下为测试代码 #include <stdio.h> int nGlobalVa ...