Gson адаптер для разнородной мультикарты

У меня есть Multimap<Class<?>, Object> заселены как

multimap.put(o.getClass, o)

каждый объект помещается в надлежащее ведро в соответствии с его классом. Мне нужно сериализовать и десериализовать мультикарту с помощью Gson. Все объекты принадлежат простым классам без параметров типа. Я имею в виду, что каждый из них может быть десериализован с помощью gson.fromJson(json, someClass); нет TypeToken нужен здесь.

Если это поможет, я мог бы использовать TypeToken или что-то в качестве ключа; Мне все равно Все используемые классы являются подклассами моего класса, если это помогает. Я не хочу разбивать мультикарту на несколько однородных списков, так как их будет десятки. Как это на самом деле ImmutableMultimapэто означало бы еще много строк, которых я хочу избежать.

Что я пробовал: не стоит упоминать. Ни один из адаптеров, которые я написал или увидел, не делает ничего подобного.

1 ответ

Решение

Если я вас правильно понимаю, вы можете сделать такой тип адаптера относительно легко.

Во-первых, давайте создадим Multimap Тип адаптера. Следующие Multimap Адаптер типа может работать с любой мультикартой, однако Classсвязанные ключи будут специализированы ниже.

final class MultimapTypeAdapter<K, V>
        extends TypeAdapter<Multimap<K, V>> {

    private final Converter<K, String> keyConverter;
    private final Function<? super K, ? extends TypeAdapter<V>> valueTypeAdapterProvider;

    private MultimapTypeAdapter(
            final Converter<K, String> keyConverter,
            final Function<? super K, ? extends TypeAdapter<V>> valueTypeAdapterProvider
    ) {
        this.keyConverter = keyConverter;
        this.valueTypeAdapterProvider = valueTypeAdapterProvider;
    }

    static <K, V> TypeAdapter<Multimap<K, V>> multimapTypeAdapter(
            final Converter<K, String> keyConverter,
            final Function<? super K, ? extends TypeAdapter<V>> valueTypeAdapterProvider
    ) {
        return new MultimapTypeAdapter<>(keyConverter, valueTypeAdapterProvider).nullSafe();
    }

    @Override
    @SuppressWarnings("resource")
    public void write(final JsonWriter jsonWriter, final Multimap<K, V> multimap)
            throws IOException {
        jsonWriter.beginObject();
        for ( final K key : multimap.keySet() ) {
            jsonWriter.name(keyConverter.convert(key));
            final TypeAdapter<? super V> typeAdapter = valueTypeAdapterProvider.apply(key);
            jsonWriter.beginArray();
            for ( final V value : multimap.get(key) ) {
                typeAdapter.write(jsonWriter, value);
            }
            jsonWriter.endArray();
        }
        jsonWriter.endObject();
    }

    @Override
    public Multimap<K, V> read(final JsonReader jsonReader)
            throws IOException {
        final ImmutableMultimap.Builder<K, V> multimapBuilder = new ImmutableMultimap.Builder<>();
        jsonReader.beginObject();
        while ( jsonReader.hasNext() ) {
            final K key = keyConverter.reverse().convert(jsonReader.nextName());
            final TypeAdapter<V> typeAdapter = valueTypeAdapterProvider.apply(key);
            jsonReader.beginArray();
            while ( jsonReader.hasNext() ) {
                final V value = typeAdapter.read(jsonReader);
                multimapBuilder.put(key, value);
            }
            jsonReader.endArray();
        }
        jsonReader.endObject();
        return multimapBuilder.build();
    }

}

Теперь вы можете создать простой Class ключевой конвертер: конвертер довольно прост и информативен. Более сложные стратегии конвертации можно найти, например, здесь и здесь (последняя не поддерживает массивы полностью).

final class ClassKeyConverter
        extends Converter<Class<?>, String> {

    private static final Converter<Class<?>, String> classKeyConverter = new ClassKeyConverter();

    private ClassKeyConverter() {
    }

    static Converter<Class<?>, String> classKeyConverter() {
        return classKeyConverter;
    }

    @Override
    protected String doForward(final Class<?> a) {
        return a.toString();
    }

    @Override
    public Class<?> doBackward(final String b) {
        final Class<?> primitiveType = primitiveTypes.get(b);
        if ( primitiveType != null ) {
            return primitiveType;
        }
        final int prefix = b.startsWith(CLASS) ? CLASS.length()
                : b.startsWith(INTERFACE) ? INTERFACE.length()
                : -1;
        if ( prefix >= 0 ) {
            try {
                return Class.forName(b.substring(prefix));
            } catch ( final ClassNotFoundException ex ) {
                throw new RuntimeException(ex);
            }
        }
        throw new IllegalArgumentException(b);
    }

    private static final Map<String, Class<?>> primitiveTypes = ImmutableMap.<String, Class<?>>builder()
            .put("boolean", boolean.class)
            .put("byte", byte.class)
            .put("short", short.class)
            .put("int", int.class)
            .put("long", long.class)
            .put("float", float.class)
            .put("double", double.class)
            .put("char", char.class)
            .build();

    private static final String CLASS = "class ";
    private static final String INTERFACE = "interface ";

}

