Reputation: 100152
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
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