Allow users to customize the internal ObjectMapper created by GenericJackson2JsonRedisSerializer.
We now allow the internally created Jackson ObjectMapper to be customized and further configured after construction of the GenericJackson2JsonRedisSerializer when a user does not explicitly provide a custom ObjectMapper during construction. Even when providing a custom ObjectMapper, not all configuration applied by the GenericJackson2JsonRedisSerialzier (such as (standard) type resolution) to the internal ObjectMapper would get applied to the user-provided ObjectMapper as well. Closes #2601
This commit is contained in:
@@ -17,6 +17,7 @@ package org.springframework.data.redis.serializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.cache.support.NullValue;
|
||||
@@ -54,6 +55,7 @@ import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @author Mao Shuai
|
||||
* @author John Blum
|
||||
* @since 1.6
|
||||
*/
|
||||
public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Object> {
|
||||
@@ -197,6 +199,16 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
|
||||
objectMapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the configured {@link ObjectMapper} used internally by this {@link GenericJackson2JsonRedisSerializer}
|
||||
* to de/serialize {@link Object objects} as {@literal JSON}.
|
||||
*
|
||||
* @return the configured {@link ObjectMapper}.
|
||||
*/
|
||||
protected ObjectMapper getObjectMapper() {
|
||||
return this.mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize(@Nullable Object source) throws SerializationException {
|
||||
|
||||
@@ -206,8 +218,9 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
|
||||
|
||||
try {
|
||||
return writer.write(mapper, source);
|
||||
} catch (IOException e) {
|
||||
throw new SerializationException("Could not write JSON: " + e.getMessage(), e);
|
||||
} catch (IOException cause) {
|
||||
String message = String.format("Could not write JSON: %s", cause.getMessage());
|
||||
throw new SerializationException(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,11 +248,33 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
|
||||
|
||||
try {
|
||||
return (T) reader.read(mapper, source, resolveType(source, type));
|
||||
} catch (Exception ex) {
|
||||
throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex);
|
||||
} catch (Exception cause) {
|
||||
String message = String.format("Could not read JSON:%s ", cause.getMessage());
|
||||
throw new SerializationException(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder method used to configure and customize the internal Jackson {@link ObjectMapper} created by
|
||||
* this {@link GenericJackson2JsonRedisSerializer} and used to de/serialize {@link Object objects}
|
||||
* as {@literal JSON}.
|
||||
*
|
||||
* @param objectMapperConfigurer {@link Consumer} used to configure and customize the internal {@link ObjectMapper};
|
||||
* must not be {@literal null}.
|
||||
* @return this {@link GenericJackson2JsonRedisSerializer}.
|
||||
* @throws IllegalArgumentException if the {@link Consumer} used to configure and customize
|
||||
* the internal {@link ObjectMapper} is {@literal null}.
|
||||
*/
|
||||
public GenericJackson2JsonRedisSerializer configure(Consumer<ObjectMapper> objectMapperConfigurer) {
|
||||
|
||||
Assert.notNull(objectMapperConfigurer,
|
||||
"Consumer used to configure and customize ObjectMapper must not be null");
|
||||
|
||||
objectMapperConfigurer.accept(getObjectMapper());
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
protected JavaType resolveType(byte[] source, Class<?> type) throws IOException {
|
||||
|
||||
if (!type.equals(Object.class) || !defaultTypingEnabled.get()) {
|
||||
|
||||
@@ -15,17 +15,20 @@
|
||||
*/
|
||||
package org.springframework.data.redis.serializer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.test.util.ReflectionTestUtils.*;
|
||||
import static org.springframework.util.ObjectUtils.*;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.util.ReflectionTestUtils.getField;
|
||||
import static org.springframework.util.ObjectUtils.nullSafeEquals;
|
||||
import static org.springframework.util.ObjectUtils.nullSafeHashCode;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -33,11 +36,14 @@ import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.cache.support.NullValue;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
|
||||
@@ -47,15 +53,22 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
import org.springframework.lang.Nullable;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link GenericJackson2JsonRedisSerializer}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @author John Blum
|
||||
*/
|
||||
class GenericJackson2JsonRedisSerializerUnitTests {
|
||||
|
||||
@@ -407,6 +420,33 @@ class GenericJackson2JsonRedisSerializerUnitTests {
|
||||
assertThat(serializer.deserialize(source, WithJsr310.class).myDate).isEqualTo(java.time.LocalDate.of(2022,9,2));
|
||||
}
|
||||
|
||||
@Test // GH-2601
|
||||
public void internalObjectMapperCustomization() {
|
||||
|
||||
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
|
||||
|
||||
com.fasterxml.jackson.databind.Module mockModule = mock(com.fasterxml.jackson.databind.Module.class);
|
||||
|
||||
ObjectMapper mockObjectMapper = mock(ObjectMapper.class);
|
||||
|
||||
Consumer<ObjectMapper> configurer = objectMapper -> mockObjectMapper.registerModule(mockModule);
|
||||
|
||||
assertThat(serializer.configure(configurer)).isSameAs(serializer);
|
||||
|
||||
verify(mockObjectMapper, times(1)).registerModule(eq(mockModule));
|
||||
verifyNoMoreInteractions(mockObjectMapper);
|
||||
verifyNoInteractions(mockModule);
|
||||
}
|
||||
|
||||
@Test // GH-2601
|
||||
public void configureWithNullConsumerThrowsIllegalArgumentException() {
|
||||
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new GenericJackson2JsonRedisSerializer().configure(null))
|
||||
.withMessage("Consumer used to configure and customize ObjectMapper must not be null")
|
||||
.withNoCause();
|
||||
}
|
||||
|
||||
private static void serializeAndDeserializeNullValue(GenericJackson2JsonRedisSerializer serializer) {
|
||||
|
||||
NullValue nv = BeanUtils.instantiateClass(NullValue.class);
|
||||
|
||||
Reference in New Issue
Block a user