Using XMLEncoder

   

Using XMLEncoder

by Philip Milne

This article covers advanced use of XMLEncoder, showing how it can be configured to create archives of any Java objects -- even when they don't follow the JavaBeans conventions. We include examples of how to make properties "transient" and how to create archives that call constructors with arguments, use static factory methods, and perform non-standard initialization steps. We also cover exception notification, the "owner" property (which can be used to a link an archive to the outside world), and methods for creating internationalized archives.

Introduction to XMLEncoder

XMLEncoderObjectOutputStreamSerializable
XMLEncoder e = new XMLEncoder(
    new BufferedOutputStream(
        new FileOutputStream("Test.xml")));
e.writeObject(new JButton("Hello, world"));
e.close()

XMLEncoder works by cloning the object graph and recording the steps that were necessary to create the clone. This way XMLEncoder has a "working copy" of the object graph that mimics the steps XMLDecoder would take to decode the file. By monitoring the state of this working copy, the encoder is able to omit operations that would set property values to their default value, producing concise documents with very little redundant information.

How XMLEncoder Uses Persistence Delegates

PersistenceDelegatesetPersistenceDelegate()

 

Note that while the user is effectively given the ability to replace the writeObject() methods of all classes, there are no corresponding facilities for dealing wtih readObject() methods. This is because XML documents created by XMLEncoder are programs that are interpreted by XMLDecoder against a fixed set of semantics. There is no need to worry about readObject() methods -- because there aren't any!

When XMLEncoder has no PersistenceDelegate for a given class it uses an instance of the DefaultPersistenceDelegate class to provide a default encoding strategy. The DefaultPersistenceDelegate assumes the object is a bean that follows the idioms laid out in the JavaBeans specification. The DefaultPersistenceDelegate begins by calling the no-argument constructor of the appropriate class. It then uses the Introspector class to get a list of properties for the class and writes out each of the property values as a set of statements that are applied to the new instance. When the values of properties are different to their default values the Encoder is called recursively, to write out an encoding for each of the instances it contained as a property value. This typically generates output in the following style (given in Java here for familiarity):

JButton b = new JButton();
b.setText("Press, me");
b.setName("Button1");
...

If you are writing a class that you want to serialize with XMLEncoder, one easy way to make sure that all the state will be captured by this default procedure is to make the object follow the JavaBeans conventions. If a class has a no-argument constructor and all of its state is exposed in its properties, each of which is independent of the others, you do not need to write a persistence delegate -- the default persistence delegate will be able to write out all of the state in your object automatically.

Exposing Non-Standard Property Setter and Getter Methods

If a bean has a property whose getter and setter methods do not use the conventional names (are not prefixed with "get", "is", or "set") you can supply Introspector with a BeanInfo class that returns a PropertyDescriptor with the appropriate methods. XMLEncoder will then take account of the new property and use a method call in the XML archive instead of a property. See this Swing Connection article, which describes how properties and method calls are represented in the XML schema recognized by XMLDecoder.

Making Properties Transient

BeanInfo info = Introspector.getBeanInfo(JTextField.class);
PropertyDescriptor[] propertyDescriptors =
                             info.getPropertyDescriptors();
for (int i = 0; i < propertyDescriptors.length; ++i) {
    PropertyDescriptor pd = propertyDescriptors[i];
    if (pd.getName().equals("text")) {
        pd.setValue("transient", Boolean.TRUE);
    }
}

Creating Custom Persistence Delegates

When an object's properties do not completely cover all of the state in an object or cover more of the object's state than is necessary for your application, you can use persistence delegates to ensure that the XML archives are written with just the right amount of information for the application. Note that because persistence delegates are used only in the writing part of this process, the XML output can be read by systems that do not have access to the persistence delegates used to create the output. In this sense, XMLEncoder is much more like a code generator than a conventional serialization system.

If an object has state not revealed in its properties and you can't change the API to expose this state through conventional JavaBeans idioms, you can write a persistence delegate to accommodate this information and leave the API of the class as is. The following sections discuss ways in which persistence delegates can be used to accommodate classes that are not, strictly speaking, beans.

 

Note: Due to a bug in 1.4.0, XMLEncoder can lose its references to custom persistence delegates when the VM runs low on memory. Thereafter, the custom persistence delegates won't be used. The workaround is to maintain strong references to all BeanInfos corresponding to classes that have custom persistence delegates.

The complete code for the workaround is shown in the report for bug #4646747.

Customizing Instantiation

Constructors whose arguments are properties

