Map 子类的 Gson 序列化也具有显式定义的属性

Posted

技术标签:

【中文标题】Map 子类的 Gson 序列化也具有显式定义的属性【英文标题】:Gson Serialization of Map subclass that also has explicitly-defined properties 【发布时间】:2019-09-02 07:26:00 【问题描述】:

我有一组生成的 java bean,其中每个 bean 定义一个或多个字段,加上它的子类 HashMap<String, T> 其中T 是参数化类型。生成的字段对JSON schema 中明确定义的架构属性进行建模,HashMap 的子类化是为了支持特定类型的附加“任意”属性(由 JSON 架构的“additionalProperties”字段,适用于那些熟悉 JSON Schema 的人)。 下面是一个生成的 bean 的例子:

public class MyModel extends HashMap<String, Foo> 
    private String prop1;
    private Long prop2;

    public String getProp1() 
        return prop1;
    

    public void setProp1(String value) 
        this.prop1 = value;
    

    public Long getProp2() 
        return prop2;
    

    public void setProp2(Long prop2) 
        this.prop2 = prop2;
    

在本例中,用户可以将prop1prop2 设置为普通bean 属性,也可以通过Mapput() 方法设置Foo 类型的任意属性,其中Foo 是只是其他一些用户定义的类型。

问题在于,默认情况下,Gson 会序列化这些生成的 bean 的实例,因此在生成的 JSON 字符串中仅包含 Map 条目,而明确定义的字段将被忽略。

这里是一段代码 sn-p,展示了我如何使用 Gson 序列化一个对象:

    private String serialize(Object obj) 
        return new Gson().toJson(obj);
    

通过调试序列化路径,我可以看到 Gson 正在选择其内部的 MapTypeAdapterFactory 来执行序列化,这是有道理的,因为只有 Map 条目以 JSON 字符串结尾。 另外,如果我序列化一个不是 HashMap 子类的 bean,那么 Gson 会选择其内部的 ReflectiveTypeAdapterFactory

我认为我需要实现我自己的自定义类型适配器,它基本上结合了ReflectiveMap 类型适配器工厂的功能。 这听起来像是一个好计划吗?有没有其他人做过类似的事情,也许可以提供一个让我开始的例子?这将是我第一次涉足Gson 自定义类型适配器。

【问题讨论】:

【参考方案1】:

我们知道默认情况下Gson 将这些对象视为Map,因此我们可以使用它来序列化所有key-value 对并使用reflection 手动序列化其余对象。

简单的序列化器实现如下所示:

class MixedJsonSerializer implements JsonSerializer<Object> 

    @Override
    public JsonElement serialize(Object src, Type typeOfSrc, JsonSerializationContext context) 
        JsonObject json = serialiseAsMap(src, context);
        serialiseAsPojo(src, context, json);

        return json;
    

    private JsonObject serialiseAsMap(Object src, JsonSerializationContext context) 
        return (JsonObject) context.serialize(src, Map.class);
    

    private void serialiseAsPojo(Object src, JsonSerializationContext context, JsonObject mapElement) 
        Method[] methods = ReflectionUtils.getAllDeclaredMethods(src.getClass());
        for (Method method : methods) 
            if (shouldSerialise(method)) 
                final Object result = ReflectionUtils.invokeMethod(method, src);
                final String fieldName = getFieldName(method);
                mapElement.add(fieldName, context.serialize(result));
            
        
    

    private boolean shouldSerialise(Method method) 
        final String name = method.getName();

        return method.getParameterCount() == 0 &&
                ReflectionUtils.USER_DECLARED_METHODS.matches(method) &&
                !IGNORED_METHODS.contains(name) &&
                (name.startsWith("is") || name.startsWith("get"));
    

    private static final List<String> IGNORED_METHODS = Arrays.asList("isEmpty", "length"); //etc

    private String getFieldName(Method method) 
        final String field = method.getName().replaceAll("^(is|get)", "");

        return StringUtils.uncapitalize(field);
    

