gson的多态性


103

我在使用Gson反序列化json字符串时遇到问题。我收到一系列命令。该命令可以是start,stop或其他类型的命令。我自然具有多态性,并且start / stop命令从command继承。

如何使用gson将其序列化回正确的命令对象?

似乎我只获得基本类型,即声明的类型,而从未获得运行时类型。


Answers:


120

这有点晚了,但是我今天必须做完全一样的事情。因此,根据我的研究以及使用gson-2.0时,您确实不想使用registerTypeHierarchyAdapter方法,而是更平凡的registerTypeAdapter。而且,您当然不需要为派生类进行instanceofs或编写适配器:只需为基础类或接口提供一个适配器,当然您对派生类的默认序列化感到满意。无论如何,这是代码(删除了打包和导入)(也可以在github中找到):

基类(在我的情况下为接口):

public interface IAnimal { public String sound(); }

两个派生类Cat:

public class Cat implements IAnimal {

    public String name;

    public Cat(String name) {
        super();
        this.name = name;
    }

    @Override
    public String sound() {
        return name + " : \"meaow\"";
    };
}

和狗:

public class Dog implements IAnimal {

    public String name;
    public int ferocity;

    public Dog(String name, int ferocity) {
        super();
        this.name = name;
        this.ferocity = ferocity;
    }

    @Override
    public String sound() {
        return name + " : \"bark\" (ferocity level:" + ferocity + ")";
    }
}

IAnimalAdapter:

public class IAnimalAdapter implements JsonSerializer<IAnimal>, JsonDeserializer<IAnimal>{

    private static final String CLASSNAME = "CLASSNAME";
    private static final String INSTANCE  = "INSTANCE";

    @Override
    public JsonElement serialize(IAnimal 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 IAnimal 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) {
            e.printStackTrace();
            throw new JsonParseException(e.getMessage());
        }
        return context.deserialize(jsonObject.get(INSTANCE), klass);
    }
}

和测试类:

public class Test {

    public static void main(String[] args) {
        IAnimal animals[] = new IAnimal[]{new Cat("Kitty"), new Dog("Brutus", 5)};
        Gson gsonExt = null;
        {
            GsonBuilder builder = new GsonBuilder();
            builder.registerTypeAdapter(IAnimal.class, new IAnimalAdapter());
            gsonExt = builder.create();
        }
        for (IAnimal animal : animals) {
            String animalJson = gsonExt.toJson(animal, IAnimal.class);
            System.out.println("serialized with the custom serializer:" + animalJson);
            IAnimal animal2 = gsonExt.fromJson(animalJson, IAnimal.class);
            System.out.println(animal2.sound());
        }
    }
}

当运行Test :: main时,将得到以下输出:

serialized with the custom serializer:
{"CLASSNAME":"com.synelixis.caches.viz.json.playground.plainAdapter.Cat","INSTANCE":{"name":"Kitty"}}
Kitty : "meaow"
serialized with the custom serializer:
{"CLASSNAME":"com.synelixis.caches.viz.json.playground.plainAdapter.Dog","INSTANCE":{"name":"Brutus","ferocity":5}}
Brutus : "bark" (ferocity level:5)

我实际上也使用registerTypeHierarchyAdapter方法完成了上述操作,但是这似乎需要实现自定义的DogAdapter和CatAdapter序列化器/反序列化器类,这在您每次要向Dog或Cat添加另一个字段时都很难维护。


5
请注意,在某些情况下,使用Class.forName进行类名的序列化和反序列化(来自用户输入)可能会带来安全隐患,因此Gson开发团队不建议这样做。code.google.com/p/google-gson/issues/detail?id=340#c2
程序员布鲁斯

4
您如何设法在序列化中不导致无限循环,您正在调用context.serialize(src);。这将再次调用您的适配器。这就是我的类似代码中发生的事情。
che javara

6
错误。此解决方案不起作用。如果您以任何方式调用context.serialize,您将最终获得无限递归。我不知道为什么人们发布时没有实际测试代码。我尝试使用2.2.1。请参见stackoverflow.com/questions/13244769/…中
che javara

4
@MarcusJuniusBrutus我运行了您的代码,它似乎仅在这种特殊情况下有效-因为您已经定义了一个超接口IAnimal,并且IAnimalAdapter使用了它。如果您只拥有“猫”,则将遇到无限递归问题。因此,此解决方案在一般情况下仍然不起作用-仅当您能够定义公共接口时。在我的情况下,没有接口,因此我不得不对TypeAdapterFactory使用不同的方法。
che javara 2014年

2
用户src.getClass()。getName()代替src.getClass()。getCanonicalName()。这说明代码也将适用于内部/嵌套类。
mR_fr0g 2015年

13

