Advanced Serialization
Home ] Up ] Object Streams and Serialization ] Serializing Families of Classes ] Serialization and Class Versioning ] Custom Serialization and Security ] Serialized Applets ] [ Advanced Serialization ]

 

 

The Externalizable Interface

If a class implements the Externalizable interface, the ObjectInputStream and ObjectOutputStream classes use that object's readExternal() and writeExternal() methods to read and write its state during serialization.

The Externalizable interface extends from Serializable:

package java.io;

import java.io.ObjectOutput;
import java.io.ObjectInput;

public interface Externalizable extends java.io.Serializable {
    /**
     * The object implements the writeExternal method to save its contents
     * by calling the methods of DataOutput for its primitive values or
     * calling the writeObject method of ObjectOutput for objects, strings
     * and arrays.
     * @exception IOException Includes any I/O exceptions that may occur
     * @since     JDK1.1
     */
    void writeExternal(ObjectOutput out) throws IOException;

    /**
     * The object implements the readExternal method to restore its
     * contents by calling the methods of DataInput for primitive
     * types and readObject for objects, strings and arrays.  The
     * readExternal method must read the values in the same sequence
     * and with the same types as were written by writeExternal.
     * @exception ClassNotFoundException If the class for an object being
     *              restored cannot be found.
     * @since     JDK1.1
     */
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

By implementing Externalizable, a class can control whether the state of superclasses is stored in the stream, and exactly which fields are stored. Only the identity of an Externalized object is automatically saved by the stream; the class is responsible for writing and reading its contents, and must coordinate with its superclasses to do so.

Unlike Serializable, the Externalizable interface does not handle code versioning automatically -- you must provide your own.

Because the state of an Externalizable object's superclasses can be indirectly manipulated by the writeExternal() and readExternal() methods, which are public, you should use Externalizable with extreme caution to avoid security problems.

The ObjectInputValidation Interface

Sometimes you go to great pains to construct a set of objects correctly, usually by putting the appropriate code in the class constructor(s). However, because restoring a set of Serializable objects does not invoke their constructors, it is often desireable to ensure that the reconstructed object is indeed valid. The ObjectInputValidation interface provides such capabilities.

Here is what it looks like:

package java.io;

/**
 * Callback interface to allow validation of objects within a graph.
 * Allows an object to be called when a complete graph of objects has
 * been deserialized.
 */
public interface ObjectInputValidation {
    /**
     * Validates the object
     * @exception InvalidObjectException If the object cannot validate itself.
     * @since     JDK1.1
     */
    public void validateObject() throws InvalidObjectException;
}

ObjectInputStream uses a registration scheme to allow you to register an ObjectInputValidation object during serialization in.

For example, if we take the RecordedPerson class we used to do custom serialization earlier, and add to it support for validation, we get the following class, with color-coded changes:

package serialization;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.InvalidObjectException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectInputValidation;
import java.io.PrintWriter;

public class RecordedPerson 
    extends Person
    implements ObjectInputValidation
{
    // Constructor to create a recorded person and 
    // the associated record file
    public RecordedPerson(String name, int age, int id)
    {
        super(name, age);
        m_id = id;
        createRecordFile();
    }
    
    // Constructor to create a recorded person
    // and to open an already existing record file.
    public RecordedPerson(int id)
    {
        super(null, -1);
        m_id = id;
        setRecordFile();
        setBasicData();
    }
    
    // Reads the record from the record file
    // and returns it as a string.
    public String getRecord()
    {
        String data = "";
        BufferedReader in = null;
        try
        {
            in = new BufferedReader(
                         new FileReader(m_recordFile));
            while(true)
            {
                String line = in.readLine();
                if (line == null)
                    break;
                data += line + "\n";
            }
        }
        catch(IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (in != null)
            {
                try
                {
                    in.close();
                }
                catch(IOException e)
                {}
            }
        }
        return data;
    }
    
    // Adds a new item to the record file for this person
    public void addRecord(String item)
    {
        PrintWriter out = null;
        try
        {
            out = new PrintWriter(
                        new FileWriter(
                                m_recordFile.getAbsolutePath(),
                                true));
            out.println(item);
        }
        catch(IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (out != null)
                out.close();
        }
    }
    
    // Creates a new record file with name and age record
    // (If the record file already exists, assumes that it's valid)
    private void createRecordFile()
    {
        setRecordFile();
        if (!m_recordFile.exists())
        {
            // Create a new record file
            PrintWriter out = null;
            try
            {
                out = new PrintWriter(
                                new FileWriter(m_recordFile));
                out.println(getName() + "," + getAge());
            }
            catch(IOException e)
            {
                e.printStackTrace();
            }
            finally
            {
                if (out != null)
                    out.close();
            }
        }
        else
        {
            // Assume for simplicity that the name and age
            // actually correspond with the current ones.
        }
    }
    
    // Sets the record File object
    // If necessary, creates the subdirectory for record files.
    private void setRecordFile()
    {
        File records = new File("records");
        if (!records.exists())
            records.mkdir();    // Create directory
        m_recordFile = new File(records, "r" + m_id + ".rec");
    }
    
    // Reads the record file and extracts the name and age
    // and sets those values for the current recorded person.
    private void setBasicData()
    {
        // Extract the name and age from the record
        String record = getRecord();
        int commaIndex = record.indexOf(',');
        int eolIndex = record.indexOf('\n');
        String name = record.substring(0, commaIndex);
        String age = record.substring(commaIndex+1, eolIndex);
        // Set the name and age values
        setName(name);
        setAge(Integer.parseInt(age));
    }
    
    // Creates a string containing RecordedPerson information.
    public String toString()
    {
        String s = super.toString();
        s += ", ID=" + m_id + ", recordFile=" + m_recordFile;
        return s;
    }
    
    private void readObject(ObjectInputStream in)
        throws IOException,
                ClassNotFoundException
    {
        in.registerValidation(this, 0);
        in.defaultReadObject();
        // Ensure that the recordFile is properly set
        setRecordFile();
    }
    
    public void validateObject() 
        throws InvalidObjectException
    {
        if (getAge() > 40)
            throw new InvalidObjectException("No persons over 40 allowed");
    }
    
    //// Data ////
    private int   m_id;
    private transient File  m_recordFile;
}

Now, when we attempt to serialize this class back in, we now get the following:

java.io.InvalidObjectException: No persons over 40 allowed
    at serialization.RecordedPerson.validateObject(RecordedPerson.java:175)
    at java.io.ObjectInputStream.doValidations(ObjectInputStream.java:548)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:437)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:225)
    at serialization.RecordedSerializeIn.main(RecordedSerializeIn.java:18)
    at symantec.tools.debug.MainThread.run(Agent.java:48)
Serialization failed.
 
The page was last updated February 19, 2008