所以,正如我在上面的 cmets 中提到的,Gson 类型适配器无法访问它们序列化或反序列化的对象的完整上下文。例如,单个类型(层次结构)的类型适配器并不真正知道它可能应用于哪个字段(这就是帖子中的问题)。为了对不同的字段应用不同的类型适配器,可以使用JsonSerializer 和JsonDeserializer(因此每个字段都必须手动处理,这是一项繁琐的工作)。另一个不好的地方是,应该处理这样的 DTO 的 ReflectiveTypeAdapterFactory 不能直接扩展,而只能通过同样受限的 GsonBuilder 接口进行扩展。
但是,可以实现使用以下算法的解决方法:
- 创建一个在反序列化时始终跳过特殊字段的排除策略(这仅影响
ReflectiveTypeAdapterFactory);
- 创建一个类型适配器工厂,为此类特殊字段创建类型适配器;
- 一旦 Gson 反序列化包装器对象,包装器对象中的特殊字段应该被跳过但设置为
null(在原语的情况下其他默认值),后反序列化器类型适配器要求注入策略来反序列化每个之前被排除策略跳过的特殊字段,因此是ReflectiveTypeAdapterFactory。
这就是诀窍。
interface IPostPatchFactory {
@Nonnull
TypeAdapterFactory createTypeAdapterFactory();
@Nonnull
ExclusionStrategy createExclusionStrategy();
}
@AllArgsConstructor(access = AccessLevel.PRIVATE)
final class PostPatchFactory
implements IPostPatchFactory {
private final Predicate<? super FieldDatum> isFieldPostPatched;
private final Predicate<? super Class<?>> isClassPostPatched;
private final Iterable<FieldPatch<?>> fieldPatches;
static IPostPatchFactory create(final Collection<FieldPatch<?>> fieldPatches) {
final Collection<FieldPatch<?>> fieldPatchesCopy = new ArrayList<>(fieldPatches);
final Collection<Field> postPatchedFields = fieldPatches.stream()
.map(FieldPatch::getField)
.collect(Collectors.toList());
final Collection<FieldDatum> postPatchedFieldAttributes = postPatchedFields.stream()
.map(FieldDatum::from)
.collect(Collectors.toList());
final Collection<? super Class<?>> isClassPostPatched = postPatchedFieldAttributes.stream()
.map(fieldDatum -> fieldDatum.declaringClass)
.collect(Collectors.toList());
return new PostPatchFactory(postPatchedFieldAttributes::contains, isClassPostPatched::contains, fieldPatchesCopy);
}
@Nonnull
@Override
public TypeAdapterFactory createTypeAdapterFactory() {
return new PostPatchTypeAdapterFactory(isClassPostPatched, fieldPatches);
}
@Nonnull
@Override
public ExclusionStrategy createExclusionStrategy() {
return new PostPatchExclusionStrategy(isFieldPostPatched);
}
@AllArgsConstructor(access = AccessLevel.PRIVATE)
private static final class PostPatchTypeAdapterFactory
implements TypeAdapterFactory {
private final Predicate<? super Class<?>> isClassPostPatched;
private final Iterable<FieldPatch<?>> fieldPatches;
@Override
@Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
final Class<? super T> rawType = typeToken.getRawType();
if ( !isClassPostPatched.test(rawType) ) {
return null;
}
return new PostPatchTypeAdapter<>(gson, gson.getDelegateAdapter(this, typeToken), fieldPatches)
.nullSafe();
}
@AllArgsConstructor(access = AccessLevel.PRIVATE)
private static final class PostPatchTypeAdapter<T>
extends TypeAdapter<T> {
private final Gson gson;
private final TypeAdapter<T> delegateTypeAdapter;
private final Iterable<FieldPatch<?>> fieldPatches;
@Override
public void write(final JsonWriter out, final T value) {
throw new UnsupportedOperationException("TODO");
}
@Override
public T read(final JsonReader in) {
final JsonElement bufferedJsonElement = JsonParser.parseReader(in);
final T value = delegateTypeAdapter.fromJsonTree(bufferedJsonElement);
for ( final FieldPatch<?> fieldPatch : fieldPatches ) {
final Field field = fieldPatch.getField();
final BiFunction<? super Gson, ? super JsonElement, ?> deserialize = fieldPatch.getDeserialize();
final Object fieldValue = deserialize.apply(gson, bufferedJsonElement);
try {
field.set(value, fieldValue);
} catch ( final IllegalAccessException ex ) {
throw new RuntimeException(ex);
}
}
return value;
}
}
}
private static final class PostPatchExclusionStrategy
implements ExclusionStrategy {
private final Predicate<? super FieldDatum> isFieldPostPatched;
private PostPatchExclusionStrategy(final Predicate<? super FieldDatum> isFieldPostPatched) {
this.isFieldPostPatched = isFieldPostPatched;
}
@Override
public boolean shouldSkipField(final FieldAttributes fieldAttributes) {
return isFieldPostPatched.test(FieldDatum.from(fieldAttributes));
}
@Override
public boolean shouldSkipClass(final Class<?> clazz) {
return false;
}
}
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode
private static final class FieldDatum {
private final Class<?> declaringClass;
private final String name;
private static FieldDatum from(final Member member) {
return new FieldDatum(member.getDeclaringClass(), member.getName());
}
private static FieldDatum from(final FieldAttributes fieldAttributes) {
return new FieldDatum(fieldAttributes.getDeclaringClass(), fieldAttributes.getName());
}
}
}
@AllArgsConstructor(staticName = "of")
@Getter
final class FieldPatch<T> {
private final Field field;
private final BiFunction<? super Gson, ? super JsonElement, ? extends T> deserialize;
}
单元测试:
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@EqualsAndHashCode
@ToString
final class ThirdPartyDTO {
private final Instant foo;
private final Instant bar;
}
public final class PostPatchFactoryTest {
private static final Collection<FieldPatch<?>> fieldPatches;
static {
try {
final Field thirdPartyDtoFooField = ThirdPartyDTO.class.getDeclaredField("foo");
thirdPartyDtoFooField.setAccessible(true);
final Field thirdPartyDtoBarField = ThirdPartyDTO.class.getDeclaredField("bar");
thirdPartyDtoBarField.setAccessible(true);
fieldPatches = ImmutableList.<FieldPatch<?>>builder()
.add(FieldPatch.of(thirdPartyDtoFooField, (gson, jsonElement) -> {
final String rawValue = jsonElement.getAsJsonObject()
.get("foo")
.getAsString();
return Instant.parse(rawValue);
}))
.add(FieldPatch.of(thirdPartyDtoBarField, (gson, jsonElement) -> {
final String rawValue = new StringBuilder(jsonElement.getAsJsonObject()
.get("bar")
.getAsString()
)
.reverse()
.toString();
return Instant.parse(rawValue);
}))
.build();
} catch ( final NoSuchFieldException ex ) {
throw new AssertionError(ex);
}
}
private static final IPostPatchFactory unit = PostPatchFactory.create(fieldPatches);
private static final Gson gson = new GsonBuilder()
.disableInnerClassSerialization()
.disableHtmlEscaping()
.addDeserializationExclusionStrategy(unit.createExclusionStrategy())
.registerTypeAdapterFactory(unit.createTypeAdapterFactory())
.create();
@Test
public void test()
throws IOException {
final ThirdPartyDTO expected = new ThirdPartyDTO(Instant.ofEpochSecond(0), Instant.ofEpochSecond(0));
try ( final JsonReader jsonReader = new JsonReader(new InputStreamReader(PostPatchFactoryTest.class.getResourceAsStream("input.json"))) ) {
final ThirdPartyDTO actual = gson.fromJson(jsonReader, ThirdPartyDTO.class);
Assertions.assertEquals(expected, actual);
}
}
}
{
"foo": "1970-01-01T00:00:00Z",
"bar": "Z00:00:00T10-10-0791"
}
(为简单起见,bar 只是一个颠倒的字符串,使其在 Java 格式模式中变得模糊,但使测试更加健壮)
请注意,这种方法是通用(并且可能适合除Instant 之外的任何其他类型),在反序列化包含特殊字段的类(内置@ 987654336@ 和 JsonDeserializer 也一样,谁在乎呢?),并失去对 @SerializedName、@JsonAdapter 等的一些特殊支持。