如何编写Jackson转换器来处理泛型集合类型?

p5fdfcr1  于 7个月前  发布在  其他
关注(0)|答案(3)|浏览(50)

我已经创建了一个JacksonConverter,它以升序排列Set元素,因此生成的JSON数组以排序顺序输出,而不是给定Set实现所使用的任意迭代顺序。它扩展了Jackson中的标准StdConverter类,正如Converter documentation所推荐的那样。
注意:强烈建议实现者扩展StdConverter,而不是直接实现Converter,因为这有助于典型样板代码的默认实现。

public class OrderedSetConverter
        extends StdConverter<Set<DayOfWeek>, Set<DayOfWeek>> {
    @Override
    public Set<DayOfWeek> convert(Set<DayOfWeek> value) {
        return value == null ? null : value.stream()
                .sorted(Comparator.nullsLast(Comparator.naturalOrder()))
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }
}

public class MyType {
    @JsonSerialize(converter = OrderedSetConverter.class)
    private Set<DayOfWeek> myValues;
}

Converter用于要转换的特定类型的元素(本例中为DayOfWeek)时,这很有效。然而,在这个实现中没有任何东西是特定于特定类型的;它对于任何Comparable类型都同样有效。因此,我更希望有一个通用的实现这个转换器,可用于任何Set的可比。
我试图通过使用泛型的简单用法来实现这一点:

public class OrderedSetConverter<E extends Comparable<? super E>>
        extends StdConverter<Set<E>, Set<E>> {
    @Override
    public Set<E> convert(Set<E> value) {
        return value == null ? null : value.stream()
                .sorted(Comparator.nullsLast(Comparator.naturalOrder()))
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }
}

