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:
John Blum
2023-06-14 13:41:19 -07:00
parent 46279d8bea
commit 17820d1d52
2 changed files with 91 additions and 16 deletions

View File

@@ -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()) {

View File

@@ -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);