Gson目前拥有一种注册类型层次适配器的机制,据报道该类型的适配器可以配置为简单的多态反序列化,但是我不知道是哪种情况,因为类型层次适配器似乎只是组合的序列化器/反序列化器/实例创建者,将实例创建的细节留给编码器,而无需提供任何实际的多态类型注册。

看起来Gson很快将拥有RuntimeTypeAdapter更简单的多态反序列化功能。有关更多信息,请参见http://code.google.com/p/google-gson/issues/detail?id=231

如果RuntimeTypeAdapter无法使用新版本,而您必须使用Gson,那么我认为您必须推出自己的解决方案,将自定义解串器注册为Type Hierarchy Adapter或Type Adapter。以下是一个这样的示例。

// output:
//     Starting machine1
//     Stopping machine2

import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;

import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;

public class Foo
{
  // [{"machine_name":"machine1","command":"start"},{"machine_name":"machine2","command":"stop"}]
  static String jsonInput = "[{\"machine_name\":\"machine1\",\"command\":\"start\"},{\"machine_name\":\"machine2\",\"command\":\"stop\"}]";

  public static void main(String[] args)
  {
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
    CommandDeserializer deserializer = new CommandDeserializer("command");
    deserializer.registerCommand("start", Start.class);
    deserializer.registerCommand("stop", Stop.class);
    gsonBuilder.registerTypeAdapter(Command.class, deserializer);
    Gson gson = gsonBuilder.create();
    Command[] commands = gson.fromJson(jsonInput, Command[].class);
    for (Command command : commands)
    {
      command.execute();
    }
  }
}

class CommandDeserializer implements JsonDeserializer<Command>
{
  String commandElementName;
  Gson gson;
  Map<String, Class<? extends Command>> commandRegistry;

  CommandDeserializer(String commandElementName)
  {
    this.commandElementName = commandElementName;
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
    gson = gsonBuilder.create();
    commandRegistry = new HashMap<String, Class<? extends Command>>();
  }

  void registerCommand(String command, Class<? extends Command> commandInstanceClass)
  {
    commandRegistry.put(command, commandInstanceClass);
  }

  @Override
  public Command deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
      throws JsonParseException
  {
    try
    {
      JsonObject commandObject = json.getAsJsonObject();
      JsonElement commandTypeElement = commandObject.get(commandElementName);
      Class<? extends Command> commandInstanceClass = commandRegistry.get(commandTypeElement.getAsString());
      Command command = gson.fromJson(json, commandInstanceClass);
      return command;
    }
    catch (Exception e)
    {
      throw new RuntimeException(e);
    }
  }
}

abstract class Command
{
  String machineName;

  Command(String machineName)
  {
    this.machineName = machineName;
  }

  abstract void execute();
}

class Stop extends Command
{
  Stop(String machineName)
  {
    super(machineName);
  }

  void execute()
  {
    System.out.println("Stopping " + machineName);
  }
}

class Start extends Command
{
  Start(String machineName)
  {
    super(machineName);
  }

  void execute()
  {
    System.out.println("Starting " + machineName);
  }
}

如果可以更改API,请注意,杰克逊目前具有一种相对简单的多态反序列化机制。我在programmerbruce.blogspot.com/2011/05/…上
程序员Bruce

RuntimeTypeAdapter现在已经完成了,不幸的是,它看起来还不是Gson核心。:-(
乔纳森

8

Marcus Junius Brutus给出了一个很好的答案(谢谢!)。为了扩展他的示例,您可以通过以下更改使他的适配器类通用以适用于所有类型的对象(不仅仅是IAnimal):

class InheritanceAdapter<T> implements JsonSerializer<T>, JsonDeserializer<T>
{
....
    public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context)
....
    public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
....
}

在测试类中:

public class Test {
    public static void main(String[] args) {
        ....
            builder.registerTypeAdapter(IAnimal.class, new InheritanceAdapter<IAnimal>());
        ....
}

1
实施他的解决方案后,我的下一个想法就是完全做到这一点:-)
David Levy 2014年

7

GSON在这里有一个很好的测试案例,展示了如何定义和注册类型层次结构适配器。

http://code.google.com/p/google-gson/source/browse/trunk/gson/src/test/java/com/google/gson/functional/TypeHierarchyAdapterTest.java?r=739

要使用该功能,请执行以下操作:

    gson = new GsonBuilder()
          .registerTypeAdapter(BaseQuestion.class, new BaseQuestionAdaptor())
          .create();

适配器的序列化方法可以是级联if-else对其序列化类型的检查。

    JsonElement result = new JsonObject();

    if (src instanceof SliderQuestion) {
        result = context.serialize(src, SliderQuestion.class);
    }
    else if (src instanceof TextQuestion) {
        result = context.serialize(src, TextQuestion.class);
    }
    else if (src instanceof ChoiceQuestion) {
        result = context.serialize(src, ChoiceQuestion.class);
    }

    return result;

