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

问题描述 投票:10回答:2

问题:

我有一堆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(...)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));
}

这里的问题是,我没有找到任何方法来获取字段名称。我试图用TypeAdapterFactory得到它,但工厂也不知道字段名称。

JsonSerializer

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

java gson
2个回答
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方法的存在。如果您没有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());
    }
}

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可能将同一个变量设置为不同的值。

© www.soinside.com 2019 - 2024. All rights reserved.