[java] gson을 사용한 다형성

Gson으로 json 문자열을 역 직렬화하는 데 문제가 있습니다. 나는 일련의 명령을 받는다. 명령은 start, stop 및 다른 유형의 명령 일 수 있습니다. 당연히 다형성이 있고 시작 / 중지 명령은 명령에서 상속됩니다.

gson을 사용하여 올바른 명령 객체로 다시 직렬화하려면 어떻게해야합니까?

내가 선언 된 유형이고 런타임 유형이 아닌 기본 유형 만 얻는 것 같습니다.



답변

조금 늦었지만 오늘도 똑같은 일을해야했습니다. 따라서 내 연구와 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 메서드를 사용하여 위의 작업을 수행 했지만 Dog 또는 Cat에 다른 필드를 추가하고 싶을 때마다 유지 관리하는 데 어려움이있는 사용자 지정 DogAdapter 및 CatAdapter serializer / deserializer 클래스를 구현해야하는 것 같습니다.


답변

Gson은 현재 단순 다형성 역 직렬화를 위해 구성 할 수 있는 Type Hierarchy Adapter등록 하는 메커니즘을 가지고 있지만 Type Hierarchy Adapter가 결합 된 serializer / deserializer / 인스턴스 생성자 인 것처럼 보이기 때문에 어떻게 그런지 알 수 없습니다. 실제 다형성 유형 등록을 제공하지 않고 인스턴스 생성의 세부 사항을 코더에게 맡기십시오.

곧 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);
  }
}


답변

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


답변

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;

역 직렬화는 약간 해키입니다. 단위 테스트 예제에서는 deserialize 할 클래스를 결정하기 위해 tell-tale 속성이 있는지 확인합니다. 직렬화중인 객체의 소스를 변경할 수있는 경우 인스턴스 클래스 이름의 FQN을 보유하는 각 인스턴스에 ‘classType’속성을 추가 할 수 있습니다. 하지만 이것은 매우 객체 지향적이지 않습니다.


답변

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

여기에 Animal, Dog 및 Cat 모델을 사용하여 전체 작업 예제를 게시했습니다.

처음부터 다시 구현하는 것보다이 어댑터에 의존하는 것이 더 낫다고 생각합니다.


답변

오랜 시간이 지났지 만 온라인에서 정말 좋은 솔루션을 찾을 수 없었습니다. 여기에 @MarcusJuniusBrutus의 솔루션에 대한 작은 왜곡이 있으며 무한 재귀를 피합니다.

동일한 deserializer를 유지하지만 serializer를 제거하십시오.

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

여기에 무한 재귀가없는 이유는 실제 런타임 클래스 (예 : IAnimal이 아닌 Dog)를에 전달하기 때문 context.deserialize입니다. 이것은 우리가 사용 registerTypeAdapter하고 사용 하지 않는 한 유형 어댑터를 호출 하지 않습니다.registerTypeHierarchyAdapter


답변

업데이트 된 답변-다른 모든 답변의 가장 좋은 부분

다양한 사용 사례에 대한 솔루션을 설명하고 있으며 무한 재귀 문제도 해결할 것입니다.

  • 사례 1 : 당신은 클래스의 컨트롤에 자신 쓸 수, 즉 Cat, Dog클래스뿐 아니라 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대신 Cat, DogWrapper대신 Dog
AlternativeAnimalAdapter대신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}}