Adavanced Serialization - Online Article

What is Serialization ?

The basic concept of object serialization is the ability to read and write objects to byte streams. These streams can be used for saving session state information by servlets, for sending parameters for Remote Method Invocation (RMI) calls, for saving state information about JavaBean technology components, and for sending objects over the network, as well as many other tasks.

To do object serialization you use an ObjectOutputStream to save your objects and an ObjectInputStream to read them back. The serialization process deals with flattening out an object tree such that all the raw data that make up an object, including all the objects referenced by that object, get saved. And, when it's time to read back an object, the original object tree graph gets recreated. Special cases like circular references and multiple references to a single object are preserved such that when the tree graph gets recreated new objects don't magically appear where a reference to another object in the tree should be.

In order for an object to support the serialization process, the class must implement the Serializable interface. There are no methods in the interface; the interface just serves as a marker to say that a class can be serialized. However, just adding implements Serializable to a class definition doesn't automatically make a class Serializable. All the instance variables of said class must be Serializable, also. If they aren't and you try to serialize the class an exception would be thrown. To mark an instance variable as not for serialization, you add the transient keyword to the definition. Classes like java.awt.Image and java.lang.Thread, which contain platform-specific implementation information, are not Serializable and should be marked as transient.

Transient Image: When you serialize an instance of a class, the only things that are saved are the non-static and non-transient instance data. Class definitions are not saved. They must be available when you try to deserialize an object.

The basic process of serializing an object is as follows:



ObjectOutputStream oos = 
new ObjectOutputStream(anOutputStream);
Serializable serializableObject = ...
oos.writeObject(serializableObject);
 


Then, to read the the object back, deserialization, you do the reverse:

ObjectInputStream ois = 
new ObjectInputStream(anInputStream);
Object serializableObject = ois.readObject();
 


The other thing that is frequently explained about serialization is overriding the readObject and writeObject methods to change the default serialization behavior. By overriding these methods you can provide additional information to the output stream, to be read back in at deserialization time:

private void writeObject(ObjectOutputStream oos)
  throws IOException {
  oos.defaultWriteObject();
  // Write/save additional fields
  oos.writeObject(new java.util.Date());
}

// assumes "static java.util.Date aDate;" declared
private void readObject(ObjectInputStream ois)
  throws ClassNotFoundException, IOException {
  ois.defaultReadObject();
  // Read/initialize additional fields
  aDate = (java.util.Date)ois.readObject();
}

One common reason to override readObject and writeObject is to serialize the data for a superclass that is not Serializable itself.

And, this is where most serialization explanations end.

Validating Streams:: When you serialize data to a file or send data across a socket there is no guarantee what you receive is what was written. In the case of saving to a file, a corrupt user can try to use a byte-level editor to change around the bytes to alter the values for when the object is restored.

With serialization, a class can provide the ObjectInputStream parameter to readObject with an ObjectInputValidation interface implementer to indicate a desire to perform validation after an object is fully restored. The interface consists of a single method: public void validateObject() throws InvalidObjectException. When implemented, the class's readObject method must register the validator with the ObjectInputStream through registerValidation(ObjectInputValidation, int), the last argument of which is the validator priority, in case there are multiple (higher values are called first):



private void readObject(ObjectInputStream ois)
  throws ClassNotFoundException, IOException {
  ...
  ois.registerValidation(validator, 0);
  ...
}
 


To demonstrate, the following class uses validation. If either of the two instance variables is the number 6 the read will fail. The test program, ValidationExample, gets the value for the two instance variables from the command line.

import java.io.*;

public class ValidationExample 
  implements Serializable, ObjectInputValidation {
  private int x, y;
  public static void main(
  String args[]) throws Exception {

  if (args.length != 2) {
  System.err.println(
  "Please pass in two numbers");
  System.exit(-1);
  }

  // Initialize object
  ValidationExample ve = new ValidationExample();
  try {
  ve.x = Integer.parseInt(args[0]);
  ve.y = Integer.parseInt(args[1]);
  } catch (NumberFormatException e) {
  System.err.println(
  "Please pass in two numbers");
  System.exit(-1);
  }

  FileOutputStream fos =
  new FileOutputStream("val.ser");
  ObjectOutputStream oos =
  new ObjectOutputStream(fos);
  oos.writeObject(ve);
  oos.close();

  try {
  FileInputStream fis = 
  new FileInputStream("val.ser");
  ObjectInputStream ois =
  new ObjectInputStream(fis);
  ValidationExample ve2 =
  (ValidationExample)ois.readObject();
  ois.close();
  System.out.println(ve2);
  } catch (InvalidObjectException invalid) {
  System.err.println(invalid.getMessage());
  }
  }

  public String toString() {
  return getClass().getName(
  ) + "[x=" + x + ",y=" + y + "]";
  }

  private void readObject(ObjectInputStream ois)
  throws ClassNotFoundException,
  IOException {
  ois.registerValidation(this, 0);
  ois.defaultReadObject();
  }

  public void validateObject() 
  throws InvalidObjectException {
  if ((x == 6) || (y == 6)) {
  throw new InvalidObjectException(
  "6 is an invalid entry. Can't restore.");
  }
  }
}
 


