19 Using Portable Object Format

Using Portable Object Format (POF) has many advantages ranging from performance benefits to language independence. It's recommended that you look closely at POF as your serialization solution when working with Coherence. For information on how to work with POF when building .NET extend clients, see "Building Integration Objects for .NET Clients" in Oracle Coherence Client Guide. For information on how to work with POF when building C++ extend clients, see "Building Integration Objects for C++ Clients" in Oracle Coherence Client Guide.

The following sections are included in this chapter:

19.1 Overview

Serialization is the process of encoding an object into a binary format. It is a critical component to working with Coherence as data must be moved around the network. The Portable Object Format (also referred to as POF) is a language agnostic binary format. POF was designed to be incredibly efficient in both space and time and has become a cornerstone element in working with Coherence. For more information on the POF binary stream, see Appendix E, "The PIF-POF Binary Format."

There are several options available for serialization including standard Java serialization, POF, and your own custom serialization routines. Each has their own trade-offs. Standard Java serialization is easy to implement, supports cyclic object graphs and preserves object identity. Unfortunately, it's also comparatively slow, has a verbose binary format, and restricted to only Java objects.

The Portable Object Format has the following advantages:

  • It's language independent with current support for Java, .NET, and C++.

  • It's very efficient, in a simple test class with a String, a long, and three ints, (de)serialization was seven times faster, and the binary produced was one sixth the size compared with standard Java serialization.

  • It's versionable, objects can evolve and have forward and backward compatibility.

  • It supports the ability to externalize your serialization logic.

  • It's indexed which allows for extracting values without deserializing the whole object. See "Using POF Extractors and POF Updaters".

19.2 Working with POF

POF requires you to implement serialization routines that know how to serialize and deserialize your objects. There are two ways to do this:

19.2.1 Implementing the PortableObject interface

The PortableObject interface is a simple interface made up of two methods:

  • public void readExternal(PofReader reader)

  • public void writeExternal(PofWriter writer)

As mentioned above, POF elements are indexed. This is accomplished by providing a numeric index for each element that you write or read from the POF stream. It's important to keep in mind that the indexes must be unique to each element written and read from the POF stream, especially when you have derived types involved because the indexes must be unique between the super class and the derived class.

Example 19-1 is a simple example of implementing the PortableObject interface:

Example 19-1 Implementation of the PortableObject Interface

public void readExternal(PofReader in) 
        throws IOException 
    {
    m_symbol    = (Symbol) in.readObject(0);
    m_ldtPlaced = in.readLong(1);
    m_fClosed   = in.readBoolean(2);
    }
 
public void writeExternal(PofWriter out) 
        throws IOException 
    {
    out.writeObject(0, m_symbol);
    out.writeLong(1, m_ldtPlaced);
    out.writeBoolean(2, m_fClosed);
    }

19.2.2 Implementing the PofSerializer interface:

The PofSerializer interface provide you with a way to externalize your serialization logic from the classes you want to serialize. This is particularly useful when you do not want to change the structure of your classes to work with POF and Coherence. The PofSerializer interface is also made up of two methods:

  • public Object deserializer(PofReader in)

  • public void serialize(PofWriter out, Object o)

As with the PortableObject interface, all elements written to or read from the POF stream must be uniquely indexed. Below is an example implementation of the PofSerializer interface:

Example 19-2 Implementation of the PofSerializer Interface

public Object deserialize(PofReader in) 
        throws IOException 
    {
    Symbol symbol    = (Symbol)in.readObject(0);
    long   ldtPlaced = in.readLong(1);
    bool   fClosed   = in.readBoolean(2);
    
    // mark that reading the object is done
    in.readRemainder(null);
 
    return new Trade(symbol, ldtPlaced, fClosed);
    }
 
public void serialize(PofWriter out, Object o) 
        throws IOException 
    {
    Trade trade = (Trade) o;
    out.writeObject(0, trade.getSymbol());
    out.writeLong(1, trade.getTimePlaced());
    out.writeBoolean(2, trade.isClosed());
    
    // mark that writing the object is done
    out.writeRemainder(null);
    }

