Cinco años más tarde, me encuentro con mi original (pero no nulo!) respuesta insatisfactoria después de tropezar con esta publicación a través de Google. Otra solución sería no usar ningún reflejo, y usar la técnica sugerida por Boann.
También hace uso de la clase GetField devuelta por el método ObjectInputStream#readFields()
, que de acuerdo con la especificación de serialización debe llamarse en el método privado readObject(...)
.
La solución hace que la deserialización de campo sea explícita almacenando los campos recuperados en un campo temporal transitorio (llamado FinalExample#fields
) de una "instancia" temporal creada por el proceso de deserialización. Todos los campos de objeto se deserializan y se llama al readResolve(...)
: se crea una nueva instancia pero esta vez usando un constructor, descartando la instancia temporal con el campo temporal. La instancia restaura explícitamente cada campo con la instancia GetField
; este es el lugar para verificar cualquier parámetro como lo haría cualquier otro constructor. Si el constructor lanza una excepción, se traduce a InvalidObjectException
y falla la deserialización de este objeto.
El micro-índice de referencia incluido garantiza que esta solución no sea más lenta que la serialización/deserialización predeterminada. De hecho, es en mi PC:
Problem: 8.598s Solution: 7.818s
Entonces aquí está el código:
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;
}
}
}
Una nota de precaución: cuando la clase se refiere a otra instancia de objeto, podría ser posible fuga del temporal "instancia" creada por el proceso de serialización: la resolución del objeto ocurre solo después de que se leen todos los sub-objetos, por lo tanto, es posible que los subobjetos mantengan una referencia al objeto temporal. Las clases pueden verificar el uso de dichas instancias construidas ilegalmente comprobando que el campo temporal GetField
es nulo. Solo cuando es nulo, se creó utilizando un constructor regular y no a través del proceso de deserialización.
Nota para mí: Quizás exista una solución mejor en cinco años. ¡Hasta entonces!
Gracias. Sospeché que también era así, pero no estaba seguro de no extrañar algo. – doublep
Su respuesta "los transitorios no pueden ser definitivos" es incorrecta: explique el código fuente de Hibernate con 'transitorio final 'por todas partes: https://github.com/hibernate/hibernate-orm/blob/4.3.7.Final/hibernate- core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java –
En realidad, la respuesta es incorrecta. Los campos 'transitorios' pueden ser' finales'. Pero para lograr que funcione para algo distinto de los valores predeterminados ('false' /' 0'/'0.0' /' null'), no solo debe implementar 'readObject()' sino también 'readResolve()', o use * Reflection *. –