最复杂的部分是找到所有POJO getter 并在给定对象上调用它们。例如,我使用了来自Spring 库的reflection API。您可以在下面找到如何使用它的示例(我假设所有 POJO 类都扩展了 HashMap):

import com.model.Foo;
import com.model.Pojo;
import com.model.Pojo1;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class GsonApp 

    public static void main(String[] args) 
        System.out.println("Pojo + Map: ");
        Pojo pojo = new Pojo();
        pojo.put("character1", new Foo("Morty", 15));
        pojo.put("character2", new Foo("Rick", 60));

        System.out.println(serialize(pojo));
        System.out.println();

        System.out.println("Map only: ");
        Pojo1 pojo1 = new Pojo1();
        pojo1.put("int1", 1);
        pojo1.put("int2", 22);
        System.out.println(serialize(pojo1));
        System.out.println();

        System.out.println("Pojo only:");
        System.out.println(serialize(new Pojo()));
        System.out.println();
    

    private static final Gson gson = createGson();

    private static Gson createGson() 
        MixedJsonSerializer adapter = new MixedJsonSerializer();
        return new GsonBuilder()
                .setPrettyPrinting()
                // in case you have many classes you need to use reflection
                // to register adapter for each needed class.
                .registerTypeAdapter(Pojo.class, adapter)
                .registerTypeAdapter(Pojo1.class, adapter)
                .create();
    

    private static String serialize(Object obj) 
        return gson.toJson(obj);
    

上面的代码打印:

Pojo + Map: 

  "character2": 
    "name": "Rick",
    "age": 60
  ,
  "character1": 
    "name": "Morty",
    "age": 15
  ,
  "prop1": "Value1",
  "ten": 10,
  "foo": 
    "name": "Test",
    "age": 123
  


Map only: 

  "int2": 22,
  "int1": 1


Pojo only:

  "prop1": "Value1",
  "ten": 10,
  "foo": 
    "name": "Test",
    "age": 123
  

如果您必须手动注册许多类,您可以使用反射扫描给定的包并为它们注册序列化程序。见:Can you find all classes in a package using reflection?

【讨论】:

@ZioberMichal,感谢您的示例。我会试一试。我在试图简化一点时遗漏的一个细节是,所有这些 bean 实际上都将继承我们的公共类之一,该类是 HashMap 的子类。所以,为了避免注册每个类,我想我会尝试 TypeAdapterFactory 方法,因为我可以确定一个特定的类是否是我们公共超类的实例(例如“DynamicModel”)。我还控制这些 bean 的生成,因此我可以生成表示 Map 值类型 T 的元数据(即 TypeToken)。 @PhilAdams,您的评论在问题上方有所改变。如果您控制生成过程,也许您可​​以更改此根类扩展Map。见Prefer composition over inheritance?。如果您在实施自定义序列化程序时发现一些问题,只需使用新的所有信息更新一个问题,也许我们将能够为您提供帮助。 @ZioberMichal,最后,我使用了您的建议,使用组合而不是继承。所有生成的动态模型都继承自我的 DynamicModel 超类,该超类包含一个 Map 类型的字段来存储任意属性,而且每个子类都定义了 0 个或多个自己的字段。我最终实现了一个 TypeAdapterFactory,它或多或少结合了 ReflectiveTypeAdapterFactory 和 MapTypeAdapterFactory 的功能。再次感谢您的帮助。 @PhilAdams,我很高兴能帮上一点忙。我也在考虑合并 ReflectiveTypeAdapterFactoryMapTypeAdapterFactory 但这个解决方案在我看来过于复杂。

以上是关于Map 子类的 Gson 序列化也具有显式定义的属性的主要内容,如果未能解决你的问题,请参考以下文章

原始类型和数组的 GSON 自定义反序列化器

当 Json 遇到 Map

使用 Gson 序列化具有瞬态字段的对象

有没有办法使用 TypeAdapter 来序列化父类和子类实例

HashMap(key,Object)中的Java Gson序列化和反序列化对象[重复]

Gson:有没有更简单的方法来序列化地图