Java serialization mechanism

The Java serialization mechanism allows converting serialized Java objects into byte sequences, which can be saved on disk, or transmitted over the network, and finally converted into original Java objects by deserialization. The byte sequence resulting from object serialization includes the object's data, information about the object's type, and the type of data stored in the object. The whole process is Java virtual machine independent, that is, an object serialized on one platform can be deserialized on another completely different platform. The classes ObjectOutputStream and ObjectInputStream are high-level data streams that contain methods for deserializing and serializing objects. For an object of a class to be serialized successfully, two conditions must be met:

1. The class must implement the java.io.Serializable interface

2. All properties of the class must be serializable. If there is a property that is not serializable, the property must be marked as ephemeral.

1. Serialization implementation.

First, call the writeObject(Object obj) method of ObjectOutputStream

public final void writeObject(Object obj) throws IOException {
        if (enableOverride) {
            writeObjectOverride(obj);
            return;
        }
        try {
            //object written to the output stream
            writeObject0(obj, false);
        } catch (IOException ex) {
            if (depth == 0) {
                writeFatalException(ex);
            }
            throw ex;
        }
}

In the method, call the writeObject0(obj, false) method to output.

   /**
     * Underlying writeObject/writeUnshared implementation.
     */
    private void writeObject0(Object obj, boolean unshared)
        throws IOException
    {
        boolean oldMode = bout.setBlockDataMode(false);
        depth++;
        try {
            // handle previously written and non-replaceable objects
            int h;
            if ((obj = subs.lookup(obj)) == null) {
                writeNull();
                return;
            } else if (!unshared && (h = handles.lookup(obj)) != -1) {
                writeHandle(h);
                return;
            } else if (obj instanceof Class) {
                writeClass((Class) obj, unshared);
                return;
            } else if (obj instanceof ObjectStreamClass) {
                writeClassDesc((ObjectStreamClass) obj, unshared);
                return;
            }

            // check for replacement object
            Object orig = obj;
            Class<?> cl = obj.getClass();
            //Details of the Class object corresponding to the serialized object
            ObjectStreamClass desc;
            for (;;) {
                // REMIND: skip this check for strings/arrays?
                Class<?> repCl;
                desc = ObjectStreamClass.lookup(cl, true);
                if (!desc.hasWriteReplaceMethod() ||
                    (obj = desc.invokeWriteReplace(obj)) == null ||
                    (repCl = obj.getClass()) == cl)
                {
                    break;
                }
                cl = repCl;
            }
            if (enableReplace) {
                Object rep = replaceObject(obj);
                if (rep != obj && rep != null) {
                    cl = rep.getClass();
                    desc = ObjectStreamClass.lookup(cl, true);
                }
                obj = rep;
            }

            // if object replaced, run through original checks a second time
            if (obj != orig) {
                subs.assign(orig, obj);
                if (obj == null) {
                    writeNull();
                    return;
                } else if (!unshared && (h = handles.lookup(obj)) != -1) {
                    writeHandle(h);
                    return;
                } else if (obj instanceof Class) {
                    writeClass((Class) obj, unshared);
                    return;
                } else if (obj instanceof ObjectStreamClass) {
                    writeClassDesc((ObjectStreamClass) obj, unshared);
                    return;
                }
            }

         
            //When serializing objects as strings, arrays, or enumerations, call the custom write method
            if (obj instanceof String) {
                writeString((String) obj, unshared);
            } else if (cl.isArray()) {
                writeArray(obj, desc, unshared);
            } else if (obj instanceof Enum) {
                writeEnum((Enum<?>) obj, desc, unshared);
            } else if (obj instanceof Serializable) {
                //2. The writing of general objects implements the Serializable interface
                writeOrdinaryObject(obj, desc, unshared);
            } else {
                //Throws an exception if the serialization interface is not implemented
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }
        } finally {
            depth--;
            bout.setBlockDataMode(oldMode);
        }
    }

