Migrating Externalizable object to a versioned implementatio
starksm64 Jul 13, 2005 8:01 PMIn looking into an issue I found that some pooled invoker changes had not been migrated to the 4.0 branch, and as part of this I wanted to externalize the current hard-coded retry connection count. This would require adding additional data to the serialized output. In order for this to be done in a backward compatible fashion, the additional data would have to come after the existing wire format. Here is an example of how a proxy class was evolved to support a version and retryCount and still have forward/backward compatibility with the previous version.
// The original version
package io.serial; import java.io.Externalizable; import java.io.ObjectOutput; import java.io.IOException; import java.io.ObjectInput; public class ProxyV1 implements Externalizable { /** The serialVersionUID @since 1.1.4.3 */ private static final long serialVersionUID = 1L; public ServerAddress address; public int maxPoolSize; public ProxyV1() { } public ProxyV1(ServerAddress sa, int maxPoolSize) { this.address = sa; this.maxPoolSize = maxPoolSize; } public void writeExternal(final ObjectOutput out) throws IOException { out.writeObject(address); out.writeInt(maxPoolSize); } public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException { address = (ServerAddress)in.readObject(); maxPoolSize = in.readInt(); } }
// Version 2 of the proxy
package io.serial; import java.io.Externalizable; import java.io.ObjectOutput; import java.io.IOException; import java.io.ObjectInput; import java.io.OptionalDataException; import java.io.EOFException; public class ProxyV2 implements Externalizable { /** The serialVersionUID @since 1.1.4.3 */ private static final long serialVersionUID = 1L; private static final int WIRE_VERSION = 1; public ServerAddress address; public int maxPoolSize; public int retryCount; public ProxyV2() { } public ProxyV2(ServerAddress sa, int maxPoolSize) { this(sa, maxPoolSize, 1); } public ProxyV2(ServerAddress sa, int maxPoolSize, int retryCount) { this.address = sa; this.maxPoolSize = maxPoolSize; this.retryCount = retryCount; } public void writeExternal(final ObjectOutput out) throws IOException { out.writeObject(address); out.writeInt(maxPoolSize); out.writeInt(WIRE_VERSION); out.writeInt(retryCount); } public void readExternal(final ObjectInput in) throws IOException, ClassNotFoundException { address = (ServerAddress)in.readObject(); maxPoolSize = in.readInt(); int version = 0; try { version = in.readInt(); } catch(EOFException e) { // No version written and there is no more data } catch(OptionalDataException e) { // No version written and there is data from other objects } switch( version ) { case 0: // This has no retryCount, default it to the hard-coded value retryCount = 10; break; case 1: readVersion1(in); break; default: /* Assume a newer version that only adds defaultable values. The alternative would be to thrown an exception */ break; } } private void readVersion1(final ObjectInput in) throws IOException, ClassNotFoundException { retryCount = in.readInt(); } }
// A sample test driver
package io.serial; import java.io.FileOutputStream; import java.io.ObjectOutputStream; import java.io.IOException; import java.io.FileInputStream; import java.io.ObjectInputStream; public class TestVersionMigration { static void writeProxy(String file) throws IOException { FileOutputStream fos = new FileOutputStream("/tmp/java5/"+file); ObjectOutputStream oos = new ObjectOutputStream(fos); ServerAddress sa = new ServerAddress("localhost", 4445, false, 60); Proxy proxy = new Proxy(sa, 10); oos.writeObject(proxy); oos.close(); fos.close(); } static void writeProxyWithExtraData(String file) throws IOException { FileOutputStream fos = new FileOutputStream("/tmp/java5/"+file); ObjectOutputStream oos = new ObjectOutputStream(fos); ServerAddress sa = new ServerAddress("localhost", 4445, false, 60); Proxy proxy = new Proxy(sa, 10); oos.writeObject(proxy); oos.writeInt(1); oos.writeUTF("Another string"); oos.close(); fos.close(); } static void readProxy(String file) throws IOException, ClassNotFoundException { FileInputStream fis = new FileInputStream("/tmp/java5/"+file); ObjectInputStream ois = new ObjectInputStream(fis); Proxy proxy = (Proxy) ois.readObject(); assert proxy.maxPoolSize == 10 : "maxPoolSize == 10"; } static void readProxyWithExtraData(String file) throws IOException, ClassNotFoundException { FileInputStream fis = new FileInputStream("/tmp/java5/"+file); ObjectInputStream ois = new ObjectInputStream(fis); Proxy proxy = (Proxy) ois.readObject(); assert proxy.maxPoolSize == 10 : "maxPoolSize == 10"; int i = ois.readInt(); assert i == 1 : "i == 1"; String s = ois.readUTF(); assert s.equals("Another string") : "s == Another string"; ois.close(); } public static void main(String[] args) throws Exception { //writeProxyWithExtraData("proxy2+.ser"); readProxyWithExtraData("proxy1+.ser"); } }
When testing the io.serial.ProxyVx class is copied into io.serial.Proxy so that either V1 or V2 is that available class. When a V1 instance is read by a V2 instance, the attempt to read pas the object boundary will result in either an EOFException if this is the last object in the stream, or an OptionalDataException if other primatives or objects exist. This occurs because the boundaries of objects are marked in the stream such that attempts to read past the object boundary is caught. This also allows a V2 object to be read by a V1 object. The extra version and retryCount in the stream are ignored.
Note that the readProxyWithExtraData validates that the int primative and String written after the Proxy exist with the expected values to validate that the object boundaries remain intact even when the readExternal/writeExternal behavior are not in synch.