19.2.3 Assigning POF Indexes

Use the following guidelines when assigning POF indexes to an object's attributes:

  • Order your reads and writes: start with the lowest index value in the serialization routine and finish with the highest. When deserializing a value, perform reads in the same order as writes.

  • Non-contiguous indexes are acceptable but must be read/written sequentially.

  • When Subclassing reserve index ranges: index's are cumulative across derived types. As such, each derived type must be aware of the POF index range reserved by its super class.

  • Don not re-purpose indexes: to support Evolvable, it's imperative that indexes of attributes are not re-purposed across class revisions.

  • Label indexes: indexes that are labeled with a public static final int, are much easier to work with, especially when using POF Extractors and POF Updaters. See "Using POF Extractors and POF Updaters". Indexes that are labeled must still be read and written out in the same order as mentioned above.

19.2.4 Using the ConfigurablePofContext Class

Coherence provides the com.tangosol.io.pof.ConfigurablePofContext serializer class which is responsible for mapping a POF serialized object to an appropriate serialization routine (either a PofSerializer implementation or by calling through the PortableObject interface).

Once your classes have serialization routines, the classes are registered with the ConfigurablePofContext class using a POF configuration file. The POF configuration file is an XML file that has a <user-type-list> element that contains a list of classes that implement PortableObject or have a PofSerializer associated with them. The <type-id> for each class must be unique, and must match across all cluster instances (including extend clients). See Appendix D, "POF User Type Configuration Elements," for detailed reference of the POF configuration elements.

The following is an example of a POF configuration file:

<?xml version='1.0'?>

<pof-config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns="http://xmlns.oracle.com/coherence/coherence-pof-config"
   xsi:schemaLocation="http://xmlns.oracle.com/coherence/coherence-pof-config
   coherence-pof-config.xsd">
   <user-type-list>
      <include>coherence-pof-config.xml</include>

      <!-- User types must be above 1000 -->
      <user-type>
         <type-id>1001</type-id>
         <class-name>com.examples.MyTrade</class-name>
         <serializer>
            <class-name>com.examples.MyTradeSerializer</class-name>
         </serializer>
      </user-type>
 
      <user-type>
        <type-id>1002</type-id>
        <class-name>com.examples.MyPortableTrade</class-name>
      </user-type>
   </user-type-list>
</pof-config>

Note:

Coherence reserves the first 1000 type-id's for internal use. As shown in the above example, the <user-type-list> includes the coherence-pof-config.xml file that is located in the root of the coherence.jar file. This is where Coherence specific user types are defined and should be included in all of your POF configuration files.

19.2.5 Configuring Coherence to Use the ConfigurablePofContext Class

Coherence can be configured to use the ConfigurablePofContext serializer class in three different ways based on the level of granularity that is required:

  • Per Service – Each service provides a full ConfigurablePofContext serializer class configuration or references a predefined configuration that is included in the operational configuration file.

  • All Services – All services use a global ConfigurablePofContext serializer class configuration. Services that provide their own configuration override the global configuration. The global configuration can also be a full configuration or reference a predefined configuration that is included in the operational configuration file.

  • JVM – The ConfigurablePofContext serializer class is enabled for the whole JVM.

19.2.5.1 Configure the ConfigurablePofContext Class Per Service

To configure a service to use the ConfigurablePofContext class, add a <serializer> element to a cache scheme in a cache configuration file. See "serializer" for a complete reference of the <serializer> element.

The following example demonstrates a distributed cache that is configured to use the ConfigurablePofContext class and defines a custom POF configuration file:

 <distributed-scheme>
   <scheme-name>example-distributed</scheme-name>
   <service-name>DistributedCache</service-name>
   <serializer>
      <instance>
         <class-name>com.tangosol.io.pof.ConfigurablePofContext</class-name>
         <init-params>
            <init-param>
               <param-type>String</param-type>
               <param-value>my-pof-config.xml</param-value>
            </init-param>
         </init-params>
      </instance>
   </serializer>
</distributed-scheme>

