Friday, November 4, 2011

javax.namespace.QName and serialization

I have already mentioned a particular quirk of javax.namespace.QName under JBoss AS 6 and how to avoid it. Unfortunately it is not the end of the story.

You see long time ago somebody made a mistake. Different JDK versions were shipped with serialization-incompatible versions of javax.namespace.QName. Ooops.

I did not try to find out when it was made and what exactly happened, like just forgotten serialVersionUID or something else. Can happen, was fixed. You can see the fix in the JDK source code. Of course the fix is not really complete if we take into account some versions of stax-api jar floating around.

Anyway, stax-api.jar in JBoss AS6 is taken care of. But our application started to fail after it was deployed in the test environment. The application is just migrated to JBoss and is running against an existing test database. And here is the problem: the application stores some JAXB unmarshalled objects in the database. JAXB really likes to use javax.xml.bind.JAXBElement class which has a field of type QName. And the previous version of the application was running under a different version of JDK. You've got the picture.

The "fix" provided by JDK does not help here. It just allows me to select the current serialVersionUID from a limited set of supported values. Deserializing an instance of javax.namespace.QName serialized with a different serialVersionUID fails.

It is possible to go to the database, identify the records that have a wrong serialVersionUID and try to change them. Hmm, understanding the format of serialized instances of java classes and Oracle BLOB functions is not what I really wanted to do. On top of that there are other situations where one might need to be able to support multiple values of serialVersionUID like for example remote invocations.

Fortunately these other situations were not applicable in my case. And I was also lucky because the framework that the application is using allows configuring the way the data is read from and written into a database. Sort of. Several configuration files and several classes after I actually could concentrate on the solution itself:

public class CustomObjectInputStream extends ObjectInputStream {

...
//
protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
// This is the fix: if the description of QName class is read, check what value of
// serialVersionUID it has. And if it is not the "current good" value, but matches
// one of the "known good" values, just return the "current good" descriptor.
ObjectStreamClass desc = super.readClassDescriptor();

if (desc != null && platformQNameDesc != null && platformQNameDesc.getName().equals(desc.getName())) {
// OK, this is QName descriptor
long readVerUID = desc.getSerialVersionUID();

if (readVerUID != platformQNameDesc.getSerialVersionUID()) {
// SerialVersionUID of the serialised class DOES NOT match
// the value used by this JDK process.
if (readVerUID == JDK_defaultSerialVersionUID ||
readVerUID == JDK_compatibleSerialVersionUID ||
readVerUID == STAXAPI_SerialVersionUID)
{
// OK, one of the "known good values". Pretend we read the correct one.
//
// This is not a very nice way, but the thing is: ObjectStreamClass
// is a class, not interface, and has a lot of (package) private fields
// that are directly read by other JDK classes. So it is either
// reflection and changing the private field or this one:
desc = platformQNameDesc;
}
}
}

return desc;
}

//
private ObjectStreamClass platformQNameDesc = ObjectStreamClass.lookup(QName.class);

//
private static final long JDK_defaultSerialVersionUID = -9120448754896609940L;
private static final long JDK_compatibleSerialVersionUID = 4418622981026545151L;
private static final long STAXAPI_SerialVersionUID = -711357515002332258L;
}

I really think that something like this must be implemented by JDK, and not only for QName. Allowing to override serialVersionUID is not enough. And maybe class java.io.ObjectInputStream should be changed as well to be able to serialize objects with non-default values of serialVersionUID.

No comments:

Post a Comment