带你快速看完9.8分神作《Effective Java》—— 序列化篇(所有RPC框架的基石)

本文阅读 22 分钟
首页 代码,Java 正文

🔥 Java学习:Java从入门到精通总结

85 其他方法优先于Java序列化

🔥 先说结论:

  1. 序列化是危险的,应该避免
  2. 如果从头开始设计一个系统,可以使用跨平台的结构化数据,如JSON或protobuf
  3. 如果必须编写可序列化的类,要加倍小心地进行试验

序列化的一个根本问题是它的可攻击范围太大,且难以防御:通过调用ObjectInputStream上的readObject方法反序列化对象。可以用来实例化类路径上任何类型的对象,只要该类型实现Serializable接口,有了实例化之后的对象,就可以执行这些类的代码,因此所有这些类都在攻击范围内。

static byte[] bomb() { 
    Set<Object> root = new HashSet<>();
    Set<Object> s1 = root;
    Set<Object> s2 = new HashSet<>();
    for (int i = 0; i < 100; i++) { 
        Set<Object> t1 = new HashSet<>();
        Set<Object> t2 = new HashSet<>();
        t1.add("foo"); // Make t1 unequal to t2
        s1.add(t1); s1.add(t2);
        s2.add(t1); s2.add(t2);
        s1 = t1;
        s2 = t2;
    }
    return serialize(root); // Method omitted for brevity
}

对象图由201个HashSet实例组成,整个流的⻓度为5744字节,但是在对其进行反序列化之前,资源就已经耗尽了。问题在于,反序列化HashSet实例需要计算其内部元素的哈希码,深度为100,反序列化Set会导致hashCode方法被调用超过2^100次。 img

避免序列化利用的最好方法是永远不要反序列化任何东西。还有其他一些机制可以在对象和字节序列之间进行转换,从而避免了Java序列化的许多危险。

这些方法共同点是它们比Java序列化简单得多。它们不支持任意对象图的自动序列化和反序列化。而是支持简单的结构化数据对象,由一组「属性-值」对组成。只支持少数基本数据类型和数组数据类型。

最前沿的跨平台结构化数据表示是JSON和protobuf,

JSONprotobuf之间最显著的区别是JSON是基于文本的,而protobuf是二进制的,但效率更高;

如果你不能完全避免Java序列化,这时的最佳选择是永远不要反序列化不可信的数据。

86 谨慎实现 Serializable 接口

🔥 先说结论:

  1. 除非一个类只在受保护的环境下使用(版本之间无交互、服务器不会暴露给不可信任的人),否则必须认真考虑是否要实现 Serializable 接口
  2. 如果一个类允许继承,更要加倍小心
  3. 除了静态成员类之外,内部类不要实现这个接口

实现 Serializable 接口的一个主要代价是,一旦类的实现被发布,它就会降低修改灵活性。

即使是设计良好的序列化形式,也会限制类的演化;而设计不良的序列化形式,则可能会造成严重后果。

可序列化会使类的演变受到限制,因为每个可序列化的类都有一个与之关联的唯一标识符UID(serial version UID)。

UID是自动产生的,这个值受到类的名称、实现的接口及其大多数成员的影响。例如,通过添加一个临时的方法,生成的序列版本UID就会更改。

实现 Serializable 接口的第二个代价是,增加了出现bug和安全漏洞的可能性。

序列化是一种语言之外的对象创建机制。依赖默认的反序列化机制,会让对象容易收到不变性破坏和非法访问。

实现 Serializable 接口的第三个代价是,如果要发布类的新版本,相关的测试负担就会增加。

当一个可序列化的类被修改时,重要的是检查是否可以在新版本中序列化一个实例,并在旧版本中反序列化它。

如果在第一次编写类时精心设计了自定义序列化形式,那么测试的工作量就会减少,后面会讨论到

如果一个类要参与一个框架,该框架依赖于Java序列化来进行对象传输或持久化,这对于类来说实现Serializable 接口就是非常重要的。

根据经验,像BigInteger和 Instant 这样的值类实现了 Serializable 接口,集合类也实现了 Serializable 接口。表示活动实体(如线程池)的类很少情况适合实现 Serializable 接口。

为继承而设计的类应该尽量不实现 Serializable 接口,接口也应该尽量不继承 Serializable。