The following example references the default definition in the operational configuration file. Refer to "serializer" to see the default ConfigurablePofContext serializer definition.

 <distributed-scheme>
    <scheme-name>example-distributed</scheme-name>
    <service-name>DistributedCache</service-name>
    <serializer>pof</serializer>
 </distributed-scheme>

19.2.5.2 Configure the ConfigurablePofContext Class for All Services

To globally configure the ConfigurablePofContext class for all services, add a <serializer> element within the <defaults> element in a cache configuration file. Both of the below examples globally configure a serializer for all cache scheme definitions and do not require any additional configuration within individual cache scheme definitions. See "defaults" for a complete reference of the <defaults> element.

The following example demonstrates a global configuration for the ConfigurablePofContext class and defines a custom POF configuration file:

<?xml version='1.0'?>

<cache-config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns="http://xmlns.oracle.com/coherence/coherence-cache-config"
   xsi:schemaLocation="http://xmlns.oracle.com/coherence/coherence-cache-config
   coherence-cache-config.xsd">
   <defaults>
      <serializer>
         <instance>
            <class-name>com.tangosol.io.pof.ConfigurablePofContext</class-name>
            <init-params>
               <init-param>
                  <param-type>String</param-type>
                  <param-value>my-pof-config.xml</param-value>
               </init-param>
            </init-params>
         </instance>
      </serializer>
   </defaults>
   ...

The following example references the default definition in the operational configuration file. Refer to "serializer" to see the default ConfigurablePofContext serializer definition.

<?xml version='1.0'?>

<cache-config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns="http://xmlns.oracle.com/coherence/coherence-cache-config"
   xsi:schemaLocation="http://xmlns.oracle.com/coherence/coherence-cache-config
   coherence-cache-config.xsd">
   <defaults>
      <serializer>pof</serializer>
   </defaults>
   ...

19.2.5.3 Configure the ConfigurablePofContext Class For the JVM