XMLEncoder e = new XMLEncoder(System.out);    
e.setPersistenceDelegate(Font.class,
                         new DefaultPersistenceDelegate(
                             new String[]{ "name",
                                           "style",
                                           "size" }) );
e.writeObject(new Font( ... ));

Note: In practice, Colors, Fonts, and all the properties of all of the subclasses of java.awt.Component in the Java platform are already dealt with by private persistence delegates of XMLEncoder just like the preceding one -- so you don't need to worry about creating persistence delegates for them yourself.

Constructors with arguments that are not properties

If the constructor's arguments don't all map directly to properties, then you need to create a persistence delegate with a custom instantiate() method. For example:

Encoder e = new XMLEncoder(System.out);
e.setPersistenceDelegate(Integer.class,
                         new PersistenceDelegate() {
    protected Expression instantiate(Object oldInstance,
                                         Encoder out) {
        return new Expression(oldInstance,
                              oldInstance.getClass(),
                              "new",
                              new Object[]{ oldInstance.toString() });
    }
});

The preceding implementation uses the special method name "new" to refer to the Integer class's string constructor.

Note: Like Colors and Fonts, Integers are already dealt with by private persistence delegates of XMLEncoder.

Factory methods

class MethodPersistenceDelegate extends PersistenceDelegate {
    protected Expression instantiate(Object oldInstance, Encoder out) {
        Method m = (Method)oldInstance;
        return new Expression(oldInstance,
                              m.getDeclaringClass(),
                              "getMethod",
                              new Object[]{m.getName(),
                                           m.getParameterTypes()} );
    }
}

Note: Again, XMLEncoder already has a private implementation just like the one above for the Method class.

When returning an expression in an instantiate() method it is always a good idea to provide the instance that will be created by the expression when it is evaluated. (See the API documentation for Expression.) If we did not provide oldInstance to the expression before handing it back to the encoder, it would have to actually call the method to produce the value. Not only is this less efficient, it also it strictly incorrect in the case of methods (and constructors) that return a different instance each time they are called. So while submitting such a delegate would succeed in creating an archive (albeit a little more slowly) it might not correctly preserve the identity of relationships in the graph.

Customizing Initialization

When the properties returned by the Introspector do not cover all of the important state governing an object's behavior it is possible to augment (or replace) the property-based initialization of an instance with other initialization code that captures the information that would otherwise be lost. The java.awt.Container class is just such a class since, although its principle function is to contain java.awt.Components, these components cannot be restored by setting any of its properties. Instead, one must call one of the add() methods to install each of its children.

Because some other special cases apply to the initialization of a Container, we will use Swing's DefaultListModel as a simpler example that illustrates the same point. If we were to use the default persistence delegate on an instance of the DefaultListModel we would not record all the elements it contained. To address this we could use a special persistence delegate that exploits special knowledge of the API of the DefaultListModel.

class DefaultListModelPersistenceDelegate extends DefaultPersistenceDelegate {
    protected void initialize(Class type, Object oldInstance,
                              Object newInstance, Encoder out) {
        // Note, the "size" property will be set here.
        super.initialize(type, oldInstance,  newInstance, out);

        javax.swing.DefaultListModel m =
            (javax.swing.DefaultListModel)oldInstance;

        for (int i = 0; i < m.getSize(); i++) {
            out.writeStatement(
                new Statement(oldInstance,
                              "add", // Could also use "addElement" here.
                              new Object[]{ m.getElementAt(i) }) );
        }
    }
}

Because we call the initialization method of the superclass (DefaultPersistenceDelegate), this persistence delegate still records all of the properties of a DefaultListModel, in addition to all of the elements that the list model contains. Note that, unlike a field-based approach, the archives produced by this delegate rely only on the existence of a suitable "add" method in the target VM and therefore work even when the implementation that reads an archive stores those elements differently from the VM that wrote the archive.

We can make one more refinement to this persistence delegate to cover the unusual case where an instance of DefaultListModel is found as the property of another bean and is already initialized with some elements. In this case (which specializes to the preceding one) we just add those elements that are present in the instance we are trying to replicate and not present in the instance that we are given. (Code differences are in bold font.)

class DefaultListModelPersistenceDelegate extends DefaultPersistenceDelegate {
    protected void initialize(Class type, Object oldInstance,
                              Object newInstance, Encoder out) {
        super.initialize(type, oldInstance,  newInstance, out);

        javax.swing.DefaultListModel m =
            (javax.swing.DefaultListModel)oldInstance;
         
                   javax.swing.DefaultListModel n =             (javax.swing.DefaultListModel)newInstance;

        for (int i =  
                   n.getSize(); i < m.getSize(); i++) {
            out.writeStatement(
                new Statement(oldInstance,
                              "add",
                              new Object[]{ m.getElementAt(i) }) );
        }
    }
}
                