在为了继承而设计的类中,Throwable 类和 Component 类都实现了 Serializable 接口。正是因为 Throwable 实现了 Serializable 接口,RMI可以将异常从服务器发送到客户端,这其实是不好的。

内部类不应该实现Serializable,静态成员类可以实现这个接口。

87 考虑使用自定义的序列化形式

即如何实现一个自定义的序列化形式,阿里内部最经典的RPC框架HSF其中有一大块就是序列化和反序列化的设计,所以这项技术有很高的实战价值,这里也给出了一些建议。

🔥 先说结论:

  1. 只有当默认的序列化形式能合理描述对象的逻辑状态时,才使用默认的序列化形式
  2. 其他情况,应该设计一个自定义的序列化形式,通过它来合理地描述对象的状态

如果对象的物理表示与其逻辑内容相同,则默认的序列化形式是合适的。

例如,默认序列化形式对于Name类来说是合理的,它只表示一个人的名字:

public class Name implements Serializable { 
    /** * Last name. Must be non-null. * * @serial */
    private final String lastName;

    /** * First name. Must be non-null. * * @serial */
    private final String firstName;

    /** * Middle name, or null if there is none. * * @serial */
    private final String middleName;

    public Name(String lastName, String firstName, String middleName) { 
        this.lastName = lastName;
        this.firstName = firstName;
        this.middleName = middleName;
    }

    // Remainder omitted
}

@serial 告诉Javadoc将此文档放在一个特殊的⻚面上,该⻚面记录序列化的形式。

从逻辑上讲,名字由三个字符串组成:姓、名和中间名。Name的实例字段精确地反映了这个逻辑内容。

此外,还必须提供readObject方法来确保约束关系和安全性。对于 Name 类而言,readObject 方法必须确保字段lastName和firstName是非null的。

下面的类表示了一个字符串列表:

public final class StringList implements Serializable { 
    private int size = 0;
    private Entry head = null;
    private static class Entry implements Serializable { 
        String data;
        Entry next;
        Entry previous;
    }
    ... // Remainder omitted
}

从逻辑上讲,这个类表示字符串序列。在物理上,它将序列表示为双向链表。当对象的物理表示与其逻辑数据内容有很大差异时,使用默认的序列化形式有四个缺点:

  1. 它将导出的API永久地束缚在该类的内部实现上

在上面的例子中,私有StringList.Entry类成为公共API的一部分。如果在将来的版本中更改了实现,StringList 类仍然需要接受链表形式的输出,并产生链表形式的输出。这个类永远也摆脱不掉处理链表项所需要的所有代码,即使不再使用链表作为内部数据结构。

  1. 会占用过多的空间

这些链表项以及链接只不过是实现细节,不值得记录在序列化形式中。

  1. 会消耗过多的时间

序列化逻辑不知道对象图的拓扑结构,因此必须经过一个高开销的遍历过程。在上面的例子中,只要沿着next遍历就足够了。

  1. 可能导致堆栈溢出

StringList的合理序列化形式只需要包含列表中的字符串数量和字符串本身即可。这构成了由StringList表示的逻辑数据,去掉了其物理表示的细节。下面是修改后的StringList版本:

public final class StringList implements Serializable { 
    private transient int size = 0;
    private transient Entry head = null;
    // No longer Serializable!

    private static class Entry { 
        String data;
        Entry next;
        Entry previous;
    }

    // Appends the specified string to the list
    public final void add(String s) { }

    /** * Serialize this {@code StringList} instance. * * @serialData The size of the list (the number of strings * it contains) is emitted ({@code int}), followed by all of * its elements (each a {@code String}), in the proper * sequence. */
    private void writeObject(ObjectOutputStream s) throws IOException { 
        s.defaultWriteObject();
        s.writeInt(size);
        // Write out all elements in the proper order.
        for (Entry e = head; e != null; e = e.next)
            s.writeObject(e.data);
    }

    private void readObject(ObjectInputStream s) throws IOException,
            ClassNotFoundException { 
        s.defaultReadObject();
        int numElements = s.readInt();
        // Read in all elements and insert them in list
        for (int i = 0; i < numElements; i++)
            add((String) s.readObject());
    }
    // Remainder omitted
}

