sintaxis - programacion orientada a objetos polimorfismo java
Gson serializa una lista de objetos polimórficos (3)
Estoy tratando de serializar / deserializar un objeto, que involucra polimorfismo, en JSON usando Gson.
Este es mi código para serializar:
ObixBaseObj lobbyObj = new ObixBaseObj();
lobbyObj.setIs("obix:Lobby");
ObixOp batchOp = new ObixOp();
batchOp.setName("batch");
batchOp.setIn("obix:BatchIn");
batchOp.setOut("obix:BatchOut");
lobbyObj.addChild(batchOp);
Gson gson = new Gson();
System.out.println(gson.toJson(lobbyObj));
Este es el resultado:
{"obix":"obj","is":"obix:Lobby","children":[{"obix":"op","name":"batch"}]}
La serialización funciona principalmente, excepto que falta el contenido de los miembros heredados (En particular obix:BatchIn
y obixBatchout
faltan). Aquí está mi clase base:
public class ObixBaseObj {
protected String obix;
private String display;
private String displayName;
private ArrayList<ObixBaseObj> children;
public ObixBaseObj()
{
obix = "obj";
}
public void setName(String name) {
this.name = name;
}
...
}
Así es como se ve mi clase heredada (ObixOp):
public class ObixOp extends ObixBaseObj {
private String in;
private String out;
public ObixOp() {
obix = "op";
}
public ObixOp(String in, String out) {
obix = "op";
this.in = in;
this.out = out;
}
public String getIn() {
return in;
}
public void setIn(String in) {
this.in = in;
}
public String getOut() {
return out;
}
public void setOut(String out) {
this.out = out;
}
}
Me doy cuenta de que podría usar un adaptador para esto, pero el problema es que estoy serializando una colección del tipo de clase base ObixBaseObj
. Hay alrededor de 25 clases que heredan de esto. ¿Cómo puedo hacer que esto funcione elegantemente?
Aprecio las otras respuestas aquí que me llevaron en mi camino para resolver este problema. RuntimeTypeAdapterFactory
una combinación de RuntimeTypeAdapterFactory
con Reflection .
También creé una clase de ayuda para asegurarme de que se usara un Gson configurado correctamente.
Dentro de un bloque estático dentro de la clase GsonHelper, tengo el siguiente código para acceder a mi proyecto y buscar y registrar todos los tipos apropiados. Todos mis objetos que pasarán por JSON-ification son un subtipo de Jsonable. Deseará cambiar lo siguiente:
- my.project en
Reflections
debería ser tu nombre de paquete. -
Jsonable.class
es mi clase base. Sustituye el tuyo Me gusta que el campo muestre el nombre canónico completo, pero claramente, si no lo quiere / necesita, puede omitir esa parte de la llamada para registrar el subtipo. Lo mismo ocurre con
className
enRuntimeAdapterFactory
; Tengo elementos de datos que ya usan el campotype
.private static final GsonBuilder gsonBuilder = new GsonBuilder() .setDateFormat("yyyy-MM-dd''T''HH:mm:ssZ") .excludeFieldsWithoutExposeAnnotation() .setPrettyPrinting(); static { Reflections reflections = new Reflections("my.project"); Set<Class<? extends Jsonable>> allTypes = reflections.getSubTypesOf(Jsonable.class); for (Class< ? extends Jsonable> serClass : allTypes){ Set<?> subTypes = reflections.getSubTypesOf(serClass); if (subTypes.size() > 0){ RuntimeTypeAdapterFactory<?> adapterFactory = RuntimeTypeAdapterFactory.of(serClass, "className"); for (Object o : subTypes ){ Class c = (Class)o; adapterFactory.registerSubtype(c, c.getCanonicalName()); } gsonBuilder.registerTypeAdapterFactory(adapterFactory); } } } public static Gson getGson() { return gsonBuilder.create(); }
Creo que un serializador / deserializador personalizado es la única forma de proceder y traté de proponerle la forma más compacta de realizarlo que he encontrado. Me disculpo por no usar sus clases, pero la idea es la misma (solo quería al menos 1 clase base y 2 clases ampliadas).
BaseClass.java
public class BaseClass{
@Override
public String toString() {
return "BaseClass [list=" + list + ", isA=" + isA + ", x=" + x + "]";
}
public ArrayList<BaseClass> list = new ArrayList<BaseClass>();
protected String isA="BaseClass";
public int x;
}
ExtendedClass1.java
public class ExtendedClass1 extends BaseClass{
@Override
public String toString() {
return "ExtendedClass1 [total=" + total + ", number=" + number
+ ", list=" + list + ", isA=" + isA + ", x=" + x + "]";
}
public ExtendedClass1(){
isA = "ExtendedClass1";
}
public Long total;
public Long number;
}
ExtendedClass2.java
public class ExtendedClass2 extends BaseClass{
@Override
public String toString() {
return "ExtendedClass2 [total=" + total + ", list=" + list + ", isA="
+ isA + ", x=" + x + "]";
}
public ExtendedClass2(){
isA = "ExtendedClass2";
}
public Long total;
}
CustomDeserializer.java
public class CustomDeserializer implements JsonDeserializer<List<BaseClass>> {
private static Map<String, Class> map = new TreeMap<String, Class>();
static {
map.put("BaseClass", BaseClass.class);
map.put("ExtendedClass1", ExtendedClass1.class);
map.put("ExtendedClass2", ExtendedClass2.class);
}
public List<BaseClass> deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
List list = new ArrayList<BaseClass>();
JsonArray ja = json.getAsJsonArray();
for (JsonElement je : ja) {
String type = je.getAsJsonObject().get("isA").getAsString();
Class c = map.get(type);
if (c == null)
throw new RuntimeException("Unknow class: " + type);
list.add(context.deserialize(je, c));
}
return list;
}
}
CustomSerializer.java
public class CustomSerializer implements JsonSerializer<ArrayList<BaseClass>> {
private static Map<String, Class> map = new TreeMap<String, Class>();
static {
map.put("BaseClass", BaseClass.class);
map.put("ExtendedClass1", ExtendedClass1.class);
map.put("ExtendedClass2", ExtendedClass2.class);
}
@Override
public JsonElement serialize(ArrayList<BaseClass> src, Type typeOfSrc,
JsonSerializationContext context) {
if (src == null)
return null;
else {
JsonArray ja = new JsonArray();
for (BaseClass bc : src) {
Class c = map.get(bc.isA);
if (c == null)
throw new RuntimeException("Unknow class: " + bc.isA);
ja.add(context.serialize(bc, c));
}
return ja;
}
}
}
y ahora este es el código que ejecuté para probar todo:
public static void main(String[] args) {
BaseClass c1 = new BaseClass();
ExtendedClass1 e1 = new ExtendedClass1();
e1.total = 100L;
e1.number = 5L;
ExtendedClass2 e2 = new ExtendedClass2();
e2.total = 200L;
e2.x = 5;
BaseClass c2 = new BaseClass();
c1.list.add(e1);
c1.list.add(e2);
c1.list.add(c2);
List<BaseClass> al = new ArrayList<BaseClass>();
// this is the instance of BaseClass before serialization
System.out.println(c1);
GsonBuilder gb = new GsonBuilder();
gb.registerTypeAdapter(al.getClass(), new CustomDeserializer());
gb.registerTypeAdapter(al.getClass(), new CustomSerializer());
Gson gson = gb.create();
String json = gson.toJson(c1);
// this is the corresponding json
System.out.println(json);
BaseClass newC1 = gson.fromJson(json, BaseClass.class);
System.out.println(newC1);
}
Esta es mi ejecución:
BaseClass [list=[ExtendedClass1 [total=100, number=5, list=[], isA=ExtendedClass1, x=0], ExtendedClass2 [total=200, list=[], isA=ExtendedClass2, x=5], BaseClass [list=[], isA=BaseClass, x=0]], isA=BaseClass, x=0]
{"list":[{"total":100,"number":5,"list":[],"isA":"ExtendedClass1","x":0},{"total":200,"list":[],"isA":"ExtendedClass2","x":5},{"list":[],"isA":"BaseClass","x":0}],"isA":"BaseClass","x":0}
BaseClass [list=[ExtendedClass1 [total=100, number=5, list=[], isA=ExtendedClass1, x=0], ExtendedClass2 [total=200, list=[], isA=ExtendedClass2, x=5], BaseClass [list=[], isA=BaseClass, x=0]], isA=BaseClass, x=0]
Algunas explicaciones: el truco lo realiza otro Gson dentro del serializador / deserializador. Solo uso isA
field para detectar la clase correcta. Para ir más rápido, uso un mapa para asociar la cadena isA
a la clase correspondiente. Luego, hago la serialización / deserialización adecuada usando el segundo objeto Gson. Lo declare como estático, por lo que no ralentizará la serialización / deserialización con la asignación múltiple de Gson.
Pro. En realidad, no escribes código con más código que este, permites que Gson haga todo el trabajo. Simplemente tiene que recordar poner una nueva subclase en los mapas (la excepción lo recuerda).
Contras usted tiene dos mapas. Creo que mi implementación puede refinarse un poco para evitar duplicaciones de mapas, pero se los dejé a usted (o al futuro editor, si corresponde).
Tal vez desee unificar la serialización y la deserialización en un objeto único, debe comprobar la clase TypeAdapter
o experimentar con un objeto que implemente ambas interfaces.
Hay una solución simple: Gson''s RuntimeTypeAdapterFactory . No tiene que escribir ningún serializador, esta clase funciona para usted. Prueba esto con tu código:
ObixBaseObj lobbyObj = new ObixBaseObj();
lobbyObj.setIs("obix:Lobby");
ObixOp batchOp = new ObixOp();
batchOp.setName("batch");
batchOp.setIn("obix:BatchIn");
batchOp.setOut("obix:BatchOut");
lobbyObj.addChild(batchOp);
RuntimeTypeAdapterFactory<ObixBaseObj> adapter =
RuntimeTypeAdapterFactory
.of(ObixBaseObj.class)
.registerSubtype(ObixBaseObj.class)
.registerSubtype(ObixOp.class);
Gson gson2=new GsonBuilder().setPrettyPrinting().registerTypeAdapterFactory(adapter).create();
Gson gson = new Gson();
System.out.println(gson.toJson(lobbyObj));
System.out.println("---------------------");
System.out.println(gson2.toJson(lobbyObj));
}
Salida:
{"obix":"obj","is":"obix:Lobby","children":[{"obix":"op","name":"batch","children":[]}]}
---------------------
{
"type": "ObixBaseObj",
"obix": "obj",
"is": "obix:Lobby",
"children": [
{
"type": "ObixOp",
"in": "obix:BatchIn",
"out": "obix:BatchOut",
"obix": "op",
"name": "batch",
"children": []
}
]
}
EDITAR: mejor ejemplo de trabajo.
Dijiste que hay alrededor de 25 clases que heredan de ObixBaseObj
.
Comenzamos a escribir una nueva clase, GsonUtils
public class GsonUtils {
private static final GsonBuilder gsonBuilder = new GsonBuilder()
.setPrettyPrinting();
public static void registerType(
RuntimeTypeAdapterFactory<?> adapter) {
gsonBuilder.registerTypeAdapterFactory(adapter);
}
public static Gson getGson() {
return gsonBuilder.create();
}
Cada vez que necesitamos un objeto Gson
, en lugar de llamar al new Gson()
, llamaremos
GsonUtils.getGson()
Agregamos este código a ObixBaseObj:
public class ObixBaseObj {
protected String obix;
private String display;
private String displayName;
private String name;
private String is;
private ArrayList<ObixBaseObj> children = new ArrayList<ObixBaseObj>();
// new code
private static final RuntimeTypeAdapterFactory<ObixBaseObj> adapter =
RuntimeTypeAdapterFactory.of(ObixBaseObj.class);
private static final HashSet<Class<?>> registeredClasses= new HashSet<Class<?>>();
static {
GsonUtils.registerType(adapter);
}
private synchronized void registerClass() {
if (!registeredClasses.contains(this.getClass())) {
registeredClasses.add(this.getClass());
adapter.registerSubtype(this.getClass());
}
}
public ObixBaseObj() {
registerClass();
obix = "obj";
}
¿Por qué? porque cada vez que se ObixBaseObj
una instancia de esta clase o una clase para niños de ObixBaseObj
, la clase se registrará en RuntimeTypeAdapter
En las clases para niños, solo se necesita un cambio mínimo:
public class ObixOp extends ObixBaseObj {
private String in;
private String out;
public ObixOp() {
super();
obix = "op";
}
public ObixOp(String in, String out) {
super();
obix = "op";
this.in = in;
this.out = out;
}
Ejemplo de trabajo:
public static void main(String[] args) {
ObixBaseObj lobbyObj = new ObixBaseObj();
lobbyObj.setIs("obix:Lobby");
ObixOp batchOp = new ObixOp();
batchOp.setName("batch");
batchOp.setIn("obix:BatchIn");
batchOp.setOut("obix:BatchOut");
lobbyObj.addChild(batchOp);
Gson gson = GsonUtils.getGson();
System.out.println(gson.toJson(lobbyObj));
}
Salida:
{
"type": "ObixBaseObj",
"obix": "obj",
"is": "obix:Lobby",
"children": [
{
"type": "ObixOp",
"in": "obix:BatchIn",
"out": "obix:BatchOut",
"obix": "op",
"name": "batch",
"children": []
}
]
}
Espero que ayude.