Using ObjectStreamField: There are two ways to define what fields get streamed when an object is serialized. By default, every non-static and non-transient field is preserved. However, if your class defines an array of ObjectStreamField objects named serialPersistentFields (that happens to be private, static, and final), then you can explicitly declare the specific fields saved. The order you place fields in the array is the order in which they are written. For instance, in the following class, only the username and counter fields are serialized, not the password.



public class MyClass implements Serializable {
  private String username;
  private int counter;
  private String password;

  private final static ObjectStreamField[]
  serialPersistentFields = {
  new ObjectStreamField(
  "username", String.class),
  new ObjectStreamField("counter", int.class)
  };
  ...
}
 


By default, no customization of readObject and writeObject is necessary when you provide a serialPersistentFields setting.

Where serialPersistentFields becomes useful is in class evolution. For instance, imagine the original version of a class with two fields:

Point point;
Dimension dimension;
 


Now, imagine version two of the class with only one field:

Rectangle rectangle;
 


If you want bidirectional serialization from either version of the class to either version of the class, you can create a serialPersistentFields in the second version to map the rectangle to the point and dimension:

private static final
ObjectStreamField[] serialPersistentFields = {
  new ObjectStreamField("point", Point.class),
  new ObjectStreamField("dimension", Dimension.class)
};
 


Then, in readObject and writeObject you have to do the actual mapping:

private void readObject(ObjectInputStream ois)
  throws ClassNotFoundException, IOException {

  // Read version one types
  ObjectInputStream.GetField fields =
  ois.readFields();
  Point point = (Point)fields.get("point", null);
  Dimension dimension =
  (Dimension)fields.get("dimension", null);

  // Convert to version two type
  rectangle = new Rectangle(point, dimension);
}

private writeObject(ObjectOutputStream oos)
  throws IOException {

  // Convert to version one types
  ObjectOutputStream.PutFields fields =
  oos.putFields();
  fields.put("point", rectangle.getLocation());
  fields.put("dimension", rectangle.getSize());

  // Write version one types
  oos.writeFields();
}
 


The version two class must also have the same serialVersionUID as the first version of the class. Just execute the serialver command on the original class version before making the change.

For a complete example of an evolving class using serialPersistentFields, see Using Serialization and the Serializable Fields API.

Encrypting Serialized Objects: The Java Cryptography Extension (JCE) provides support for encryption. With regards to serialization, you can use JCE to either encrypt everything along a stream with a CipherOutputStream, or you can seal individual objects with a Cipher through the SealedObject class. Usually, you'd use CipherOutputStream and encrypt a whole stream. However, SealedObject allows you to wrap any Serializable object into a sealed one for later usage, perhaps to save in a servlet session.

To demonstrate, the following example uses a CipherOutputStream to encrypt a series of objects written to disk, then uses a CipherInputStream to decrypt the file and read the objects back. The cipher streams act just like any other filtering streams: just add the stream between the file and the object streams. The hardest part of the code is creating the key for the encryption/decryption Cipher.



import java.io.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import java.awt.*;

public class CipherExample {
  // Password must be at least 8 characters
  private static final String password =
  "zukowski";