In this example we make use of the newInstance variable, which refers to an instance in exactly the state it will be as this file is read -- just as we are about to perform these initialization steps. This policy of examining the partially initialized instance to make sure we are accommodating any state that this new instance may already have is often unnecessary, but it is a good practice in persistence delegates for classes that may be subclassed. This technique is especially effective in the internal persistence delegate of the java.awt.Container class, since it removes the need for any of the Container derivatives in the Swing tool kit to include special-case code for handling their children.

Initialization without assumptions

There are some implicit assumptions in the preceding persistence delegate: mainly that all the elements in the newInstance will exist in the instance that is being archived ( oldInstance). It is possible to write a persistence delegate that does not make any assumptions about the newInstance and will always perform exactly the operations required to initialize the newInstance -- regardless of its initial state. Persistence delegates like these are non-trivial to write and we have found delegates written in the style above to be more than adequate for the components in the AWT and Swing packages.

Persistence delegates for classes that implement List and Map

An internal persistence delegate for the AbstractList class has been provided that performs strictly correct initialization of the AbstractList class and its derivatives (including the java.util.Vector class) by not making any assumptions about initial state. The interested reader is recommended to look at the source code of the package-private java.beans.MetaData, which contains all the internal persistence delegates used by XMLEncoder -- including delegates that may be used for classes implementing the java.util.List and java.util.Map interfaces. These delegates are installed in XMLEncoder so that they will automatically be applied to all instances of java.util.AbstractList, java.util.AbstractMap, java.util.Hashtable, and their derivatives. They may be declared to apply to other classes implementing these interfaces as follows:

XMLEncoder e = new XMLEncoder(System.out);
e.setPersistenceDelegate(MyList.class,
                         e.getPersistenceDelegate(List.class));
e.writeObject( ... );

Registering for Exception Notifications

XMLEncoder e = new XMLEncoder(System.out);
e.setExceptionListener(new ExceptionListener() {
    public void exceptionThrown(Exception exception) {
        exception.printStackTrace();
    }
});
e.writeObject( ... );

 

Using the Owner Property

  • Call methods on the owner (or its properties) as the archive is being loaded.
  • Install the owner (or some property it contains) as a property of some UI element (typically as the target of an EventHander so that UI events can be used to call methods on the owner).
  • Set the properties of the owner to parts of the UI that require programmatic manipulation.
CreateUIFactorize.xmlFactorize

Creating Internationalized Applications

The XML file ResourcesExample.xml is an example of the second way. It can be loaded with the standard XMLDecoder readObject() idioms -- see the API documentation for XMLDecoder. The XML archive, instead of containing explicit strings for its user interface components, draws the strings from a resource bundle using keys. You can see this archive create a localized user interface by placing the resources directory from the Stylepad application in your class path. (Stylepad is one of the "jfc" demos in the J2SE SDK.)

ResourcesExample.java shows how you can use XMLEncoder to generate the ResourcesExample.xml archive above from the object graph and the resource bundle -- by registering the contents of the resource bundle with XMLEncoder prior to calling its writeObject() method.

Note: The features required to write such files appeared first in 1.4 Beta 2. The code base of 1.4 Beta 1 supports reading XML archives that are written this way but does not support the constructs used here to create them.

Encoding Singleton Instances: A Special Note

XMLEncoder needs special treatment for encoding of singleton instances. Without special treatment, here is what happens:

  1. Call XMLEncoder.writeObject(singletonInstance);
  2. It tries to create a new instance of the class so as to compare it against the object and decide which properties contain the default values and do not need to be encoded.
  3. Since there can only be one instance of a singleton, the two instances are always found to be identical and no properties are encoded.

To work around this problem, do this:

  1. Save the current singleton instance.
  2. Set the class singleton variable to null so that the next call to getInstance() will return a new instance with the default property values.
  3. Call XMLEncoder.writeObject() on the original instance. Encoding will now work properly, comparing a fresh instance to the original instance.
  4. Restore the class singleton instance to the original instance.

Philip Milne, a former member of the Swing team, designed and implemented Long-Term Persistence for JavaBeans. A big fan of interactive GUI builders such as NeXT's InterfaceBuilder, he is happy to continue to answer questions relating to JSR-57. He can be contacted at: philip<AT>pmilne<DOT>net

Left Curve
Java SDKs and Tools
Right Curve
Left Curve
Java Resources
Right Curve
JavaOne Banner
Java 8 banner (182)