transient 修饰符表示要从类的默认序列化表单中省略该实例字段

writeObject 做的第一件事是调用defaultWriteObject,readObject 做的第一件事是调用 defaultReadObject,即使StringList 的所有字段都是transient 的。

序列化规范要求你无论如何都要调用它们。这些调用的存在使得在以后的版本中添加非transient实例字段成为可能,同时保留了向后和向前兼容性。

方法的@serialData标记告诉Javadoc实用工具将此文档放在序列化形式的文档页面上。

如果使用默认的序列化形式,并且标记了一个或多个字段为 transient,请记住,当反序列化实例 时,这些字段将初始化为默认值。如果字段不能被设置为初始值,就必须提供一个readObject方法,该方法调用defaultReadObject方法,然后将transient字段恢复为可接受的值。

另一种方法是延迟加载。

如果在反序列化对象的方法上加了同步,则也必须在对象序列化的方法上加上同步。如果你有一个线程安全的对象,它通过同步每个方法来实现线程安全,并且你选择使用默认的序列化形式,那么使用以下writeObject方法:

private synchronized void writeObject(ObjectOutputStream s) throws IOException { 
    s.defaultWriteObject();
}

无论选择哪种序列化形式,都要在编写的每个可序列化类中声明UID。只需添加一行:

private static final long serialVersionUID = randomLongValue;

一旦写好了randomLongValue之后就不要更改序列版本UID。

88 保护性地编写 readObject 方法

🔥 先说结论:

下面的几点有助于编写更健壮的 readObject 方法:

  1. 对于类中的private对象引用字段,要保护性的拷⻉这些字段中的每个对象。不可变类中的可变组件就属于这一类别
  2. 对于任何约束条件,如果检查失败就抛出一个InvalidObjectException异常。这些检查动作应该 跟在所有的保护性拷⻉之后
  3. 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口
  4. 无论是直接方法还是间接方法,都不要调用类中任何可被覆盖的方法

之前编写过一个日期类:

public class Period { 
    private final Date start;
    private final Date end;

    public Period(Date start, Date end) { 

        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());

        if (this.start.compareTo(this.end) > 0)
            throw new IllegalArgumentException(this.start + " after " + this.end);
    }

    public Date start() { 
        return new Date(start.getTime());
    }

    public Date end() { 
        return new Date(end.getTime());
    }

    public String toString() { 
        return start + " - " + end;
    }
}

假设要把这个类成为可序列化的。因为Period对象的物理表示法正好反映了它的逻辑数据内容,所以,使用默认的序列化形式是合理的。

实际上,如果仅在类的声明中增加implements Serializable,这个类就不再保证它的关键约束了。

问题在于readObject方法实际上相当于另外一个公有的构造器,它要求同其他构造器一样:必须检查其参数的有效性,并且在必要的时候对参数进行保护性拷⻉。

假设我们仅仅在Period类的声明加上了implements Serializable,这个代码可能会产生一个Period实例,他的结束时间比起始时间还早。

public class BogusPeriod { 

    private static final byte[] serializedForm = { 
            (byte) 0xac, (byte) 0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06,
            0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte) 0xf8,
            0x2b, 0x4f, 0x46, (byte) 0xc0, (byte) 0xf4, 0x02, 0x00, 0x02,
            0x4c, 0x00, 0x03, 0x65, 0x6e, 0x64, 0x74, 0x00, 0x10, 0x4c,
            0x6a, 0x61, 0x76, 0x61, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f,
            0x44, 0x61, 0x74, 0x65, 0x3b, 0x4c, 0x00, 0x05, 0x73, 0x74,
            0x61, 0x72, 0x74, 0x71, 0x00, 0x7e, 0x00, 0x01, 0x78, 0x70,
            0x73, 0x72, 0x00, 0x0e, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x75,
            0x74, 0x69, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x68, 0x6a,
            (byte) 0x81, 0x01, 0x4b, 0x59, 0x74, 0x19, 0x03, 0x00, 0x00,
            0x78, 0x70, 0x77, 0x08, 0x00, 0x00, 0x00, 0x66, (byte) 0xdf,
            0x6e, 0x1e, 0x00, 0x78, 0x73, 0x71, 0x00, 0x7e, 0x00, 0x03,
            0x77, 0x08, 0x00, 0x00, 0x00, (byte) 0xd5, 0x17, 0x69, 0x22,
            0x00, 0x78
    };

    public static void main(String[] args) { 
        Period p = (Period) deserialize(serializedForm);
        System.out.println(p);
    }

    // Returns the object with the specified serialized form
    static Object deserialize(byte[] sf) { 
        try { 
            return new ObjectInputStream(
                    new ByteArrayInputStream(sf)).readObject();
        } catch (IOException | ClassNotFoundException e) { 
            throw new IllegalArgumentException(e);
        }
    }
}