И теперь вы можете создать фабрику адаптеров типов, которая может обрабатывать такую ​​мультикарту:

final class ClassKeyMultimapTypeAdapterFactory
        implements TypeAdapterFactory {

    private static final TypeAdapterFactory classKeyMultimapTypeAdapterFactory = new ClassKeyMultimapTypeAdapterFactory();

    static final Type classKeyMultimapType = TypeToken.getParameterized(Multimap.class, Class.class, Object.class).getType();

    private ClassKeyMultimapTypeAdapterFactory() {
    }

    static TypeAdapterFactory classKeyMultimapTypeAdapterFactory() {
        return classKeyMultimapTypeAdapterFactory;
    }

    @Override
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        if ( !isClassKeyMultimap(typeToken) ) {
            return null;
        }
        @SuppressWarnings("unchecked")
        final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) multimapTypeAdapter(classKeyConverter(), type -> gson.getDelegateAdapter(this, TypeToken.get(type)));
        return typeAdapter;
    }

    private static boolean isClassKeyMultimap(final TypeToken<?> typeToken) {
        if ( Multimap.class.isAssignableFrom(typeToken.getRawType()) ) {
            final Type type = typeToken.getType();
            if ( type instanceof ParameterizedType ) {
                final ParameterizedType parameterizedType = (ParameterizedType) type;
                if ( Class.class.equals(parameterizedType.getActualTypeArguments()[0]) ) {
                    // We expect to process `Multimap<Class<?>, ?>` only
                    return true;
                }
            }
        }
        return false;
    }

}

Наконец, вы можете проверить это:

private static final Gson gson = new GsonBuilder()
        .disableHtmlEscaping()
        .registerTypeAdapterFactory(classKeyMultimapTypeAdapterFactory())
        .create();

public static void main(final String... args) {
    final Multimap<Class<?>, Object> multimapBefore = ImmutableMultimap.<Class<?>, Object>builder()
            .put(int.class, 2)
            .put(int.class, 3)
            .put(int.class, 4)
            .put(Integer.class, 2)
            .put(Integer.class, 3)
            .put(Integer.class, 4)
            .put(String.class, "foo")
            .put(String.class, "bar")
            .put(String.class, "baz")
            .build();
    System.out.println(multimapBefore);
    final String json = gson.toJson(multimapBefore, classKeyMultimapType);
    System.out.println(json);
    final Multimap<Class<?>, Object> multimapAfter = gson.fromJson(json, classKeyMultimapType);
    System.out.println(multimapAfter);
    if ( !multimapBefore.equals(multimapAfter) ) {
        throw new AssertionError("multimaps do not equal");
    }
}

Выход:

{int=[2, 3, 4], class java.lang.Integer=[2, 3, 4], class java.lang.String=[foo, bar, baz]}
{"int":[2,3,4],"class java.lang.Integer":[2,3,4],"class java.lang.String":["foo","bar","baz"]}
{int=[2, 3, 4], class java.lang.Integer=[2, 3, 4], class java.lang.String=[foo, bar, baz]}

Обновление 1

Хорошо, давайте приступим к созданию isClassKeyMultimap метод немного умнее.

Я хотел бы работать с Multimap<Class<Something>, Something>, тоже.

Вы, наверное, говорите о TypeToken литералы. Да, я использовал TypeToken.getParameterized(...) забыв, что Class Экземпляр также может быть параметризован. Все, что вам нужно сделать, чтобы сделать его умнее, это просто добавить дополнительную проверку метода.

if ( Multimap.class.isAssignableFrom(typeToken.getRawType()) ) {
    final Type type = typeToken.getType();
    if ( type instanceof ParameterizedType ) {
        final ParameterizedType parameterizedType = (ParameterizedType) type;
        final Type actualTypeArg0 = parameterizedType.getActualTypeArguments()[0];
        // raw java.lang.Class (Class.class, Class.forName("java.lang.Class"), etc)
        if ( actualTypeArg0 == Class.class ) {
            return true;
        }
        // or maybe it's something like a Class<...> instance that:
        // * can be generated by javac when you parameterize a type (this is why Gson TypeToken's look "weird")
        // * or create a ParameterizedType instance yourself, say using TypeToken.getParameterized or your custom ParameterizedType implementation
        if ( actualTypeArg0 instanceof ParameterizedType && ((ParameterizedType) actualTypeArg0).getRawType() == Class.class ) {
            return true;
        }
    }
}
return false;

Специальные комментарии должны объяснить, почему предыдущая реализация не охватывала все случаи. Более того, вы можете написать повторно используемый служебный метод, который распознает сам необработанный класс.

private static Type getRawClass(final Type type) {
    if ( type instanceof ParameterizedType ) {
        return ((ParameterizedType) type).getRawType();
    }
    return type;
}

И тогда эти две проверки могут быть объединены в одну:

if ( getRawClass(actualTypeArg0) == Class.class ) {
    return true;
}

== должно нормально работать с java.class.Class экземпляры, поскольку его экземпляры являются эффективными весами и могут улучшить читабельность здесь.

Другие вопросы по тегам