Effective Java 第三版—— 90.考虑序列化代理替代序列化实例

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

Effective Java 第三版—— 90.考虑序列化代理替代序列化实例
  1. 考虑序列化代理替代序列化实例

正如在条目 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.SerializationProxyreadResolve方法:

// 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;
}
</e></e></e></e>

序列化代理模式有两个限制。它与用户可扩展的类不兼容(条目 19)。而且,它与一些对象图包含循环的类不兼容:如果试图从对象的序列化代理的readResolve方法中调用对象上的方法,得到一个 ClassCastException异常,因为你还没有对象,只有该对象的序列化代理。

最后,序列化代理模式增强的功能和安全性并不是免费的。 在我的机器上,使用序列化代理序列化和反序列化Period实例,比使用防御性拷贝多出14%的昂贵开销。

总之,只要发现自己必须在不能由客户端扩展的类上编写readObject或writeObject方法时,请考虑序列化代理模式。 使用重要不变性来健壮序列化对象时,这种模式可能是最简单方法。

Original: https://www.cnblogs.com/IcanFixIt/p/10680068.html
Author: 林本托
Title: Effective Java 第三版—— 90.考虑序列化代理替代序列化实例

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/537070/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Java代码如何创建GUID字符串呢?

    随机字符串是我们日常开发中,经常使用的一种字符串,那么下文将讲述具有代表性的字符串GUID GUID字符串是全球唯一标识,是我们经常使用的一种唯一标识 如:分布式系统中使用其作为表…

    Java 2023年6月15日
    075
  • 22.1.26 递归改动态规划

    22.1.26 递归改动态规划 1.套路: 1)递归:根据题目写出递归版本; 2)记忆化搜索:用某种结构存储已经计算过的信息,省去重复计算的过程; 3)严格表结构:将递归套路转化为…

    Java 2023年6月13日
    090
  • Guava Retryer实现接口重试

    前言 小黑在开发中遇到个问题,我负责的模块需要调用某个三方服务接口查询信息,查询结果直接影响后续业务逻辑的处理; 这个接口偶尔会因网络问题出现超时,导致我的业务逻辑无法继续处理; …

    Java 2023年6月7日
    086
  • java读写锁

    工作遇到了金钱计算,需要用到读写锁保证数据安全。记录一下。 单纯读没有限制,读写、写写的时候会有安全问题。 _hashMap_存在并发线程安全问题,而 _hashtable_线程安…

    Java 2023年6月9日
    077
  • spring boot 集成minIo

    minio的安装与配置 1、什么是 MinIO? MinIO 是一款高性能、分布式的对象存储系统。 对象存储服务OSS(Object Storage Service)是一种海量、安…

    Java 2023年6月5日
    0104
  • java Builder模式

    Builder 模式也叫建造者模式,builder模式的作用将一个复杂对象的构建与他的表示分离,一步一步创建一个复杂对象的创建型模式。在不知道内部建造细节的情况下,可以更精细的控制…

    Java 2023年6月16日
    0105
  • 进程和线程的对比和区别

    进程中包括有多个线程,进程与进程之间是相对比较独立的。 进程中有一个 逻辑内存,每个进程都会有分配到一个独立的内存空间,还分配了一个文件/网络句柄, 句柄类似一个标识符,所有的进程…

    Java 2023年5月30日
    072
  • Smack 3.3.1 发布,Java 的 XMPP 开发包

    Smack 3.3.1 发布了,这是一个小更新版本,主要更新包括: [SMACK-441] – Memory leak in KeepAliveManager [SMA…

    Java 2023年5月29日
    084
  • Spring Boot 通过RestTemplat返回pdf转图片

    Springboot 通过 RestTemplat 返回 pdf转图片 开发环境: java 8 spring boot 2.x 前后端分离 需求:前端发送pdf文件路径来,后端获…

    Java 2023年6月5日
    061
  • 设计模式之中介者模式

    中介者模式又称调停者模式,属于行为型模式;中介者模式包装了一系列对象相互作用的方式,使得这些对象不必互相明显引用。从而使它们可以较松散地耦合。当这些对象中的某些对象之间的相互作用发…

    Java 2023年6月5日
    088
  • 如何阅读

    推荐一本好书 如何阅读一本书如果你还在为看不懂文档或看得很慢而烦恼,看了也很快的就忘了,不停的在原地踏步。并不是因为你笨,而是因为你没有找到一个阅读的方法。贯穿始终的一个点就是,提…

    Java 2023年6月5日
    082
  • quartz框架(四)-Job相关内容

    本篇博文,博主主要介绍job相关的内容。 job是业务类需要实现的接口,代表需要被调度框架进行调度的任务。job源码如下所示,从源码中我们可以看到,job接口只有一个excute方…

    Java 2023年6月7日
    074
  • CSS基础

    Css 作用:用来修饰HTML页面,设置元素的样式,让html页面更加美观 一、引入css的三种方式 1、内联定义:在对象的标记内使用 &#x8BED;&#x6CD…

    Java 2023年6月13日
    060
  • 每日一记:关于Arrays.asList和Collections.unmodifiableList的一点理解

    1、正常创建一个List,对List进行操作 List collect = Stream.of(1 ,3 ,5 ,7 ,9).collect(Collectors.toList()…

    Java 2023年6月5日
    079
  • Java HashMap 四个构造函数

    HashMap():构造一个空的 HashMap ,默认初始容量(16)和默认负载系数(0.75)。 HashMap(int initialCapacity):构造一个空的 Has…

    Java 2023年6月5日
    079
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球