Cinq ans plus tard, je trouve ma réponse initiale insatisfaisante après être tombé sur cet article via Google. Une autre solution serait de ne pas utiliser de réflexion du tout, et d'utiliser la technique suggérée par Boann.
Il fait également appel au GetField retournée par ObjectInputStream#readFields()
qui, conformément à la spécification de sérialisation, doit être appelée dans la méthode privée readObject(...)
méthode.
La solution rend la désérialisation des champs explicite en stockant les champs récupérés dans un champ temporaire transitoire (appelé FinalExample#fields
) d'une "instance" temporaire créée par le processus de désérialisation. Tous les champs de l'objet sont alors désérialisés et readResolve(...)
est appelé : une nouvelle instance est créée, mais cette fois-ci en utilisant un constructeur, en rejetant l'instance temporaire avec le champ temporaire. L'instance restaure explicitement chaque champ en utilisant la fonction GetField
c'est l'endroit où il faut vérifier les paramètres comme pour tout autre constructeur. Si une exception est levée par le constructeur, elle est traduite en un message de type InvalidObjectException
et la désérialisation de cet objet échoue.
Le micro-benchmark inclus garantit que cette solution n'est pas plus lente que la sérialisation/désérialisation par défaut. En effet, elle l'est sur mon PC :
Problem: 8.598s Solution: 7.818s
Ensuite, voici le code :
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectInputStream.GetField;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;
import org.junit.Test;
import static org.junit.Assert.*;
public class FinalSerialization {
/**
* Using default serialization, there are problems with transient final
* fields. This is because internally, ObjectInputStream uses the Unsafe
* class to create an "instance", without calling a constructor.
*/
@Test
public void problem() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
WrongExample x = new WrongExample(1234);
oos.writeObject(x);
oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
WrongExample y = (WrongExample) ois.readObject();
assertTrue(y.value == 1234);
// Problem:
assertFalse(y.ref != null);
ois.close();
baos.close();
bais.close();
}
/**
* Use the readResolve method to construct a new object with the correct
* finals initialized. Because we now call the constructor explicitly, all
* finals are properly set up.
*/
@Test
public void solution() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
FinalExample x = new FinalExample(1234);
oos.writeObject(x);
oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
FinalExample y = (FinalExample) ois.readObject();
assertTrue(y.ref != null);
assertTrue(y.value == 1234);
ois.close();
baos.close();
bais.close();
}
/**
* The solution <em>should not</em> have worse execution time than built-in
* deserialization.
*/
@Test
public void benchmark() throws Exception {
int TRIALS = 500_000;
long a = System.currentTimeMillis();
for (int i = 0; i < TRIALS; i++) {
problem();
}
a = System.currentTimeMillis() - a;
long b = System.currentTimeMillis();
for (int i = 0; i < TRIALS; i++) {
solution();
}
b = System.currentTimeMillis() - b;
System.out.println("Problem: " + a / 1000f + "s Solution: " + b / 1000f + "s");
assertTrue(b <= a);
}
public static class FinalExample implements Serializable {
private static final long serialVersionUID = 4772085863429354018L;
public final transient Object ref = new Object();
public final int value;
private transient GetField fields;
public FinalExample(int value) {
this.value = value;
}
private FinalExample(GetField fields) throws IOException {
// assign fields
value = fields.get("value", 0);
}
private void readObject(ObjectInputStream stream) throws IOException,
ClassNotFoundException {
fields = stream.readFields();
}
private Object readResolve() throws ObjectStreamException {
try {
return new FinalExample(fields);
} catch (IOException ex) {
throw new InvalidObjectException(ex.getMessage());
}
}
}
public static class WrongExample implements Serializable {
private static final long serialVersionUID = 4772085863429354018L;
public final transient Object ref = new Object();
public final int value;
public WrongExample(int value) {
this.value = value;
}
}
}
Une note d'avertissement : chaque fois que la classe fait référence à une autre instance d'objet, il est possible de faire fuir l'"instance" temporaire créée par le processus de sérialisation : la résolution de l'objet ne se produit qu'après la lecture de tous les sous-objets, il est donc possible que les sous-objets conservent une référence à l'objet temporaire. Les classes peuvent vérifier l'utilisation de telles instances illégalement construites en vérifiant que la classe GetField
le champ temporaire est nul. Ce n'est que lorsqu'il est nul qu'il a été créé à l'aide d'un constructeur normal et non par le processus de désérialisation.
Note à soi-même : Peut-être qu'une meilleure solution existera dans cinq ans. À ce moment-là !