反序列化有点麻烦。在单元测试示例中,它检查是否存在讲述属性,以决定将其反序列化到哪个类。如果可以更改要序列化的对象的源,则可以向每个实例添加一个“ classType”属性,该属性保存实例类名称的FQN。但是,这是如此非面向对象的。


4

Google已经发布了自己的RuntimeTypeAdapterFactory来处理多态性,但不幸的是,它不是gson核心的一部分(您必须将类复制并粘贴到项目中)。

例:

RuntimeTypeAdapterFactory<Animal> runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory
.of(Animal.class, "type")
.registerSubtype(Dog.class, "dog")
.registerSubtype(Cat.class, "cat");

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory(runtimeTypeAdapterFactory)
    .create();

在这里,我使用动物,狗和猫模型发布了完整的工作示例。

我认为最好依靠此适配器,而不是从头开始重新实现它。


2

很长一段时间过去了,但是我无法在网上找到一个真正好的解决方案。.@MarcusJuniusBrutus的解决方案略有改动,可以避免无限递归。

保留相同的解串器,但删除串行器-

public class IAnimalAdapter implements JsonDeSerializer<IAnimal> {
  private static final String CLASSNAME = "CLASSNAME";
  private static final String INSTANCE  = "INSTANCE";

  @Override
  public IAnimal 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) {
        e.printStackTrace();
        throw new JsonParseException(e.getMessage());
    }
    return context.deserialize(jsonObject.get(INSTANCE), klass);
  }
}

然后,在您的原始课程中,添加一个字段@SerializedName("CLASSNAME")。现在,诀窍是在基类的构造函数中对此进行初始化,以便使您的接口成为抽象类。

public abstract class IAnimal {
  @SerializedName("CLASSNAME")
  public String className;

  public IAnimal(...) {
    ...
    className = this.getClass().getName();
  }
}

这里没有无限递归的原因是我们将实际的运行时类(即Dog not IAnimal)传递给context.deserialize。只要我们registerTypeAdapter不使用,就不会调用我们的类型适配器registerTypeHierarchyAdapter


2

更新的答案-所有其他答案的最佳组成部分

我正在描述各种用例的解决方案,并且还将解决无限递归问题。

  • 案例1:你是在类的控制,即,你写你自己的CatDog类以及该IAnimal接口。您只需遵循@ marcus-junius-brutus提供的解决方案(评分最高的答案)

    如果有一个通用的基本接口,将不会有任何无限递归 IAnimal

    但是,如果我不想实现IAnimal或任何此类接口怎么办?

    然后,@ marcus-junius-brutus(评分最高的答案)将产生无限递归错误。在这种情况下,我们可以执行以下操作。

    我们将必须在基类和包装子类中创建一个复制构造函数,如下所示:

// Base class(modified)
public class Cat implements IAnimal {

    public String name;

    public Cat(String name) {
        super();
        this.name = name;
    }
    // COPY CONSTRUCTOR
    public Cat(Cat cat) {
        this.name = cat.name;
    }

    @Override
    public String sound() {
        return name + " : \"meaow\"";
    };
}



    // The wrapper subclass for serialization
public class CatWrapper extends Cat{


    public CatWrapper(String name) {
        super(name);
    }

    public CatWrapper(Cat cat) {
        super(cat);
    }
}

和序列化器的类型Cat

public class CatSerializer implements JsonSerializer<Cat> {

    @Override
    public JsonElement serialize(Cat src, Type typeOfSrc, JsonSerializationContext context) {

        // Essentially the same as the type Cat
        JsonElement catWrapped = context.serialize(new CatWrapper(src));

        // Here, we can customize the generated JSON from the wrapper as we want.
        // We can add a field, remove a field, etc.


        return modifyJSON(catWrapped);
    }

    private JsonElement modifyJSON(JsonElement base){
        // TODO: Modify something
        return base;
    }
}

那么,为什么要使用复制构造函数?

好了,一旦定义了复制构造函数,无论基类有多大变化,您的包装器都将继续扮演相同的角色。其次,如果我们没有定义复制构造函数,而只是简单地将基类作为子类,那么我们就不得不就扩展类(即)进行“交谈” CatWrapper。您的组件很有可能根据基类而不是包装器类型进行交谈。

有没有简单的选择?

当然,它已经由Google引入-这是RuntimeTypeAdapterFactory实现:

RuntimeTypeAdapterFactory<Animal> runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory
.of(Animal.class, "type")
.registerSubtype(Dog.class, "dog")
.registerSubtype(Cat.class, "cat");

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory(runtimeTypeAdapterFactory)
    .create();

