在运行时交换 Jackson 自定义序列化器/反序列化器

Swap Jackson custom serializer / deserializer during runtime

我有以下系统:

我使用 @JsonSerialize@JsonDeserialize 注释为我的一些 objects 定义自定义序列化。 我想为 export/import.

以不同的方式序列化那些 objects

所以我想为 export/import 任务交换序列化器/反序列化器。像这样:

如果我对 docs 的理解正确,那两个注释只允许一个 using class。但是我想注册多个 serializers/deserializers 并根据一些条件逻辑使用它们。

您可能希望为服务器和客户端配置两个单独的 ObjectMapper 实例。

服务器模块:

ObjectMapper serverMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(ServerDTO.class, new CustomerFileSerializer());
module.addDeserializer(ServerDTO.class, new CustomerFileDeserializer());
serverMapper.registerModule(module);

ServerDTO serverDto = serverMapper.readValue(jsonInput, ServerDTO.class);
String serialized = serverMapper.writeValueAsString(serverDto);

客户端模块:

ObjectMapper clientMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(ClientDTO.class, new CustomerClientSerializer());
module.addDeserializer(ClientDTO.class, new CustomerClientDeserializer());
clientMapper.registerModule(module);

ClientDTO clientDTO = clientMapper.readValue(jsonInput, ClientDTO.class);
String serialized = clientMapper.writeValueAsString(clientDTO);

所以我最近几天一直在努力解决这个问题。这是我到目前为止取得的进展:

我在 Spring 和 made sure they are configured like the default 中为默认的 ObjectMapper 做了两次覆盖。

我的自定义映射器如下所示:

@Configuration
public class JacksonConfig {

    @Bean
    @Primary
    public ObjectMapper defaultV7ObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule())
                        .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
                        .configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);

        // emulate the default settings as described here: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-customize-the-jackson-objectmapper
        objectMapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        SimpleModule module = new SimpleModule();
        module.addSerializer(Customer.class, new CustomerClientSerializer());
        module.addDeserializer(Customer.class, new CustomerClientDeserializer());
        objectMapper.registerModule(module);

        return objectMapper;
    }

    @Bean("exportImportMapper")
    public ObjectMapper exportImportMapper() {
        ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule())
                .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
                .configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);

        // emulate the default settings as described here: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-customize-the-jackson-objectmapper
        objectMapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        SimpleModule module = new SimpleModule();
        module.addSerializer(Customer.class, new CustomerFileSerializer());
        module.addDeserializer(Customer.class, new CustomerFileDeserializer());
        objectMapper.registerModule(module);

        return objectMapper;
    }
}

我还从我的实体中删除了 @JsonSerialize@JsonDeserialize 注释。

但是,从注释到通过模块添加序列化程序的这一变化有一个很大的不同。

  • 假设我有一个 class A,它有一个带有 @JsonSerialize@JsonDeserialize 注释的 Customer 属性。
  • 假设我有一个 class B 有一个 Customer 属性 没有注释。

通过删除注释并设置 serializer/deserializer,如上所示,我现在已将这些 serializers/deserializers 添加到 both 客户属性。所以不等价。

还是我漏掉了什么?

这是我的解决方案

它不漂亮,但它的工作。

我保留了旧的 jackson 配置不变,因此客户端 <-> 服务器序列化保持不变。 然后我添加了这个自定义 ObjectMapper 来处理我的服务器<->文件。

我的自定义 ObjectMapper 执行以下操作:

  1. 它注册了一个新的自定义 JacksonAnnotationIntrospector,我将其配置为忽略某些注释。我还将它配置为在 属性 同时具有 @TransferJsonTypeInfo@JsonTypeInfo 注释时使用我自制的注释 @TransferJsonTypeInfo
  2. 我为这个 ObjectMapper 注册了我的 CustomerFileSerializerCustomerFileDeserializer
@Service
public class ImportExportMapper {

    protected final ObjectMapper customObjectMapper;

    private static final JacksonAnnotationIntrospector IGNORE_JSON_ANNOTATIONS_AND_USE_TRANSFERJSONTYPEINFO = BuildImportExportJacksonAnnotationIntrospector();

    public ImportExportMapper(){
        customObjectMapper = new ObjectMapper().registerModule(new JavaTimeModule())
                .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
                .configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);

        // emulate the default settings as described here: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-customize-the-jackson-objectmapper
        customObjectMapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
        customObjectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        SimpleModule module = new SimpleModule();
        module.addSerializer(Customer.class, new CustomerFileSerializer());
        module.addDeserializer(Customer.class, new CustomerFileDeserializer());

        customObjectMapper.setAnnotationIntrospector(IGNORE_JSON_ANNOTATIONS_AND_USE_TRANSFERJSONTYPEINFO);

        customObjectMapper.registerModule(module);
    }

    public String writeValueAsString(Object data) {
        try {
            return customObjectMapper.writeValueAsString(data);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            throw new IllegalArgumentException();
        }
    }

    public ObjectTransferData readValue(String fileContent, Class clazz) throws JsonProcessingException {
        return customObjectMapper.readValue(fileContent, clazz);
    }

    private static JacksonAnnotationIntrospector BuildImportExportJacksonAnnotationIntrospector() {
        return new JacksonAnnotationIntrospector() {

            @Override
            protected <A extends Annotation> A _findAnnotation(final Annotated annotated, final Class<A> annoClass) {
                if (annoClass == JsonTypeInfo.class && _hasAnnotation(annotated, FileJsonTypeInfo.class)) {
                    FileJsonTypeInfo fileJsonTypeInfo = _findAnnotation(annotated, TransferJsonTypeInfo.class);
                    if(fileJsonTypeInfo != null && fileJsonTypeInfo.jsonTypeInfo() != null) {
                        return (A) fileJsonTypeInfo.jsonTypeInfo(); // this cast should be safe because we have checked the annotation class
                    }
                }
                if (ignoreJsonAnnotations(annoClass)) return null;
                return super._findAnnotation(annotated, annoClass);
            }
        };
    }

    private static <A extends Annotation> boolean ignoreJsonAnnotations(Class<A> annoClass) {
        if (annoClass == JsonSerialize.class) {
            return true;
        }
        if(annoClass == JsonDeserialize.class){
            return true;
        }
        if(annoClass == JsonIdentityReference.class){
            return true;
        }
        return annoClass == JsonIdentityInfo.class;
    }
}

我的自定义注解是这样定义和描述的:

/**
 * This annotation inside of a annotation solution is a way to tell the importExportMapper how to serialize/deserialize
 * objects that already have a wrongly defined @JsonTypeInfo annotation (wrongly defined for the importExportMapper).
 *
 * Idea is taken from here: 
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FileJsonTypeInfo {
    JsonTypeInfo jsonTypeInfo();
}

并且是这样使用的:

    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
    @JsonTypeInfo(defaultImpl = Customer.class, property = "", use = JsonTypeInfo.Id.NONE)
    @TransferJsonTypeInfo(jsonTypeInfo = @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "customeridentifier"))
    @JsonIdentityReference(alwaysAsId = true)
    @JsonDeserialize(using = CustomerClientDeserializer.class)
    @JsonSerialize(using = CustomerClientSerializer.class)
    private Customer customer;