gson 的多态性

Posted

技术标签:

【中文标题】gson 的多态性【英文标题】:Polymorphism with gson 【发布时间】:2011-08-13 14:43:36 【问题描述】:

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

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

似乎我只得到基本类型,即声明的类型,而不是运行时类型。

【问题讨论】:

***.com/q/19588020/3315914 【参考方案1】:

Gson 目前有一个register a Type Hierarchy Adapter 的机制,据报道可以配置为简单的多态反序列化,但我不明白这是怎么回事,因为类型层次结构适配器似乎只是一个组合的序列化器/反序列化器/实例创建器,将实例创建的细节留给编码人员,而不提供任何实际的多态类型注册。

看起来 Gson 很快就会有 RuntimeTypeAdapter 用于更简单的多态反序列化。请参阅http://code.google.com/p/google-gson/issues/detail?id=231 了解更多信息。

如果无法使用新的RuntimeTypeAdapter,并且您必须使用 Gson,那么我认为您必须推出自己的解决方案,将自定义反序列化器注册为类型层次结构适配器或类型适配器。以下是一个这样的例子。

// 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,那么请注意Jackson目前有一个相对简单的多态反序列化机制。我在programmerbruce.blogspot.com/2011/05/… 发布了一些示例 RuntimeTypeAdapter 现在完成了,不幸的是它看起来还没有在 Gson 核心中。 :-(【参考方案2】:

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;

反序列化有点麻烦。在单元测试示例中,它检查是否存在说明属性以决定要反序列化到哪个类。如果您可以更改您正在序列化的对象的来源,您可以为每个包含实例类名称的 FQN 的实例添加一个“classType”属性。不过,这非常不面向对象。

【讨论】:

【参考方案3】:

这有点晚了,但我今天必须做同样的事情。所以,根据我的研究,在使用 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。

【讨论】:

请注意,在某些情况下,使用 Class.forName 对类名进行序列化和反序列化(来自用户输入)可能会带来安全隐患,因此 Gson 开发团队不鼓励这样做。 code.google.com/p/google-gson/issues/detail?id=340#c2 您是如何设法避免在序列化中出现无限循环的,您正在调用 context.serialize(src);这将再次调用您的适配器。这就是我的类似代码中发生的情况。 错了。此解决方案不起作用。如果你以任何方式调用 context.serialize,你最终会得到无限递归。我想知道为什么人们在没有实际测试代码的情况下发布。我尝试使用 2.2.1。请参阅***.com/questions/13244769/… 中描述的错误 @MarcusJuniusBrutus 我运行了你的代码,它似乎只在这种特殊情况下有效——因为你已经定义了一个超接口 IAnimal,而 IAnimalAdapter 使用它。如果您只有“猫”,那么您将遇到无限递归问题。所以这个解决方案在一般情况下仍然不起作用——只有当你能够定义一个通用接口时。就我而言,没有接口,所以我不得不对 TypeAdapterFactory 使用不同的方法。 用户 src.getClass().getName() 而不是 src.getClass().getCanonicalName()。这意味着代码也适用于内部/嵌套类。【参考方案4】:

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>());
        ....

【讨论】:

实施他的解决方案后,我的下一个想法就是这样做:-)【参考方案5】:

很久过去了,但我在网上找不到一个很好的解决方案.. 这是@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 不是 IAnimal)传递给 context.deserialize。这不会调用我们的类型适配器,只要我们使用registerTypeAdapter 而不是registerTypeHierarchyAdapter

【讨论】:

【参考方案6】:

如果你想为一个类型管理一个 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

【讨论】:

【参考方案7】:

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();

Here我已经发布了一个使用动物、狗和猫模型的完整工作示例。

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

【讨论】:

对于那些想玩它的人-它在org.danilopianini:gson-extras下的maven中。不好的是它不适用于 Kotlin 数据类。【参考方案8】:

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

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

案例 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();

在这里,您需要在Animal 中引入一个名为“type”的字段,并将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

【讨论】:

【参考方案9】:

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

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

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

【讨论】:

以上是关于gson 的多态性的主要内容,如果未能解决你的问题,请参考以下文章

使用 Gson & Retrofit 使用多态 json "data: put_anything_here "

如何在Kotlin中用GSON实现多态列表反序列化?

fastjson(1.2.58)支持多态结构的序列化、反序列化

多态性的基因多态性

什么是基因多态性 关于基因多态性介绍

多态性是啥意思