如果运行这个程序,它会打印出「Fri Jan 01 12:00:00 PST 1999 - Sun Jan 01 12:00:00 PST 1984」。

为了修整这个问题,可以为Period提供一个readObject方法,该方法首先调用defaultReadObject,然后检查被反序列化之后的对象有效性。

private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException{ 
    s.defaultReadObject();
    // Check that our invariants are satisfied
    if (start.compareTo(end) > 0)
        throw new InvalidObjectException(start +" after "+ end);
}

这里依旧隐藏着一个更为微妙的问题。通过伪造字节流,要想创建可变的Period实例仍是有可能的,做法是:

  • 字节流以一个有效的Period实例开头,然后加上两个额外的引用,指向Period实例中的start和end。攻击者从ObjectInputStream中读取Period实例,然后读取附加在其后面的引用。这些对象引用使得攻击者能够访问到Period对象内部的start和end,攻击者可以改变Period实例。
public class MutablePeriod { 
    public final Period period;
    // period's start field, to which we shouldn't have access
    public final Date start;
    // period's end field, to which we shouldn't have access
    public final Date end;

    public MutablePeriod() { 
        try { 
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);
            // Serialize a valid Period instance
            out.writeObject(new Period(new Date(), new Date()));
            /* * Append rogue "previous object refs" for internal * Date fields in Period. For details, see "Java * Object Serialization Specification," Section 6.4. */
            byte[] ref = { 0x71, 0, 0x7e, 0, 5};
            // Ref #5
            bos.write(ref);
            // The start field
            ref[4] = 4;
            // Ref # 4
            bos.write(ref);
            // The end field
            // Deserialize Period and "stolen" Date references
            ObjectInputStream in = new ObjectInputStream(
                    new ByteArrayInputStream(bos.toByteArray()));
            period = (Period) in.readObject();
            start = (Date) in.readObject();
            end = (Date) in.readObject();
        } catch (IOException | ClassNotFoundException e) { 
            throw new AssertionError(e);
        }
    }
}

运行下面的程序可以查看正在进行的攻击:

public static void main(String[] args) { 
    MutablePeriod mp = new MutablePeriod();
    Period p = mp.period;
    Date pEnd = mp.end;
    // Let's turn back the clock
    pEnd.setYear(78);
    System.out.println(p);
    
    // Bring back the 60s!
    pEnd.setYear(69);
    System.out.println(p);
}

这个程序产生的输出结果如下:

Wed Nov 22 00:21:29 PST 2017 - Wed Nov 22 00:21:29 PST 1978
Wed Nov 22 00:21:29 PST 2017 - Sat Nov 22 00:21:29 PST 1969

这个结果明显是错误的,问题的根源在于,Period 的readObject 方法并没有完成足够的保护性拷⻉。

当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,就必须做保护性拷贝。下面的这些 readObject 方法可以确保 Period 类的约束条件不会遭到破坏:

// readObject method with defensive copying and validity checking
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException{ 
    s.defaultReadObject();
    
    // Defensively copy our mutable components
    start = new Date(start.getTime());
    end = new Date(end.getTime());
    
    // Check that our invariants are satisfied
    if (start.compareTo(end) > 0)
        throw new InvalidObjectException(start +" after "+ end);
}

为了使用 readObject 方法,我们必须要将start和end字段声明成为非final的。

Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017
Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017

89 对于单例控制,枚举类型优先于readResolve

🔥 先说结论:

  1. 应该使用枚举类型来实施单例控制
  2. 如果做不到,就必须提供一个 readResolve 方法,并确保该类的所有实例化字段都是基本类型,或者是 transient 的

