[JAVA] Item 90: Consider serialization proxies instead of serialized instances

90. Consider serializing proxies instead of serialized instances

The serialization proxy pattern is a technique that significantly reduces the risks described in this chapter.

Serialization proxy pattern

In the serialization proxy pattern, first create a private static inner class. This inner class (called a serialization proxy) is a concise representation of the logical state of an instance of the enclosing class.

The serialization proxy has one constructor, the argument of which is the enclosing class. This constructor simply copies the data from the arguments, without the need for validation checks or defensive copies.

Intentionally, the default serialized form of the serialization proxy should be the full serialized form of the enclosing class. Both the enclosing class and the serialization proxy should implement Serializable.

The following is an example of the Period class serialization proxy seen in Item50 and Item88.

//Nested class. Dedicated for serialization.
private static class SerializationProxy implements Serializable {
    //The field can be final
    private final Date start;
    private final Date end;

    SerializationProxy(Period period) {
        //No need for defensive copy
        this.start = period.start;
        this.end = period.end;
    }

    //Set UID (Item87)
    private static final long serialVersionUID = 234098243823485285L;
}

Next, add the writeReplace method to the enclosing class. An example is below.

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

With this method, the enclosing class makes it clear to the serialization system that the serialization target is the serialization proxy.

By putting the above writeReplace in the enclosing class, the instance of the enclosing class should not be serialized, but it is necessary to protect against the case where an attacker breaks the invariant and loads the enclosing class. is there.

Therefore, put the following readObject method in the enclosing class.

//ReadObject method of serialized class
private void readObject(ObjectInputStream stream) throw InvalidObjectException {
    throw new InvalidObjectException("Proxy required");
}

This way, the serialized version of the enclosing class will not be loaded.

Finally, place the readResolve method on the serialization proxy. This readResolve method returns an instance that is logically equal to the enclosing class. Due to the existence of this method, what was converted to the serialization proxy at the time of serialization will be returned to the form of the enclosing class at the time of deserialization.

The readResolve method uses only the public API of the enclosing class to create an instance of the enclosing class, which shows the goodness of this pattern. This erases many of the features of serialization. This is because deserialized instances will be created in the same way as constructors, static factories, etc. If the constructor, static factory, etc. comply with the invariants, the deserialized instance also protects the invariants.

The readResolve method of the Period serialization proxy is as follows.

private Object readResolve() {
    return new Period(start, end);
}

Similar to the defensive copy method, serialization proxies can prevent fake byte stream attacks and field stealing attacks. Unlike the method described above, the serialization proxy allows fields to be final and classes to be immutable (Item17). Also, with serialization proxies, you don't have to think about which fields can be allowed to be attacked, and explicit validation checks during deserialization.

Another advantage of this pattern is that the instance when serialized and the instance when deserialized can have different classes. Consider EnumSet (Item36). In this class, there are no constructors, only factory methods. From the client's point of view, the factory method returns EnumSet, but either the subclass RegularEnumSet or JumboEnumSet is returned (depending on the number of elements, 64 or less is Regular). Now consider serializing an enum set with 60 elements, adding 5 elements to the enum type, and deserializing the enum set. It was a RegularEnumSet instance when it was serialized, but when it is deserialized, the JumboEnumSet is a better size. For this implementation, EnumSet actually uses the serialization proxy mechanism. Specifically, it is as follows.

    private static class SerializationProxy<E extends Enum<E>>
        implements java.io.Serializable
    {

        private static final Enum<?>[] ZERO_LENGTH_ENUM_ARRAY = new Enum<?>[0];

        /**
         * The element type of this enum set.
         *
         * @serial
         */
        private final Class<E> elementType;

        /**
         * The elements contained in this enum set.
         *
         * @serial
         */
        private final Enum<?>[] elements;

        SerializationProxy(EnumSet<E> set) {
            elementType = set.elementType;
            elements = set.toArray(ZERO_LENGTH_ENUM_ARRAY);
        }

        /**
         * Returns an {@code EnumSet} object with initial state
         * held by this proxy.
         *
         * @return a {@code EnumSet} object with initial state
         * held by this proxy
         */
        @SuppressWarnings("unchecked")
        private Object readResolve() {
            // instead of cast to E, we should perhaps use elementType.cast()
            // to avoid injection of forged stream, but it will slow the
            // implementation
            EnumSet<E> result = EnumSet.noneOf(elementType);
            for (Enum<?> e : elements)
                result.add((E)e);
            return result;
        }

        private static final long serialVersionUID = 362491234563181265L;
    }

The serialization proxy pattern has two limitations.

Also, if you use the serialization proxy pattern, the performance will be slightly degraded.

Recommended Posts

Item 90: Consider serialization proxies instead of serialized instances
Item 87: Consider using a custom serialized form
[Read Effective Java] Chapter 2 Item 1 "Consider static factory methods instead of constructors"
Item 36: Use EnumSet instead of bit fields
Item 37: Use EnumMap instead of ordinal indexing