Ever had a java.io.NotSerializableException in your code and found it very hard to debug? The JVM stack trace is nearly useless, telling you which code triggered the serialization, but not what it is trying to serialize.
Particularly tedious are HTTP sessions that refuse to serialize. In Wicket we store detached components in the session between requests. If you run a clustered app-server with session replication and add objects to your components that don’t implement Serializable, you’re toast. To avoid you deploying something with this issue and getting fired, we have a setting in development mode that tries to serialize the session on every page request. Unfortunately, although it helps you spot this problem early, it’s lumbered with the same unhelpful java.io.NotSerializableException as everything else in the world.
How do we make these issues easier to debug? Well, I’ve just written an ObjectOutputStream subclass which produces output like this:
java.lang.RuntimeException: Unable to serialize class: com.foo.C
Field hierarchy is:
Class com.foo.A
public com.foo.B myFieldForB
public com.foo.C myFieldForC <----- field that is not serializable
at DebuggingObjectOutputStream.writeObjectOverride(DebuggingObjectOutputStream.java:105)
at DebuggingObjectOutputStream.writeObjectOverride(DebuggingObjectOutputStream.java:96)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:298)
at DebuggingObjectOutputStream.main(DebuggingObjectOutputStream.java:28)
Caused by: java.io.NotSerializableException: com.foo.C
... 4 more
Eelco is going to plumb this into our Wicket session serialization debug check so we have much sweeter output for these problems in future. Nice! :-)
Here's the code:
import java.io.IOException; import java.io.NotSerializableException; import java.io.ObjectOutputStream; import java.io.Serializable; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.HashSet; import java.util.LinkedList; public class DebuggingObjectOutputStream extends ObjectOutputStream { private static final long serialVersionUID = 1L; private LinkedList stack = new LinkedList(); private HashSet set = new HashSet(); public DebuggingObjectOutputStream() throws IOException {} @Override protected final void writeObjectOverride(Object obj) throws IOException { // Check for circular reference. if (set.contains(obj)) { return; } if (stack.isEmpty()) { stack.add("Class " + obj.getClass().getName()); } set.add(obj); Field[] fields = obj.getClass().getFields(); for (int i = 0; i < fields.length; i++) { StringBuffer buffer = new StringBuffer(); Field f = fields[i]; int m = f.getModifiers(); if (fields[i].getType().isPrimitive() || Modifier.isTransient(m) || Modifier.isStatic(m)) { continue; } if (Modifier.isPrivate(m)) { buffer.append("private "); } if (Modifier.isProtected(m)) { buffer.append("protected "); } if (Modifier.isPublic(m)) { buffer.append("public "); } if (Modifier.isFinal(m)) { buffer.append("final "); } if (Modifier.isVolatile(m)) { buffer.append("volatile "); } buffer.append(f.getType().getName()).append(""); buffer.append(" ").append(f.getName()); stack.add(buffer.toString()); if (Serializable.class.isAssignableFrom(fields[i].getType())) { try { writeObjectOverride(fields[i].get(obj)); } catch (IllegalAccessException e) { throw new RuntimeException( getPrettyPrintedStack(fields[i].getType().getName()), e); } } else { throw new RuntimeException( getPrettyPrintedStack(fields[i].getType().getName()).toString(), new NotSerializableException(fields[i].getType().getName()) ); } stack.removeLast(); } if (stack.size() == 1) { set.clear(); stack.removeLast(); } } private String getPrettyPrintedStack(String type) { set.clear(); StringBuffer result = new StringBuffer(); StringBuffer spaces = new StringBuffer(); result.append("Unable to serialize class: "); result.append(type); result.append("\nField hierarchy is:"); while (!stack.isEmpty()) { spaces.append(" "); result.append("\n").append(spaces).append(stack.removeFirst()); } result.append(" <----- field that is not serializable"); return result.toString(); } } |
:-)
Hey. Google alerts (sometimes) works fantastic. Half an hour after you published this, I got the alert on ‘Wicket Java’. No more secrets :)
Is this really needed:
// Check for circular reference.
if (set.contains(obj)) { return; }
does objectoutputstream really calls write object for the same object twice? I thought this was handled internally by just writing a handle.
Also is this reliable:
if (stack.isEmpty()) { stack.add(“Class ” obj.getClass().getName()); }
How do you know it is really stacked liked that? It doesn’t have to have childs but it could be siblings.
The docs claim you’re on your own if you implement this, and if you look at ObjectOutputStream, it’s not clever in this instance – you really are doing everything manually, including being responsible for invoking your function recursively on all the fields. I don’t see anything in the code that would stop circular references from magically going away.
And yes, it is reliable. The initial isEmpty() is because you want to catch the first invocation of the method so you know what the class is of the top-level object you’re serialising. Subsequent calls are all recursive. You’ll note I push and pop exactly once for each field as I iterate over them. Siblings would therefore appear on the same level of the stack. But it’s a stack, not a tree – I’m only recording the bits of the tree that are directly above me on the stack – that’s the whole point.
Unfortunately, our first try was hopelessly naive. Anonymous classes and method level classes weren’t handled, it didn’t recurse parents, didn’t take writeObject and Externalizable into account. And that’s just for starters :/.
It took me the whole friggin’ weekend, but I finally have something that works pretty well. I had to hack quite a bit, as the whole serialization department of the JDK is not very accessible, but it should work for most cases, and when it doesn’t I fallback on just printing the original exception.
I’m afraid it’s a bit too large too paste, as the main class, SerializableChecker is almost 700 lines. :)
Yeah, this turned out to be more complex than we both thought it would be. ;-)
Good work cleaning it up and making it all usable – it’s now looking really nice.
Crazy Bob had the same idea: http://crazybob.org/2007/02/debugging-serialization.html