在运行时交换 Jackson 自定义序列化器/反序列化器
Swap Jackson custom serializer / deserializer during runtime
我有以下系统:
- 我正在从 spring 控制器向我的客户端发送
MediaType.APPLICATION_JSON_VALUE
s,反之亦然。
- 我的 to-be-serialized classes 也有一个 export/import 功能。 JSON 文件是使用
ObjectMapper
并利用 writeValueAsString
和 readValue
方法创建的。我正在读取和写入 json 文件。
- 这两个序列化路径目前使用相同的 serializers/deserializers。
我使用 @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 执行以下操作:
- 它注册了一个新的自定义 JacksonAnnotationIntrospector,我将其配置为忽略某些注释。我还将它配置为在 属性 同时具有
@TransferJsonTypeInfo
和 @JsonTypeInfo
注释时使用我自制的注释 @TransferJsonTypeInfo
。
- 我为这个 ObjectMapper 注册了我的
CustomerFileSerializer
和 CustomerFileDeserializer
。
@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;
我有以下系统:
- 我正在从 spring 控制器向我的客户端发送
MediaType.APPLICATION_JSON_VALUE
s,反之亦然。 - 我的 to-be-serialized classes 也有一个 export/import 功能。 JSON 文件是使用
ObjectMapper
并利用writeValueAsString
和readValue
方法创建的。我正在读取和写入 json 文件。 - 这两个序列化路径目前使用相同的 serializers/deserializers。
我使用 @JsonSerialize
和 @JsonDeserialize
注释为我的一些 objects 定义自定义序列化。
我想为 export/import.
所以我想为 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 执行以下操作:
- 它注册了一个新的自定义 JacksonAnnotationIntrospector,我将其配置为忽略某些注释。我还将它配置为在 属性 同时具有
@TransferJsonTypeInfo
和@JsonTypeInfo
注释时使用我自制的注释@TransferJsonTypeInfo
。 - 我为这个 ObjectMapper 注册了我的
CustomerFileSerializer
和CustomerFileDeserializer
。
@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;