问题
using Gson 2.2.2 I'm trying to serialize an array list of POJOs (Behaviors).
i have an adapter that's pretty much a copy of what i've seen online:
public class BehaviorAdapter implements JsonSerializer<Behavior> {
private static final String CLASSNAME = "CLASSNAME";
private static final String INSTANCE = "INSTANCE";
@Override
public JsonElement serialize(Behavior src, Type typeOfSrc,
JsonSerializationContext context) {
JsonObject retValue = new JsonObject();
String className = src.getClass().getCanonicalName();
retValue.addProperty(CLASSNAME, className);
JsonElement elem = context.serialize(src);
retValue.add(INSTANCE, elem);
return retValue;
}
}
The i register it like this:
GsonBuilder builder = new GsonBuilder();
builder.registerTypeHierarchyAdapter(Behavior.class, new BehaviorAdapter());
gson = builder.create();
Then when i try to serialize my ArrayList:
String json2 = gson.toJson(behaviors);
I get a stack overflow.
It appears that on line:
JsonElement elem = context.serialize(src);
It starts a recursive loop, going again and again through my serializer. So How do i register it so that this won't happen? I need to serialize the list and maintain polymorphism.
回答1:
Looks like you found the infinite loop the JsonSerializer docs warn about:
However, you should never invoke it on the src object itself since that will cause an infinite loop (Gson will call your call-back method again).
The easiest way I can think of is to create a new Gson instance that does not have the handler installed, and run your instances through that.
As an end run, you could just serialize the List<Behavior> instead:
public class BehaviorListAdapter implements JsonSerializer<List<Behavior>> {
private static final String CLASSNAME = "CLASSNAME";
private static final String INSTANCE = "INSTANCE";
@Override
public JsonElement serialize(List<Behavior> src, Type typeOfSrc,
JsonSerializationContext context) {
JsonArray array = new JsonArray();
for (Behavior behavior : src) {
JsonObject behaviorJson = new JsonObject();
String className = behavior.getClass().getCanonicalName();
behaviorJson.addProperty(CLASSNAME, className);
JsonElement elem = context.serialize(behavior);
behaviorJson.add(INSTANCE, elem);
array.add(behaviorJson);
}
return array;
}
}
GsonBuilder builder = new GsonBuilder();
// use a TypeToken to make a Type instance for a parameterized type
builder.registerTypeAdapter(
(new TypeToken<List<Behavior>>() {}).getType(),
new BehaviorListAdapter());
gson = builder.create();
回答2:
Take a look at RuntimeTypeAdapterFactory. The test for that class has an example:
RuntimeTypeAdapterFactory<BillingInstrument> rta = RuntimeTypeAdapterFactory.of(
BillingInstrument.class)
.registerSubtype(CreditCard.class);
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(rta)
.create();
CreditCard original = new CreditCard("Jesse", 234);
assertEquals("{\"type\":\"CreditCard\",\"cvv\":234,\"ownerName\":\"Jesse\"}",
gson.toJson(original, BillingInstrument.class));
BillingInstrument deserialized = gson.fromJson(
"{type:'CreditCard',cvv:234,ownerName:'Jesse'}", BillingInstrument.class);
assertEquals("Jesse", deserialized.ownerName);
assertTrue(deserialized instanceof CreditCard);
This class isn't in core Gson; you'll need to copy it into your project to use it.
回答3:
I get what you are trying to do here, and i had the same issue.
I ended writing a simple abstract class
public abstract class TypedJsonizable extends Jsonizable {}
and registering a TypeHierarchyAdapter to my Gson instance
protected static Gson gson = new GsonBuilder()
.registerTypeHierarchyAdapter
(TypedJsonizable.class,new TypedJsonizableSerializer());
The key for this TypeAdapter is not to invoke context.serialize and context.deserialize cause this would cause an infinite loop as stated from Jeff Bowman in his answer, this TypeAdapter use reflection to avoid that.
import com.google.gson.*;
import org.apache.log4j.Logger;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
public class TypedJsonizableSerializer implements JsonSerializer<TypedJsonizable>, JsonDeserializer<TypedJsonizable> {
static final String CLASSNAME_FIELD = "_className";
Logger logger = Logger.getLogger(TypedJsonizable.class);
@Override
public JsonElement serialize(TypedJsonizable src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject contentObj = new JsonObject();
contentObj.addProperty(CLASSNAME_FIELD,src.getClass().getCanonicalName());
for (Field field : src.getClass().getDeclaredFields()) {
field.setAccessible(true);
try {
if (field.get(src)!=null)
contentObj.add(field.getName(),context.serialize(field.get(src)));
} catch (IllegalAccessException e) {
logger.error(e.getMessage(),e);
}
}
return contentObj;
}
@Override
public TypedJsonizable deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String className = jsonObject.get(CLASSNAME_FIELD).getAsString();
if (className == null || className.isEmpty())
throw new JsonParseException("Cannot find _className field. Probably this instance has not been serialized using Jsonizable jsonizer");
try {
Class<?> clazz = Class.forName(className);
Class<?> realClazz = (Class<?>) typeOfT;
if (!realClazz.equals(clazz))
throw new JsonParseException(String.format("Cannot serialize object of class %s to %s", clazz.getCanonicalName(),realClazz.getCanonicalName()));
Object o = clazz.getConstructor().newInstance();
for (Field field : o.getClass().getDeclaredFields()) {
field.setAccessible(true);
if (jsonObject.has(field.getName())) {
field.set(o,context.deserialize(jsonObject.get(field.getName()) , field.getGenericType()));
}
}
return (TypedJsonizable) o;
} catch (ClassNotFoundException e) {
throw new JsonParseException(String.format("Cannot find class with name %s . Maybe the class has been refactored or sender and receiver are not using the same jars",className));
} catch (IllegalAccessException e){
throw new JsonParseException(String.format("Cannot deserialize, got illegalAccessException %s ",e.getMessage()));
} catch (NoSuchMethodException | InstantiationException | InvocationTargetException e) {
throw new JsonParseException(String.format("Cannot deserialize object of class %s, unable to create a new instance invoking empty constructor",className));
}
}
}
回答4:
I've found another solution (workaround) for this issue: don't serialize base class in your class hierarchy but use descendants. For example:
...
protected static Gson gson;
...
GsonBuilder gsb = new GsonBuilder();
gsb.registerTypeAdapter(SomeBase.class, new MQPolymorphicSerializer<SomeBase>());
gson = gsb.create();
SomeBase:
public class SomeBase{
... }
SomeDescendant:
public class SomeDescendant extends SomeBase {
...
}
Stack overflow exception case:
gson.toJson(new SomeBase());
Workaround case:
gson.toJson(new SomeDescendant());
...and finally - serializer example:
public class MQPolymorphicSerializer<T> implements JsonSerializer<T>, JsonDeserializer<T> {
private static final String CLASSNAME = "CLASSNAME";
private static final String INSTANCE = "INSTANCE";
@Override
public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject retValue = new JsonObject();
String className = src.getClass().getName();
retValue.addProperty(CLASSNAME, className);
JsonElement elem = context.serialize(src);
retValue.add(INSTANCE, elem);
return retValue;
}
@Override
public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
JsonPrimitive prim = (JsonPrimitive) jsonObject.get(CLASSNAME);
String className = prim.getAsString();
Class<?> klass = null;
try {
klass = Class.forName(className);
} catch (ClassNotFoundException e) {
throw new JsonParseException(e.getMessage());
}
return context.deserialize(jsonObject.get(INSTANCE), klass);
}
}
来源:https://stackoverflow.com/questions/13244769/gson-polymorphic-serialization