Determine that the object belongs to the serialized object, and then call the writeOrnaryObject(obj, desc, unshared) method.

    /**
     * Writes representation of a "ordinary" (i.e., not a String, Class,
     * ObjectStreamClass, array, or enum constant) serializable object to the
     * stream.
     */
    private void writeOrdinaryObject(Object obj,
                                     ObjectStreamClass desc,
                                     boolean unshared)
        throws IOException
    {
        if (extendedDebugInfo) {
            debugInfoStack.push(
                (depth == 1 ? "root " : "") + "object (class \"" +
                obj.getClass().getName() + "\", " + obj.toString() + ")");
        }
        try {
            desc.checkSerialize();

            bout.writeByte(TC_OBJECT);
            //Write the description of the object
            writeClassDesc(desc, false);
            handles.assign(unshared ? null : obj);
            if (desc.isExternalizable() && !desc.isProxy()) {
                writeExternalData((Externalizable) obj);
            } else {
                //Write object serialization information
                writeSerialData(obj, desc);
            }
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }

Let's take a look at writeSerialData(obj, desc)

    /**
     * Writes instance data for each serializable class of given object, from
     * superclass to subclass.
     */
    private void writeSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;
            if (slotDesc.hasWriteObjectMethod()) {
                //If the serialized object custom implements the writeObject method, execute this code block
                PutFieldImpl oldPut = curPut;
                curPut = null;
                SerialCallbackContext oldContext = curContext;

                if (extendedDebugInfo) {
                    debugInfoStack.push(
                        "custom writeObject data (class \"" +
                        slotDesc.getName() + "\")");
                }
                try {
                    curContext = new SerialCallbackContext(obj, slotDesc);
                    bout.setBlockDataMode(true);
                    slotDesc.invokeWriteObject(obj, this);
                    bout.setBlockDataMode(false);
                    bout.writeByte(TC_ENDBLOCKDATA);
                } finally {
                    curContext.setUsed();
                    curContext = oldContext;
                    if (extendedDebugInfo) {
                        debugInfoStack.pop();
                    }
                }

                curPut = oldPut;
            } else {
                //Call the default method to write instance data
                defaultWriteFields(obj, slotDesc);
            }
        }
    }

The defaultWriteFields(obj, slotDesc) method processes the object data to be serialized, including fields of primitive types and fields of object types. The parameter ObjectStreamClass of the Method stores the information of a Class object, and its instance variables include: Class object, Class name, serialVersionUID, whether Serializable interface or Externalizable interface is implemented, non-transient modified variables, and custom Method objects of writeObject and readObject.

   /**
     * Fetches and writes values of serializable fields of given object to
     * stream.  The given class descriptor specifies which field values to
     * write, and in which order they should be written.
     */
    private void defaultWriteFields(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        Class<?> cl = desc.forClass();
        if (cl != null && obj != null && !cl.isInstance(obj)) {
            throw new ClassCastException();
        }

        desc.checkDefaultSerialize();

        int primDataSize = desc.getPrimDataSize();
        if (primVals == null || primVals.length < primDataSize) {
            primVals = new byte[primDataSize];
        }
        //Get the basic type data of the class and save it to the primVals byte array
        desc.getPrimFieldValues(obj, primVals);
        //Primitive type data of primVals is written to the underlying byte container
        bout.write(primVals, 0, primDataSize, false);

        //Get all field objects of the corresponding class
        ObjectStreamField[] fields = desc.getFields(false);
        Object[] objVals = new Object[desc.getNumObjFields()];
        int numPrimFields = fields.length - objVals.length;
        //Get the obj type data of the class and save it to the objVals byte array
        desc.getObjFieldValues(obj, objVals);
        //For all fields of type Object, loop
        for (int i = 0; i < objVals.length; i++) {
            if (extendedDebugInfo) {
                debugInfoStack.push(
                    "field (class \"" + desc.getName() + "\", name: \"" +
                    fields[numPrimFields + i].getName() + "\", type: \"" +
                    fields[numPrimFields + i].getType() + "\")");
            }
            try {
                //Each Object type field is written, recursively called
                writeObject0(objVals[i],
                             fields[numPrimFields + i].isUnshared());
            } finally {
                if (extendedDebugInfo) {
                    debugInfoStack.pop();
                }
            }
        }
    }

The above is the implementation process of Java object serialization. During use, note that static static variables and transient fields will not be serialized. If the member variable of a serialized class is an object type, the class of the object type must implement serialization, otherwise a NotSerializableException will be thrown. If the subclass implements Serializable and the parent class does not implement the Serializable interface, the parent class will not be serialized. After deserialization, it can be found that the fields of the parent class will be lost. The Java serialization mechanism verifies whether the version is consistent by judging the serialVersionUID of the class. When deserializing, the JVM compares the serialVersionUID in the incoming byte stream with the serialVersionUID of the local corresponding entity class. If they are the same, the deserialization is successful, otherwise an InvalidClassException is thrown.

Tags: Java

Posted by nepton on Fri, 03 Jun 2022 17:58:35 +0530