An entire JVM instance can be configured to use POF using the following system properties:

  • tangosol.pof.enabled=true - Enables POF for the entire JVM instance.

  • tangosol.pof.config=CONFIG_FILE_PATH - The path to the POF configuration file you want to use. If the files is not in the classpath, then it must be presented as a file resource (for example, file:///opt/home/coherence/mycustom-pof-config.xml).

19.3 Using POF Extractors and POF Updaters

In Coherence, the ValueExtractor and ValueUpdater interfaces are used to extract and update values of objects that are stored in the cache. The PofExtractor and PofUpdater interfaces take advantage of the POF indexed state to extract or update an object without the requirement to go through the full serialization/deserialization routines.

PofExtractor and PofUpdater adds flexibility in working with non-primitive types in Coherence. For most cases where you're working with extend clients, it's no longer required to have corresponding Java classes in the grid. Because POF extractors and POF updaters can navigate the binary, the entire key/value does not have to be deserialized into Object form. This implies that indexing can be achieved by simply using POF extractors to pull a value to index on. There are however circumstances where you must provide a corresponding Java class:

  • Key Association – When using key association, Coherence always deserializes keys to determine whether they implement key association.

  • Cache Stores – When using a cache store, Coherence passes the deserialized version of the key and value to the cache store to write to the back end.

19.3.1 Navigating a POF object

Due to the fact that the Portable Object Format (POF) is indexed, it's possible to quickly traverse the binary to a specific element for extraction or updating. It's the responsibility of the PofNavigator interface to traverse a POF value object and return the desired POF value object. Out of the box, Coherence provides a SimplePofPath class that can navigate a POF value based on integer indexes. In the simplest form, provide the index of the attribute to be extracted/updated.Consider the following example:

public class Contact
        implements PortableObject
    {
    ...
    // ----- PortableObject interface ---------------------------------------
 
    /**
    * {@inheritDoc}
    */
    public void readExternal(PofReader reader)
            throws IOException
        {
        m_sFirstName     = reader.readString(FIRSTNAME);
        m_sLastName      = reader.readString(LASTNAME);
        m_addrHome       = (Address) reader.readObject(HOME_ADDRESS);
        m_addrWork       = (Address) reader.readObject(WORK_ADDRESS);
        m_mapPhoneNumber = reader.readMap(PHONE_NUMBERS, null);
        }
 
    /**
    * {@inheritDoc}
    */
    public void writeExternal(PofWriter writer)
            throws IOException
        {
        writer.writeString(FIRSTNAME, m_sFirstName);
        writer.writeString(LASTNAME, m_sLastName);
        writer.writeObject(HOME_ADDRESS, m_addrHome);
        writer.writeObject(WORK_ADDRESS, m_addrWork);
        writer.writeMap(PHONE_NUMBERS, m_mapPhoneNumber);
        }
 
    ....
 
    // ----- constants -------------------------------------------------------
 
    /**
    * The POF index for the FirstName property
    */
    public static final int FIRSTNAME = 0;
 
    /**
    * The POF index for the LastName property
    */
    public static final int LASTNAME = 1;
 
    /**
    * The POF index for the HomeAddress property
    */
    public static final int HOME_ADDRESS = 2;
 
    /**
    * The POF index for the WorkAddress property
    */
    public static final int WORK_ADDRESS = 3;
 
    /**
    * The POF index for the PhoneNumbers property
    */
    public static final int PHONE_NUMBERS = 4;
 
    ...
}

Notice that there's a constant for each data member that is being written to and from the POF stream. This is an excellent practice to follow as it simplifies both writing your serialization routines and makes it easier to work with POF extractors and POF updaters. By labeling each index, it becomes much easier to think about the index. As mentioned above, in the simplest case, the work address can be pulled out of the contact by using the WORK_ADDRESS index. The SimplePofPath also allows using an Array of ints to traverse the PofValues. For example, to get the zip code of the work address use [WORK_ADDRESS, ZIP]. The example are discussed in more detail below.

19.3.2 Using PofExtractors

PofExtractors are typically used when querying a cache and should greatly improve the performance of your queries. For example, using the class demonstrated above, to query the cache for all Contacts with the last names Jones, the query is as follows:

ValueExtractor veName = new PofExtractor(String.class, Contact.LASTNAME);
Filter         filter = new EqualsFilter(veName, "Jones");
 
// find all entries that have a last name of Jones
Set setEntries = cache.entrySet(filter);

In the above use case, PofExtractor has a convenience constructor that uses a SimplePofPath to retrieve a singular index, in our case the Contact.LASTNAME index. To find all Contacts with the area code 01803, the query is as follows:

ValueExtractor veZip = new PofExtractor(
   String.class, new SimplePofPath(new int[] {Contact.WORK_ADDRESS, Address.ZIP}));
 
Filter filter = new EqualsFilter(veZip, "01803");
 
// find all entries that have a work address in the 01803 zip code
Set setEntries  = cache.entrySet(filter);

Notice that in the previous examples, the PofExtractor constructor has a first argument with the class of the extracted value or null. The reason for passing type information is that POF uses a compact form in the serialized value when possible. For example, some numeric values are represented as special POF intrinsic types in which the type implies the value. As a result, POF requires the receiver of a value to have implicit knowledge of the type. PofExtractor uses the class supplied in the constructor as the source of the type information. If the class is null, PofExtractor infers the type from the serialized state, but the extracted type may differ from the expected type. String types, in fact, can be correctly inferred from the POF stream, so null is sufficient in the previous examples. In general, however, null should not be used.

19.3.3 Using PofUpdaters

PofUpdater works in the same way as PofExtractor except that they update the value of an object rather than extract it. To change all entries with the last name of Jones to Smith, use the UpdaterProcessor as follows:

ValueExtractor veName  = new PofExtractor(String.class, Contact.LASTNAME);
Filter         filter  = new EqualsFilter(veName, "Jones");
ValueUpdater   updater = new PofUpdator(Contact.LASTNAME);

// find all Contacts with the last name Jones and change them to have the last
// name "Smith"

cache.invokeAll(filter, new UpdaterProcessor(updater, "Smith"));

Note:

while these examples operate on String based values, this functionality works on any POF encoded value.