Customizing Object Serialization

   

Sometimes it is useful, or even necessary, to control how an individual object is serialized. If for instance you want to encrypt the data values held by the object's attributes, you would not want to use the default serialization mechanisms.

To override how an object is serialized or deserialized, you must implement two methods in your class with these exact signatures:

 private void writeObject(java.io.ObjectOutputStream out)      throws IOException  private void readObject(java.io.ObjectInputStream in)      throws IOException, ClassNotFoundException; 

You might have noticed that the Serializable interface does not define any methods. If you look back at the Employee class from listing 22.4, no methods had to be implemented when you made the Employee class implement the Serializable interface. Because writeObject() and readObject() are not in the Serializable interface, the serialization system calls these methods by using Reflection. Reflection essentially allows programs to access methods and constructors of components based on knowing their signature. Reflection is generally a complicated API, and for most of your programs, you won't need to be concerned with actually getting Reflection to work. However, you do need to know that Reflection requires the signatures of the methods to be exact. Therefore, it is critical that you use exactly these signatures. Failure to make the methods private will cause the serialization mechanism to use its default algorithms, which is not what you want when implementing these two methods.

See Chapter 28, "Reflection."

If your classes subclass another class, they do not need to be concerned with calling super.writeObject() or super.readObject() on the parent object. You also don't need to be concerned about how subclasses will serialize the class, because each portion of the object will be handled separately by the serialization mechanism.

On the other hand, if you want to use the default mechanism within the writeObject() method, you can do so by calling out.defaultWriteObject(). Or from the readObject() method, you can call in.defaultReadObject(). This method uses the default serialization mechanism to write out the part of object that can be serialized. Then you can handle the specifics in a custom manner.

Listing 22.6 contains a class called User that implements the readObject and writeObject methods. The class does this so that it can encrypt the string values for the username and password values for an object. The encryption routine just reverses the values before writing out the data and then reverses them back after reading them in. This obviously does not provide an ounce of real encryption, but it's not the point of the example.