回忆前面的条目3,其中给出了一个单例的示例:

public class Elvis { 
    public static final Elvis INSTANCE = new Elvis();
    
    private Elvis() {  ... }
    public void leaveTheBuilding() {  ... }
}

如果在这个类上面增加 implements Serializable,它就不再是一个单例了,原因如下:

  • 任何一个 readObject 方法,都会返回一个新建的实例,这个新建的实例不同于类初始化时创建的实例

readResolve 特性允许你用 readObject 创建的实例代替另外一个实例。对于一个正在被反序列化的对象,如果它的类定义了一个 readResolve 方法,那么在反序列化之后,新建对象上的 readResolve 方法就会被调用,返回的对象将取代新建的对象。

如果 Elvis 类要实现 Serializable 接口,下面的 readResolve 方法就足以保证它的单例属性:

private Object readResolve() { 
    // Return the one true Elvis and let the garbage collector
    // take care of the Elvis impersonator.
    return INSTANCE;
}

该方法忽略了被反序列化的对象,只返回类初始化创建的那个特殊的 Elvis 实例。

如果依赖 readResolve 进行实例控制,带有对象引用类型的字段都必须声明为transient。否则,攻击者就有可能采用第88条中 MutablePeriod 类似的代码进行攻击。

以下面这个单例为例:

public class Elvis implements Serializable { 
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() { 
    }

    private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel"};

    public void printFavorites() { 
        System.out.println(Arrays.toString(favoriteSongs));
    }

    private Object readResolve() { 
        return INSTANCE;
    }
}

写一个「盗用者」类,它既有 readResolve 方法,又有实例字段,实例字段指向被序列化的单例的引用:

public class ElvisStealer implements Serializable { 
    static Elvis impersonator;
    private static final long serialVersionUID = 0;
    private Elvis payload;

    private Object readResolve() { 
        // Save a reference to the "unresolved" Elvis instance
        impersonator = payload;

        // Return object of correct type for favoriteSongs field
        return new String[]{ "A Fool Such as I"};
    }
}

在序列化流中,用「盗用者」实例代替单例的引用,现在就有了一个循环:Elvis包含ElvisStealer,ElvisStealer包含Elvis。此外,「盗用者」的 readResolve 方法执行impersonator = payload;,以便该引用可以在 readResolve 方法运行之后被访问到。

下面的代码反序列化一个手工制作的流,为Elvis产生两个截然不同的实例。

package com.wjw.effectivejava1;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class ElvisImpersonator { 

    private static final byte[] serializedForm = { 
            (byte) 0xac, (byte) 0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x05, 0x45,
            0x6c, 0x76, 0x69, 0x73, (byte) 0x84, (byte) 0xe6, (byte) 0x93, 0x33,
            (byte) 0xc3, (byte) 0xf4, (byte) 0x8b, 0x32, 0x02, 0x00, 0x01, 0x4c,
            0x00, 0x0d, 0x66, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x53,
            0x6f, 0x6e, 0x67, 0x73, 0x74, 0x00, 0x12, 0x4c, 0x6a, 0x61, 0x76,
            0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x4f, 0x62, 0x6a, 0x65,
            0x63, 0x74, 0x3b, 0x78, 0x70, 0x73, 0x72, 0x00, 0x0c, 0x45, 0x6c,
            0x76, 0x69, 0x73, 0x53, 0x74, 0x65, 0x61, 0x6c, 0x65, 0x72, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x4c,
            0x00, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x74, 0x00,
            0x07, 0x4c, 0x45, 0x6c, 0x76, 0x69, 0x73, 0x3b, 0x78, 0x70, 0x71,
            0x00, 0x7e, 0x00, 0x02
    };

    public static void main(String[] args) { 
        // Initializes ElvisStealer.impersonator and returns
        // the real Elvis (which is Elvis.INSTANCE)
        Elvis elvis = (Elvis) deserialize(serializedForm);
        Elvis impersonator = ElvisStealer.impersonator;
        elvis.printFavorites();
        impersonator.printFavorites();
    }

    static Object deserialize(byte[] sf) { 
        try { 
            return new ObjectInputStream(
                    new ByteArrayInputStream(sf)).readObject();
        } catch (IOException | ClassNotFoundException e) { 
            throw new IllegalArgumentException(e);
        }
    }
}

