我有一堆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
我已经尝试通过TypeAdapter
和TypeAdapterFactory
来做,但我需要类的字段名称,以区分两个时间戳。
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。
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));
}
}
一些说明:
Timestamp
属性。它依赖于getter方法的存在。如果您没有getter,则必须使用其他方法来读取时间戳属性。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符合命名模式或实现通用接口/具有共同注释,则可以进一步限制注册的内容。
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());
}
}
简单,简短且由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可能将同一个变量设置为不同的值。