Listing 22.6 Source Code for User.java
 import java.io.*; public class User implements Serializable {   // Private instance variables   private String userName = null;   private String password = null;   // Constructor   public User( String name, String passwd )   {     super();     userName = name;     password = passwd;   }   //Public accessors   public String getUserName()   {     return userName;   }   public void setUserName( String name )   {     userName = name;   }   public String getPassword()   {     return password;   }   public void setPassword( String passwd )   {     password = passwd; }   // Implement the necessary writeObject method for handling the   // serialization of this object other than the default way   private void writeObject( ObjectOutputStream out ) throws IOException   {     String userNameEncrypted = encryptStr( getUserName() );     String passwordEncrypted = encryptStr( getPassword() );     out.writeObject( userNameEncrypted );     out.writeObject( passwordEncrypted );   }   // Implement the necessary readObject method for handling the   // serialization of this object other than the default way   private void readObject( ObjectInputStream in )     throws IOException, ClassNotFoundException   {     String userNameEncrypted = (String)in.readObject();     String passwordEncrypted = (String)in.readObject();     setUserName( decryptStr( userNameEncrypted ));     setPassword( decryptStr( passwordEncrypted ));   }   // Extremely simple encryption routine just to show the point   // of something being done with the data   public String encryptStr( String normalStr )   {     StringBuffer buf = new StringBuffer( normalStr );     buf.reverse();     return buf.toString();   }   // Simple decryption routine   public String decryptStr( String encryptStr )   {     StringBuffer buf = new StringBuffer( encryptStr );     buf.reverse();     return buf.toString();   }   // Override the toString() method from Object to pretty print a User object   public String toString()   {     StringBuffer strBuf = new StringBuffer( "User: " );     strBuf.append( getUserName() );     strBuf.append( " Password: " );     strBuf.append( getPassword() );     return strBuf.toString();   } } 

Listing 22.7 is a class called UserTest that can be used to test the serialization of the User class. It creates several instances of the User class and serializes them to a file. It then reads them back in and prints out the objects to the console.

Listing 22.7 Source Code for UserTest.java
 import java.io.*; public class UserTest {   // Constant for the user data filename public static final String USER_DATA_FILE = "users.dat";   public static void main(String[] args)   {     // Create a couple of User objects     User user1 = new User( "User1", "foobar" );     User user2 = new User( "admin", "admin" );     User user3 = new User( "guest", "guest" );     try     {       // Create the streams to write the objects out       // Notice there is really no difference here. The real       // difference is in the User class       FileOutputStream out = new FileOutputStream( USER_DATA_FILE );       ObjectOutputStream objOutStr = new ObjectOutputStream( out );       objOutStr.writeObject( user1 );       objOutStr.writeObject( user2 );       objOutStr.writeObject( user3 );       // Read the users back in       FileInputStream in = new FileInputStream( USER_DATA_FILE );       ObjectInputStream objInStr = new ObjectInputStream( in );       // Print all of the User's out       while( true )       {         System.out.println( objInStr.readObject() );       }     }     catch( EOFException ex )     {       System.out.println( "All records have been read" );     }     catch( FileNotFoundException ex )     {       System.out.println( "Could not find the file " + USER_DATA_FILE );     }     catch( IOException ex )     {       System.out.println(  "There was a problem with serialization of the users" ); }     catch( ClassNotFoundException ex )     {       System.out.println( "Could not find the User class definition" );     }   } } 

Running the UserTest class produces this output:

 C:\jdk1.3se_book\classes>java UserTest User: User1 Password: foobar User: admin Password: admin User: guest Password: guest All records have been read C:\jdk1.3se_book\classes> 

Using the Transient Keyword

In the previous example, the user's login and password were written out to the file. Although the encryption routine was primitive, even the better encryption routines can be broken in some amount of time. There are techniques that have been around for a while that enable encryption routines to be broken in a relative amount of time. The real question here is whether the password field should even be written out. What if there are other fields that are sensitive that should not be serialized? Fortunately, that's where the transient keyword can help. By declaring a field as transient, the value will not be serialized along with the rest of the object.

Note

If you have written your own serialization using the approach from Listing 22.8, you must make sure that you don't write out the fields that you don't want to be serialized. The transient keyword will not prevent it in this case. You must handle it manually.


Listing 22.8 is almost exactly the same as the Employee class from Listing 22.4. The only change was adding the transient keyword to the payRate attribute so that it will not be serialized out to the file. The name of the class has also been renamed to PrivateEmployee to keep the two classes separate for comparison.

Listing 22.8 Source Code for PrivateEmployee.java
 public class PrivateEmployee implements java.io.Serializable {   // Private instance variables for an employee   private String firstName = null;   private String lastName = null;   transient private float payRate;   private int hoursWorked;   // Constructor   public PrivateEmployee( String first, String last, float rate, int hours )   {     super();     firstName = first;     lastName = last;     payRate = rate;     hoursWorked = hours;   }   // Override the toString() method from object to display an employee   public String toString()   {     StringBuffer strBuf = new StringBuffer( "First: " );     strBuf.append( getFirstName() );     strBuf.append( " Last: " );     strBuf.append( getLastName() );     strBuf.append( " Rate: " );     strBuf.append( getPayRate() );     strBuf.append( " Hours: " );     strBuf.append( getHoursWorked() );     return strBuf.toString();   }   // Public Getters for the object's state   public String getFirstName()   {     return firstName;   }   public String getLastName()   {     return lastName;   }   public float getPayRate()   {     return payRate;   }   public int getHoursWorked()   {     return hoursWorked;   } } 

The EmployeeTest class from before has been changed to use the new PrivateEmployee class. Listing 22.9 shows this new test class.

Listing 22.9 Source Code for PrivateEmployeeTest.java
 import java.io.*; public class PrivateEmployeeTest {   // Used to keep from hard-coding the filename in multiple places   public static final String EMPLOYEE_FILE = "employees.dat";   public static void main(String[] args)   {     // Create a few employees     PrivateEmployee employee1 = new PrivateEmployee( "Zachary", "Cavaness", 10.25f, 40 );     PrivateEmployee employee2 = new PrivateEmployee( "Joshua", "Cavaness", 10.25f, 40 );     PrivateEmployee employee3 =  new PrivateEmployee( "Tracy", "Cavaness", 21.50f, 40 );     try     {       // Create the File and Object streams to serialize to the file       FileOutputStream out =  new FileOutputStream( EmployeeTest.EMPLOYEE_FILE ); ObjectOutputStream objOutStr = new ObjectOutputStream( out );       objOutStr.writeObject( employee1 );       objOutStr.writeObject( employee2 );       objOutStr.writeObject( employee3 );       // close the open resources       objOutStr.close();       out.close();     }     catch( FileNotFoundException ex )     {       System.out.println( "Could not create the employee file" );       System.exit(-1);     }     catch( IOException ex )     {       System.out.println( "Could not serialize the employees" );       System.exit(-1);     }     // Read the employees back in     try     {       Object obj = null;       // Create the file and object streams       FileInputStream in =  new FileInputStream( PrivateEmployeeTest.EMPLOYEE_FILE ); ObjectInputStream objInStr = new ObjectInputStream( in );      // In this example, you keep going until the EOFException is       // raised and then you catch that exception and stop while( true )       {         obj = objInStr.readObject();         System.out.println( obj );       }     }     catch( EOFException ex )     {       System.out.println( "All records have been read in" );     }     catch( FileNotFoundException ex )     {       System.out.println( "Could not  open the file " + PrivateEmployeeTest.EMPLOYEE_FILE graphics/ccc.gif );     }     catch( IOException ex )     {       System.out.println( "Could not serialize the employees" );     }     catch( ClassNotFoundException ex )     {       System.out.println(  "Could not find the class definition for serialized class"); }   } } 

The only changes from the previous test class are that references to EmployeeTest have been changed to PrivateEmployee and the actual name of the test class was changed to PrivateEmployeeTest. Other than those two changes, the test class is the same. Running the PrivateEmployeeTest class, the output should look like this:

 First: Zachary Last: Cavaneess Rate: 0.0 Hours: 40 First: Joshua Last: Cavaneess Rate: 0.0 Hours: 40 First: Tracy Last: Cavaneess Rate: 0.0 Hours: 40 All records have been read in C:\jdk1.3se_book\classes> 

Notice that the rate values are 0.0 for all three users. This is because the rate was never serialized to the file and when the deserialization occurred, the payRate attribute was set to the initial values for the float primitive, which is 0.0.

   


Special Edition Using Java 2 Standard Edition
Special Edition Using Java 2, Standard Edition (Special Edition Using...)
ISBN: 0789724685
EAN: 2147483647
Year: 1999
Pages: 353

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net