这并不像我希望的那样工作,因为Jackson试图(但失败了)将集合的元素类型转换为Comparable,而不是实际的类型(例如,DayOfWeek)。

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.lang.Comparable` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
 at [Source: (String)"{"daysOfWeek":["MONDAY","TUESDAY","WEDNESDAY","THURSDAY","FRIDAY","SATURDAY","SUNDAY"]}"; line: 1, column: 16] (through reference chain: com.example.MyType["daysOfWeek"]->java.util.HashSet[0])
    at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
    at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1764)
    at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400)
    at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1209)
    at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserialize(AbstractDeserializer.java:274)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer._deserializeFromArray(CollectionDeserializer.java:347)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:244)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:28)
    at com.fasterxml.jackson.databind.deser.std.StdDelegatingDeserializer.deserialize(StdDelegatingDeserializer.java:175)
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:324)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:187)
    at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4593)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3548)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3516)

我能想到的最好的方法是将其用作抽象超类,并为实际使用的每个类型提供每个特定子类型的Converter

public abstract class OrderedSetConverter<E extends Comparable<? super E>>
        extends StdConverter<Set<E>, Set<E>> {
    @Override
    public Set<E> convert(Set<E> value) {
        return value == null ? null : value.stream()
                .sorted(Comparator.nullsLast(Comparator.naturalOrder()))
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }
}

public class DayOfWeekSetConverter extends OrderedSetConverter<DayOfWeek> { }

public class MyType {
    @JsonSerialize(converter = DayOfWeekSetConverter.class)
    private Set<DayOfWeek> myValues;
}

我如何为泛型集合类型编写转换器并让Jackson找出元素类型?

fcwjkofz

fcwjkofz1#

OrderedSetConverter不知道在初始化过程中必须示例化哪种类型。我们需要指导石化过程使用哪种类型。如果你想保持转换器的通用性,你需要在创建转换器时提供这些信息。要扩展默认行为,您需要实现自定义com.fasterxml.jackson.databind.cfg.HandlerInstantiator

class ConverterHandlerInstantiator extends HandlerInstantiator {

    @Override
    public Converter<?, ?> converterInstance(MapperConfig<?> config, Annotated annotated, Class<?> implClass) {
        if (config instanceof DeserializationConfig) {
            JsonDeserialize jsonDeserialize = annotated.getAnnotation(JsonDeserialize.class);
            Class contentAs = jsonDeserialize.contentAs();
            if (contentAs != Void.class) {
                return new OrderedSetConverter<>(contentAs);
            }
        }
        return super.converterInstance(config, annotated, implClass);
    }

    @Override
    public JsonDeserializer<?> deserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> deserClass) {
        return null;
    }

    @Override
    public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> keyDeserClass) {
        return null;
    }

    @Override
    public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) {
        return null;
    }

    @Override
    public TypeResolverBuilder<?> typeResolverBuilderInstance(MapperConfig<?> config, Annotated annotated, Class<?> builderClass) {
        return null;
    }

    @Override
    public TypeIdResolver typeIdResolverInstance(MapperConfig<?> config, Annotated annotated, Class<?> resolverClass) {
        return null;
    }
}

正如你在上面注意到的,代码使用了来自JsonDeserialize注解的contentAs属性。我们需要在你的类中指定它:

class MyType {
    @JsonSerialize(converter = OrderedSetConverter.class)
    @JsonDeserialize(converter = OrderedSetConverter.class, contentAs = DayOfWeek.class)
    private Set<DayOfWeek> myValues;
}

当我们序列化对象时,这个值是不需要的,所以,我们可以跳过它。现在,我们需要注册我们的自定义instantiator:

ObjectMapper mapper = new ObjectMapper();
mapper.setHandlerInstantiator(new ConverterHandlerInstantiator());

最后,我们的通用转换器:

class OrderedSetConverter<E extends Comparable<? super E>> extends StdConverter<Set<E>, Set<E>> {
    private final Class<E> contentClass;

    OrderedSetConverter() {
        this(null);
    }

    public OrderedSetConverter(Class<E> contentClass) {
        this.contentClass = contentClass;
    }

    @Override
    public Set<E> convert(Set<E> value) {
        return value == null ? null : value.stream()
                .sorted(Comparator.nullsLast(Comparator.naturalOrder()))
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }

    @Override
    public JavaType getInputType(TypeFactory typeFactory) {
        if (contentClass == null) {
            return super.getInputType(typeFactory);
        }
        return typeFactory.constructCollectionType(Set.class, contentClass);
    }
}
r3i60tvu

r3i60tvu2#

Another answer指出,可以使用自定义的JacksonHandlerInstantiator在对象构造时在Converter中设置类型变量。这个答案需要使用@JsonDeserialize.contentAs手动指定集合类型。在尝试了这种方法之后,我发现了一种在HandlerInstantiator中自动推导类型变量的方法:

HandlerInstantiator

public class OrderedSetConverterHandlerInstantiator extends HandlerInstantiator {
    @Override
    public Converter<?, ?> converterInstance(
            MapperConfig<?> config, Annotated annotated, Class<?> implClass) {
        if (OrderedSetJacksonConverter.class.equals(implClass)) {
            return new OrderedSetJacksonConverter<>(
                    getSetContentType(config, annotated));
        } else {
            return null;
        }
    }

    private JavaType getSetContentType(MapperConfig<?> config, Annotated annotated) {
        Type setType = getPropertyType(config, annotated);
        return TypeFactory.defaultInstance().constructType(setType).getContentType();
    }

    private Type getPropertyType(MapperConfig<?> config, Annotated annotated) {
        // TODO: Enhance to handle other cases, such as annotated constructor parameter
        BeanPropertyDefinition beanPropertyDefinition =
                SimpleBeanPropertyDefinition.construct(config, 
                        (AnnotatedMember) annotated);
        AnnotatedMember setter = beanPropertyDefinition.getSetter();
        AnnotatedMember getter = beanPropertyDefinition.getGetter();
        AnnotatedMember field = beanPropertyDefinition.getField();

        Type setType;
        if (setter != null) {
            setType = ((Method) setter.getMember()).getGenericParameterTypes()[0];
        } else if (getter != null) {
            setType = ((Method) getter.getMember()).getGenericReturnType();
        } else if (field != null) {
            setType = ((Field) field.getMember()).getGenericType();
        } else {
            throw new UnsupportedOperationException(
                    "Annotated type not yet supported: " + annotated.getClass());
        }
        return setType;
    }
    
    // Implement other required methods such as serializerInstance to return null
}

Converter

public class OrderedSetConverter<T extends Comparable<? super T>> 
        extends StdConverter<Set<T>, Set<T>> {
    private final JavaType contentType;

    public OrderedSetJacksonConverter(JavaType contentType) {
        this.contentType = contentType;
    }

    @Override
    public Set<T> convert(Set<T> value) {
        return value == null ? null : value.stream()
                .sorted(Comparator.nullsLast(Comparator.naturalOrder()))
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }

    @Override
    public JavaType getInputType(TypeFactory typeFactory) {
        return typeFactory.constructCollectionType(Set.class, contentType);
    }

    @Override
    public JavaType getOutputType(TypeFactory typeFactory) {
        return typeFactory.constructCollectionType(Set.class, contentType);
    }
}

ObjectMapper配置

JsonMapper jsonMapper = JsonMapper.builder()
            .handlerInstantiator(new OrderedSetConverterHandlerInstantiator())

解决方案的一个笨拙之处在于确定属性类型的方式,因为它手动迭代Annotated值可能是(getter/setter/field)的所有可能的注解元素类型来确定元素类型。希望有一个更干净,更习惯的方法,虽然如果有,我还没有找到它。

Spring framework中注册转换器

Spring Web附带了自己的HandlerInstantiatorSpringHandlerInstantiator,它会自动应用到由Jackson2ObjectMapperBuilder创建的ObjectMapper。此处理程序使用HandlerInstantiator提供的类型的类路径中的任何相关bean。
为了在使用自定义类型的同时保持这种行为,可以编写一个HandlerInstantiator类,在适当的时候使用自定义转换器,否则回退到默认的SpringHandlerInstantiator。实现这一点的一种方式如下:

public class DelegatingHandlerInstantiator extends SpringHandlerInstantiator {
    private final OrderedSetConverterHandlerInstantiator handler =
            new OrderedSetConverterHandlerInstantiator();

    public DelegatingHandlerInstantiator(AutowireCapableBeanFactory beanFactory) {
        super(beanFactory);
    }

    @Override
    public Converter<?, ?> converterInstance(
            MapperConfig<?> config, Annotated annotated, Class<?> implClass) {
        Converter<?, ?> converter =
                handler.converterInstance(config, annotated, implClass);
        return Objects.requireNonNullElseGet(converter,
                () -> super.converterInstance(config, annotated, implClass));
    }
}

然后,应用程序使用的Jackson2ObjectMapperBuilder应该配置为使用此处理程序。如果应用程序是Spring Boot应用程序,则可以创建Jackson2ObjectMapperBuilderCustomizer bean,以在Jackson2ObjectMapperBuilder bean中设置HandlerInstantiator

@Configuration
public class JacksonConfig {
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer(
            AutowireCapableBeanFactory beanFactory) {
        return b -> b.handlerInstantiator(
                new DelegatingHandlerInstantiator(beanFactory));
    }
}
yqhsw0fo

yqhsw0fo3#

一种解决方案是使用JsonSerializer,而不是Converter

public class MyType {
    @JsonSerialize(using = OrderedSetSerializer.class)
    private Set<DayOfWeek> myValues;
}
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.CollectionSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

import java.io.IOException;
import java.util.Objects;
import java.util.Set;

public class OrderedSetSerializer<T extends Comparable<? super T>> 
        extends StdSerializer<Set<T>> implements ContextualSerializer {
    private final CollectionSerializer collectionSerializer;

    /**
     * For Jackson to instantiate and call 
     * {@link #createContextual(SerializerProvider, BeanProperty)}.
     */
    public OrderedSetSerializer() {
        super(Set.class, false);
        this.collectionSerializer = null;
    }

    public OrderedSetSerializer(JavaType valueType, 
                                CollectionSerializer collectionSerializer) {
        super(valueType);
        this.collectionSerializer =
                Objects.requireNonNull(collectionSerializer);
    }

    @Override
    public void serialize(Set<T> value, JsonGenerator gen, 
                          SerializerProvider provider) throws IOException {
        Objects.requireNonNull(collectionSerializer,
                "No-arg OrderedSetSerializer cannot be used for serialization, and exists for createContextual");

        Set<T> sorted = value == null ? null : value.stream()
                .sorted(Comparator.nullsLast(Comparator.naturalOrder()))
                .collect(Collectors.toCollection(LinkedHashSet::new));

        collectionSerializer.serialize(sorted, gen, provider);
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, 
                                              BeanProperty property) throws JsonMappingException {
        CollectionSerializer collectionSerializer = 
        new CollectionSerializer(property.getType(), false,
                prov.findTypeSerializer(property.getType()),
                prov.findValueSerializer(property.getType().findTypeParameters(Set.class)[0]));
        return new OrderedSetSerializer<>(property.getType(), collectionSerializer);
    }
}

同样地,为了强制执行格式化顺序,可以使用JsonDeserializer

public class MyType {
    @JsonDeserialize(using = OrderedSetDeserializer.class)
    private Set<DayOfWeek> myValues;
}
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;

import java.io.IOException;
import java.util.Set;

public class OrderedSetDeserializer<T extends Comparable<? super T>> 
        extends StdDeserializer<Set<T>> implements ContextualDeserializer {
    /**
     * For Jackson to instantiate and call
     * {@link #createContextual(DeserializationContext, BeanProperty)}.
     */
    public OrderedSetDeserializer() {
        super(Set.class);
    }

    public OrderedSetDeserializer(JavaType valueType) {
        super(valueType);
    }

    @Override
    public Set<T> deserialize(JsonParser parser, 
                              DeserializationContext context) throws IOException {
        JavaType parameterizedSet = context.getTypeFactory()
                .constructParametricType(Set.class, getValueType().getBindings());
        Set<T> value = context.readValue(parser, parameterizedSet);

        return value == null ? null : value.stream()
                .sorted(Comparator.nullsLast(Comparator.naturalOrder()))
                .collect(Collectors.toCollection(LinkedHashSet::new));
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext context, 
                                                BeanProperty property) {
        return new OrderedSetDeserializer<>(context.getContextualType());
    }
}

相关问题