在这里,您需要在其中引入一个名为“ type”的字段,Animal并将其内部的值Dog设置为“ dog”,Cat“ cat”

完整的示例:https : //static.javadoc.io/org.danilopianini/gson-extras/0.2.1/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.html

  • 情况2:您无法控制课程。您加入公司或使用一个已经定义了类并且您的经理不希望您以任何方式更改它们的库-您可以将类子类化,并让它们实现一个通用的标记接口(该接口没有任何方法) )如AnimalInterface

    例如:

// The class we are NOT allowed to modify

public class Dog implements IAnimal {

    public String name;
    public int ferocity;

    public Dog(String name, int ferocity) {
        super();
        this.name = name;
        this.ferocity = ferocity;
    }

    @Override
    public String sound() {
        return name + " : \"bark\" (ferocity level:" + ferocity + ")";
    }
}


// The marker interface

public interface AnimalInterface {
}

// The subclass for serialization

public class DogWrapper  extends Dog implements AnimalInterface{

    public DogWrapper(String name, int ferocity) {
        super(name, ferocity);
    }

}

// The subclass for serialization

public class CatWrapper extends Cat implements AnimalInterface{


    public CatWrapper(String name) {
        super(name);
    }
}

因此,我们将使用CatWrapper代替CatDogWrapper代替DogAlternativeAnimalAdapter代替IAnimalAdapter

// The only difference between `IAnimalAdapter` and `AlternativeAnimalAdapter` is that of the interface, i.e, `AnimalInterface` instead of `IAnimal`

public class AlternativeAnimalAdapter implements JsonSerializer<AnimalInterface>, JsonDeserializer<AnimalInterface> {

    private static final String CLASSNAME = "CLASSNAME";
    private static final String INSTANCE  = "INSTANCE";

    @Override
    public JsonElement serialize(AnimalInterface 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 AnimalInterface 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) {
            e.printStackTrace();
            throw new JsonParseException(e.getMessage());
        }
        return context.deserialize(jsonObject.get(INSTANCE), klass);
    }
}

我们执行一个测试:

public class Test {

    public static void main(String[] args) {

        // Note that we are using the extended classes instead of the base ones
        IAnimal animals[] = new IAnimal[]{new CatWrapper("Kitty"), new DogWrapper("Brutus", 5)};
        Gson gsonExt = null;
        {
            GsonBuilder builder = new GsonBuilder();
            builder.registerTypeAdapter(AnimalInterface.class, new AlternativeAnimalAdapter());
            gsonExt = builder.create();
        }
        for (IAnimal animal : animals) {
            String animalJson = gsonExt.toJson(animal, AnimalInterface.class);
            System.out.println("serialized with the custom serializer:" + animalJson);
            AnimalInterface animal2 = gsonExt.fromJson(animalJson, AnimalInterface.class);
        }
    }
}

输出:

serialized with the custom serializer:{"CLASSNAME":"com.examples_so.CatWrapper","INSTANCE":{"name":"Kitty"}}
serialized with the custom serializer:{"CLASSNAME":"com.examples_so.DogWrapper","INSTANCE":{"name":"Brutus","ferocity":5}}

1

如果要管理一个类型的TypeAdapter,另一个用于其子类型,则可以使用这样的TypeAdapterFactory:

public class InheritanceTypeAdapterFactory implements TypeAdapterFactory {

    private Map<Class<?>, TypeAdapter<?>> adapters = new LinkedHashMap<>();

    {
        adapters.put(Animal.class, new AnimalTypeAdapter());
        adapters.put(Dog.class, new DogTypeAdapter());
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
        TypeAdapter<T> typeAdapter = null;
        Class<?> currentType = Object.class;
        for (Class<?> type : adapters.keySet()) {
            if (type.isAssignableFrom(typeToken.getRawType())) {
                if (currentType.isAssignableFrom(type)) {
                    currentType = type;
                    typeAdapter = (TypeAdapter<T>)adapters.get(type);
                }
            }
        }
        return typeAdapter;
    }
}

本厂将发送最准确的TypeAdapter


0

如果将Marcus Junius Brutus的答案与user2242263的编辑结合在一起,则可以通过将适配器定义为使用接口类型来避免在适配器中指定大型类层次结构。然后,您可以在接口中提供toJSON()和fromJSON()的默认实现(仅包括这两个方法),并使序列化所需的每个类都实现您的接口。为了处理转换,可以在子类中提供静态的fromJSON()方法,该方法从您的接口类型反序列化并执行适当的转换。这对我来说效果非常好(只需要小心地对包含哈希表的类进行序列化/反序列化-在实例化gson构建器时添加以下内容即可:

GsonBuilder builder = new GsonBuilder().enableComplexMapKeySerialization();

希望这可以帮助某人节省一些时间和精力!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.