  public static void main(String args[]
  ) throws Exception {
  Point point = new Point(100, 200);
  Dimension dim = new Dimension(300, 400);
  Rectangle rect = new Rectangle(point, dim);

  // Create Key
  byte key[] = password.getBytes();
  DESKeySpec desKeySpec = new DESKeySpec(key);
  SecretKeyFactory keyFactory =
  SecretKeyFactory.getInstance("DES");
  SecretKey secretKey =
  keyFactory.generateSecret(desKeySpec);

  // Create Cipher
  Cipher desCipher =
  Cipher.getInstance("DES/ECB/PKCS5Padding");
  desCipher.init(Cipher.ENCRYPT_MODE, secretKey);

  // Create stream
  FileOutputStream fos =
  new FileOutputStream("out.des");
  BufferedOutputStream bos =
  new BufferedOutputStream(fos);
  CipherOutputStream cos =
  new CipherOutputStream(bos, desCipher);
  ObjectOutputStream oos =
  new ObjectOutputStream(cos);

  // Write objects
  oos.writeObject(point);
  oos.writeObject(dim);
  oos.writeObject(rect);
  oos.flush();
  oos.close();

  // Change cipher mode
  desCipher.init(Cipher.DECRYPT_MODE, secretKey);

  // Create stream
  FileInputStream fis =
  new FileInputStream("out.des");
  BufferedInputStream bis =
  new BufferedInputStream(fis);
  CipherInputStream cis =
  new CipherInputStream(bis, desCipher);
  ObjectInputStream ois =
  new ObjectInputStream(cis);

  // Read objects
  Point point2 = (Point)ois.readObject();
  Dimension dim2 = (Dimension)ois.readObject();
  Rectangle rect2 = (Rectangle)ois.readObject();
  ois.close();

  // Compare original with what was read back
  int count = 0;
  if (point.equals(point2)) {
  System.out.println("Points are okay.");
  count++;
  }
  if (dim.equals(dim2)) {
  System.out.println("Dimensions are okay.");
  count++;
  }
  if (rect.equals(rect2)) {
  System.out.println("Rectangles are okay.");
  count++;
  }
  if (count != 3) {
  System.out.println(
  "Problem during encryption/decryption");
  }
  }
}
 


The second form of encryption is sealing an object. This is done by calling the constructor for SealedObject, just passing the constructor the serializable object to seal and the Cipher to use for encryption:



  SealedObject sealedObject =
  new SealedObject(serializable, cipher);
 


When it is time to unseal the object, there are three getObject methods you can use:



getObject(Cipher c) 
getObject(Key key) 
getObject(Key key, String provider) 

Each of these unsealing methods works like the readObject method from ObjectInputStream -- you have to cast the object returned to the appropriate type. Which method you use depends upon the circumstances, though the second is probably the most frequently used, as it means the decrypter does not need to know the encryption parameters. The first version is also commonly used for when a Cipher object is shared within an application.

Here's an example that seals and unseals an AWT Rectangle object.



import java.io.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import java.awt.*;

public class SealedExample {
  // Password must be at least 8 characters
  private static final String password = "zukowski";

  public static void main(String args[]
  ) throws Exception {
  Point point = new Point(100, 200);
  Dimension dim = new Dimension(300, 400);
  Rectangle rect = new Rectangle(point, dim);

  // Create Key
  byte key[] = password.getBytes();
  DESKeySpec desKeySpec =
  new DESKeySpec(key);
  SecretKeyFactory keyFactory =
  SecretKeyFactory.getInstance("DES");
  SecretKey secretKey =
  keyFactory.generateSecret(desKeySpec);

  // Create Cipher
  Cipher desCipher = 
  Cipher.getInstance("DES/ECB/PKCS5Padding");
  desCipher.init(Cipher.ENCRYPT_MODE, secretKey);

  // Seal object
  SealedObject sealedObject =
  new SealedObject(rect, desCipher);

  // Change cipher mode
  desCipher.init(Cipher.DECRYPT_MODE, secretKey);

  // Unseal object
  Rectangle rect2 = 
  (Rectangle)sealedObject.getObject(secretKey);

  // Just print each out
  System.out.println(rect);
  System.out.println(rect2);
  }
}
 


Other Pointers: There is much more going on with serialization than explained here. The fact that the Java Object Serialization Specification hasn't changed since the 1.2 release of the Java 2 platform doesn't help the situation, with changes to serialization in both the 1.3 release and 1.4 release just increasing the enhancements.

One thing that hasn't changed since the beginning though is turning serialization off. Sometimes, when you extend from a class that already extends Serializable, you just don't want your class to be serializable also. In those cases, all you have to do is throw NotSerializableException from your readObject and writeObject methods:



private void readObject(ObjectInputStream ois)
  throws ClassNotFoundException, IOException {
  throw new NotSerializableException();
}

private void writeObject(ObjectOutputStream ois)
  throws IOException {
  throw new NotSerializableException();
}
 

Conclusion

In this article you learned about some of the extra features available for object serialization. While you may not always need all these features, it is good to know and understand the options that are available so that, when necessary, the capabilities can be designed into your systems.

About the Author:

No further information.




Comments

No comment yet. Be the first to post a comment.