从一个Java字段写入两个JSON条目

时间:2017-12-18 14:51:27

标签: java gson

问题:

我有一堆DTOs,其结构如下:

public class Foobar {
    private String name;
    private Timestamp time1;
    private Timestamp time2;
    private int value;
}

我需要将Timestamps序列化为两个单独的值(一次调用.toString(),并根据ISO标准格式化一次),以便向后兼容旧版本从现在开始,API也支持一种不错的时间格式。

因此,Foobar的JSON输出应如下所示:

{
    "name":"<some name>",
    "time1":"<some non standard time>",
    "iso_time1":"<ISO formatted time>",
    "time2":"<some non standard time>",
    "iso_time2":"<ISO formatted time>",
    "value":<some number>
}

由于现有代码,我被限制在Gson。

问题:

是否可以通用方式执行此操作,这对我的所有DTOs 都有效,而不会更改DTOs

我希望避免为每个现有的DTO写一个TypeAdapter/Serializer/new DTO

我尝试了什么

TypeAdapter

我已经尝试通过TypeAdapterTypeAdapterFactory来完成,但我需要类的字段名称才能区分这两个时间戳。

write(...)的{​​{1}}方法说明了我遇到的问题(TypeAdapter):

T extends Timestamp

这里的问题是,我没有找到任何方法来获取字段名称。我尝试使用@Override public void write(final JsonWriter out, final T value) throws IOException { out.value(value.toString()); out.name(TIMESTAMP_ISO_PREFIX + fieldName).value(toISOFormat(value)); } 来获取它,但工厂也不知道字段名称。

JsonSerializer

我也尝试通过TypeAdapterFactory来做,但是不可能返回两个JSON元素并返回一个JsonObject会破坏现有的API。

2 个答案:

答案 0 :(得分:2)

方法1:使用JsonSerialiser

您可以为对象创建JsonSerialiser(即比Timestamp高一级)并使用它根据需要附加额外字段:

/**
 * Appends extra fields containing ISO formatted times for all Timestamp properties of an Object.
 */
class TimestampSerializer implements JsonSerializer<Object> {
    private Gson gson = new GsonBuilder().create();

    @Override
    public JsonElement serialize(Object src, Type typeOfSrc, JsonSerializationContext context) {
        JsonElement tree = gson.toJsonTree(src);
        if (tree instanceof JsonObject) {
            appendIsoTimestamps(src, (JsonObject) tree);
        }
        return tree;
    }

    private JsonObject appendIsoTimestamps(Object src, JsonObject object) {
        try {
            PropertyDescriptor[] descriptors = Introspector.getBeanInfo(src.getClass()).getPropertyDescriptors();
            for (PropertyDescriptor descriptor : descriptors) {
                if (descriptor.getPropertyType().equals(Timestamp.class)) {
                    Timestamp ts = (Timestamp) descriptor.getReadMethod().invoke(src);
                    object.addProperty("iso_" + descriptor.getName(), ts.toInstant().toString());
                }
            }
            return object;
        } catch (IllegalAccessException | InvocationTargetException | IntrospectionException e) {
            throw new JsonIOException(e);
        }
    }

使用示例:

public class GsonSerialiserTest {
    public static void main(String[] args) {
        GsonBuilder builder = new GsonBuilder();
        builder.registerTypeAdapter(Foobar.class, new TimestampSerializer());
        Gson gson = builder.create();
        Foobar baz = new Foobar("baz", 1, new Timestamp(System.currentTimeMillis()));
        System.out.println(gson.toJson(baz));
    }
}

一些注意事项:

  • 此示例使用java bean introspector查找Timestamp属性。它依赖于getter方法的存在。如果您没有吸气剂,您将不得不使用其他方法来阅读您的时间戳属性。
  • 序列化程序委托给另一个gson构建器(它不能调用JsonSerializationContext中的那个或者最终会递归调用它自己)。如果现有序列化依赖于构建器中的其他工具,则必须连接单独的构建器并将其传递给序列化器。

如果要对所有对象执行此操作,则需要序列化为整个Object层次结构注册适配器:

builder.registerTypeHierarchyAdapter(Object.class, typeAdapter);

如果您只想修改DTO的子集,可以动态注册它们。 Reflections库使这一切变得简单:

TimestampSerializer typeAdapter = new TimestampSerializer();

Reflections reflections = new Reflections(new ConfigurationBuilder()
    .setScanners(new SubTypesScanner(false))
    .setUrls(ClasspathHelper.forClassLoader(ClasspathHelper.contextClassLoader()))
    .filterInputsBy(new FilterBuilder().includePackage("com.package.dto", "com.package.other")));

Set<Class<?>> classes = reflections.getSubTypesOf(Object.class);

for (Class<?> type : classes) {
    builder.registerTypeAdapter(type, typeAdapter);
}

上面的示例在命名包中注册所有内容。如果您的DTO符合命名模式或实现通用接口/具有共同注释,则可以进一步限制注册的内容。

方法2:注册TypeAdapterFactory

TypeAdapters在读者/作者级别工作,需要更多工作才能实现,但它们可以让您获得更多控制权。

向构建器注册TypeAdapterFactory将允许您控制要编辑的类型。此示例将适配器应用于所有类型:

public static void main(String[] args) {
    GsonBuilder builder = new GsonBuilder();

    builder.registerTypeAdapterFactory(new TypeAdapterFactory() {
        public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
            // Return null here if you don't want to handle the type.
            // This example returns an adapter for every type.
            return new TimestampAdapter<>(type);
        }
    });

    Gson gson = builder.create();
    Foobar baz = new Foobar("baz", 1);
    String json = gson.toJson(baz);
    System.out.println(json);
    System.out.println(gson.fromJson(json, Foobar.class));
}

适配器......

class TimestampAdapter<T> extends TypeAdapter<T> {
    private TypeToken<T> type;
    private Gson gson = new GsonBuilder().create();

    public TimestampAdapter(TypeToken<T> type) {
        this.type = type;
    }

    @Override
    public void write(JsonWriter out, T value) throws IOException {
        JsonObject object = appendIsoTimestamps(value, (JsonObject) gson.toJsonTree(value));
        TypeAdapters.JSON_ELEMENT.write(out, object);
    }

    private JsonObject appendIsoTimestamps(T src, JsonObject tree) {
        try {
            PropertyDescriptor[] descriptors = Introspector.getBeanInfo(src.getClass()).getPropertyDescriptors();
            for (PropertyDescriptor descriptor : descriptors) {
                if (descriptor.getPropertyType().equals(Timestamp.class)) {
                    Timestamp ts = (Timestamp) descriptor.getReadMethod().invoke(src);
                    tree.addProperty("iso_" + descriptor.getName(), ts.toInstant().toString());
                }
            }
            return tree;
        } catch (IllegalAccessException | InvocationTargetException | IntrospectionException e) {
            throw new JsonIOException(e);
        }
    }

    @Override
    public T read(JsonReader in) {
        return gson.fromJson(in, type.getType());
    }
}

答案 1 :(得分:0)

一个简单,简短且由DTO驱动的解决方案是为同一个字段创建第二个具有不同名称的getter / setter。

public class SerializationTest {

    private String foo;

    public String getFoo() { return foo; }

    // getter for json serialization
    public String getBar() { return foo; }
}

您可能必须修改序列化设置才能生效,如下所示:

objectMapper.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.PUBLIC_ONLY);
objectMapper.setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.PUBLIC_ONLY);

另请注意,此方法存在潜在的缺陷,例如在反序列化时 - 两个setter可能会将相同的变量设置为不同的值。