bmargulies
bmargulies

Reputation: 100152

Reading a Serialized lambda with an obsolete interface

Once upon a time, someone noticed that a third party library had the following :

public interface SerializableFunction<I, O> implements Function<I, O>, Serializable {
}

And they wrote some code with a Serializable class (call it Q) with a field of type SerializableFunction<X,Y> for some X and Y. The field is always assigned to a lambda.

class Q implements Serializable {
    SerializableFunction<X,Y> lfield;
}

Unfortunately, that library has an unfriendly license, and we need to stop using it. Is there any way to create a readObject method for Q that can read in the old data? Or is there no choice but to create new data?

The class Q is available when reading. However, it uses a different SerializableFunction interface — different only in that it's in a different package.

Early indications are that there is no way around this: the fundamental exception is an IllegalArgumentException reading the lambda. I don't see a way to get into the process and avoid it, but perhaps I'm missing something.

Upvotes: 1

Views: 313

Answers (1)

Holger
Holger

Reputation: 298479

Normally, the serialization mechanism is quite robust. If you declare the same serialVersionUID in your class as expected by the stream class descriptor, the Serialization implementation will ignore absent fields and leave new fields, not present in the stream, at their default values.

You can even implement a readObject method for initializing the new fields or provide a translation of refactored fields, e.g.

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    ObjectInputStream.GetField fields = ois.readFields();
    this.foo = (String)fields.get("oldNameOfFoo", null);
}

However, there is one catch: the objects referred by now-absent fields are still deserialized before dropped, propagating any connected problem to the caller. Since we can’t inject translation code like a readObject method into the JRE provided SerializedLambda representation, there are not much options to catch the absence of the defining class of a serialized lambda expression.

In either case, we need control over the ObjectInputStream creation. If we have hands on it, first, we create a replacement SerializedLambda class which allows us to customize the process, i.e. redirect the old functional interface to the new one:

public final class MySerializedLambda implements Serializable {
    private final Class<?> capturingClass;
    private final String functionalInterfaceClass;
    private final String functionalInterfaceMethodName;
    private final String functionalInterfaceMethodSignature;
    private final String implClass;
    private final String implMethodName;
    private final String implMethodSignature;
    private final int implMethodKind;
    private final String instantiatedMethodType;
    private final Object[] capturedArgs;

    private MySerializedLambda() {
        throw new UnsupportedOperationException();
    }

    private Object readResolve() throws ReflectiveOperationException {
        String funcInterfaceClass = this.functionalInterfaceClass;
        if(funcInterfaceClass.equals("package/to/old/SerializableFunction")) {
            funcInterfaceClass="package/to/new/SerializableFunction";
        }
        SerializedLambda serializedLambda = new SerializedLambda(capturingClass,
            funcInterfaceClass, functionalInterfaceMethodName,
            functionalInterfaceMethodSignature, implMethodKind, implClass, implMethodName,
            implMethodSignature, instantiatedMethodType, capturedArgs);
        Method m = capturingClass
                  .getDeclaredMethod("$deserializeLambda$", SerializedLambda.class);
        m.setAccessible(true);
        return m.invoke(null, serializedLambda);
    }
}

It has exactly the same fields as the original SerializedLambda class so we can read them, then retrace what SerializedLambda will do in the readResolve() step, but replace the functional interface.

To use this class, we need a subclass of ObjectInputStream:

try(FileInputStream os=new FileInputStream(serialized);
    ObjectInputStream oos=new ObjectInputStream(os) {
        @Override
        protected ObjectStreamClass readClassDescriptor()
                                    throws IOException, ClassNotFoundException {
            final ObjectStreamClass d = super.readClassDescriptor();
            if(d.getName().equals("java.lang.invoke.SerializedLambda")) {
                return ObjectStreamClass.lookup(MySerializedLambda.class);
            }
            return d;
        }
    }) {
    Q q = (Q)oos.readObject();
}

The readClassDescriptor() is responsible for verifying the compatibility of the stream class with the runtime class, so if we redirect the result afterwards, the different name/package/serialVersionUID have no impact.
Unfortunately, the functional interface within the SerializedLambda instance is represented as a String, so we can’t redirect it the same way…
Note that the string representation of the interface uses the JVM internal syntax, i.e. / instead of . for package and class separation and $ for inner classes.

Quite hacky, but I’m afraid, there is no better solution.


Note that serializing lambda expression always is fragile due to the dependency to the compiler-generated method holding the lambda’s body. The solution above works if you use an otherwise unchanged class being compiled with the same compiler, so that the functional signature matches and the synthetic method happens to be present in exactly the same form as before.

Upvotes: 2

Related Questions