这个程序会产生如下的输出,最终证明可以创建两个截然不同的Elvis实例:

[Hound Dog, Heartbreak Hotel]
[A Fool Such as I]

通过将favoriteSongs字段声明为transient,可以修复这个问题,但是最好把Elvis做成一个单元素的枚举类型。如果将一个可序列化的实例受控的类编写为枚举,Java就可以绝对保证除了所声明的常量之外,不会有其他实例。

package com.wjw.effectivejava2;

import java.util.Arrays;

public enum Elvis { 
    INSTANCE;

    private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel"
    };

    public void printFavorites() { 
        System.out.println(Arrays.toString(favoriteSongs));
    }
}

readResolve的可访问性十分重要:

  • 如果把readResolve 方法放在一个final 类上,它应该是私有的
  • 如果把readResolve 方法放在一个非final 类上,就必须认真考虑它的的访问性
  • 如果它是private的,就不适用于任何一个子类
  • 如果它是default的,就适用于同一个包内的子类
  • 如果它是protected的或者是public的,并且子类没有覆盖它,对序列化的子类进行反序列化,就会产生一个超类实例,这样可能会导致 ClassCastException 异常

90 考虑用序列化代理代替序列化实例

🔥 先说结论:

  • 如果必须在一个不能被继承的类上编写readObject或writeObject方法时,应该考虑使用序列化代理模式

本篇文章一直在讨论的一件事:决定实现Serializable接口,会增加出错和出现安全问题的可能性,因为它允许利用语言之外的机制来创建实例。序列化代理模式的方法可以减少这些⻛险。

首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的逻辑状态。这个嵌套类被称为序列化代理(seralization proxy),它应该有一个单独的构造器,其参数类型就是外围类。

外围类及其序列代理都必须声明实现Serializable接口。

以第50条中编写不可变的Period 类为例:

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 = 10234098243823485285L; // Any number will do (Item 87)
}

接下来,将下面的 writeReplace 方法添加到外围类中。

private Object writeReplace() { 
    return new SerializationProxy(this);
}

writeReplace 方法在序列化之前,将外围类的实例转变成了它的序列化代理。

有了 writeReplace 方法之后,序列化系统永远不会产生外围类的序列化实例,但是攻击者有可能伪造企图违反该类约束条件的示例。为了防止此类攻击,只需要在外围类中添加如下 readObject 方法:

private void readObject(ObjectInputStream stream) throws InvalidObjectException { 
    throw new InvalidObjectException("Proxy required");
}

最后在 SerializationProxy 类中提供一个 readResolve 方法,他返回一个逻辑上等价的外围类的实例。这个方法的出现,导致序列化系统在反序列化的时候将序列化代理转为外围类的实例。

如果该类的静态工厂或者构造器建立了这些约束条件,并且它的实例方法保持着这些约束条件,你就可以确信序列化也确保着这些约束条件。

private Object readResolve() { 
    return new Period(start, end); // Uses public constructor
}

与前两种方法不同,这种方法允许 Period 类的字段为 final。

序列化代理模式允许反序列化实例有着与原始序列化实例不同的类。以 EnumSet 的情况为例,它们返回的是两种子类之一,具体取决于底层枚举类型的大小。如果底层的枚举类型有64个或者少于64个的元素,静态工厂就返回一个RegularEnumSet,否则返回一个JunmboEnumSet。

EnumSet 就是使用序列化代理模式:

private static class SerializationProxy<E extends Enum<E>> implements Serializable { 
    private static final long serialVersionUID = 362491234563181265L;

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

序列化代理模式有两个局限性:

  1. 不能被继承
  2. 如果企图从一个对象的序列化代理的readResovle方法内部调用这个对象的方法,就会得到一个ClassCastException异常,因为你还没有这个对象,只有它的序列化代理
本文为互联网自动采集或经作者授权后发布,本文观点不代表立场,若侵权下架请联系我们删帖处理!文章出自:https://wangjiawei.blog.csdn.net/article/details/122298955
-- 展开阅读全文 --
安全面试之XSS(跨站脚本攻击)
« 上一篇 07-24

发表评论

成为第一个评论的人

热门文章

标签TAG

最近回复