Skip to content

Commit 4da3bdf

Browse files
committed
Refactor GenericJackson2JsonRedisSerializer.
* Cleanup and simpifly source code. * Fix compiler warnings. * Edit Javadoc. Closes #2609
1 parent 433bb89 commit 4da3bdf

File tree

1 file changed

+108
-87
lines changed

1 file changed

+108
-87
lines changed

src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java

+108-87
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616
package org.springframework.data.redis.serializer;
1717

1818
import java.io.IOException;
19+
import java.io.Serial;
1920
import java.util.Collections;
2021
import java.util.function.Supplier;
2122

2223
import org.springframework.cache.support.NullValue;
2324
import org.springframework.core.KotlinDetector;
25+
import org.springframework.data.redis.util.RedisAssertions;
2426
import org.springframework.data.util.Lazy;
2527
import org.springframework.lang.Nullable;
2628
import org.springframework.util.Assert;
@@ -46,41 +48,66 @@
4648
import com.fasterxml.jackson.databind.type.TypeFactory;
4749

4850
/**
49-
* Generic Jackson 2-based {@link RedisSerializer} that maps {@link Object objects} to JSON using dynamic typing.
51+
* Generic Jackson 2-based {@link RedisSerializer} that maps {@link Object objects} to and from {@literal JSON}
52+
* using dynamic typing.
5053
* <p>
51-
* JSON reading and writing can be customized by configuring {@link JacksonObjectReader} respective
52-
* {@link JacksonObjectWriter}.
54+
* {@literal JSON} reading and writing can be customized by configuring a {@link JacksonObjectReader}
55+
* and {@link JacksonObjectWriter}.
5356
*
5457
* @author Christoph Strobl
5558
* @author Mark Paluch
5659
* @author Mao Shuai
60+
* @author John Blum
61+
* @see org.springframework.data.redis.serializer.JacksonObjectReader
62+
* @see org.springframework.data.redis.serializer.JacksonObjectWriter
63+
* @see com.fasterxml.jackson.databind.ObjectMapper
5764
* @since 1.6
5865
*/
5966
public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Object> {
6067

61-
private final ObjectMapper mapper;
68+
/**
69+
* Register {@link NullValueSerializer} in the given {@link ObjectMapper} with an optional
70+
* {@code classPropertyTypeName}. This method should be called by code that customizes
71+
* {@link GenericJackson2JsonRedisSerializer} by providing an external {@link ObjectMapper}.
72+
*
73+
* @param objectMapper the object mapper to customize.
74+
* @param classPropertyTypeName name of the type property. Defaults to {@code @class} if {@literal null}/empty.
75+
* @since 2.2
76+
*/
77+
public static void registerNullValueSerializer(ObjectMapper objectMapper, @Nullable String classPropertyTypeName) {
78+
79+
// Simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here
80+
// since we need the type hint embedded for deserialization using the default typing feature.
81+
objectMapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName)));
82+
}
6283

6384
private final JacksonObjectReader reader;
6485

6586
private final JacksonObjectWriter writer;
6687

6788
private final Lazy<Boolean> defaultTypingEnabled;
6889

90+
private final ObjectMapper mapper;
91+
6992
private final TypeResolver typeResolver;
7093

7194
/**
72-
* Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing.
95+
* Creates {@link GenericJackson2JsonRedisSerializer} initialized with an {@link ObjectMapper} configured for
96+
* default typing.
7397
*/
7498
public GenericJackson2JsonRedisSerializer() {
7599
this((String) null);
76100
}
77101

78102
/**
79-
* Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing using the
80-
* given {@literal name}. In case of an {@literal empty} or {@literal null} String the default
81-
* {@link JsonTypeInfo.Id#CLASS} will be used.
103+
* Creates {@link GenericJackson2JsonRedisSerializer} initialized with an {@link ObjectMapper} configured for
104+
* default typing using the given {@link String name}.
105+
* <p>
106+
* In case {@link String name} is {@literal empty} or {@literal null}, then {@link JsonTypeInfo.Id#CLASS}
107+
* will be used.
82108
*
83-
* @param classPropertyTypeName name of the JSON property holding type information. Can be {@literal null}.
109+
* @param classPropertyTypeName {@link String name} of the JSON property holding type information;
110+
* can be {@literal null}.
84111
* @see ObjectMapper#activateDefaultTypingAsProperty(PolymorphicTypeValidator, DefaultTyping, String)
85112
* @see ObjectMapper#activateDefaultTyping(PolymorphicTypeValidator, DefaultTyping, As)
86113
*/
@@ -89,13 +116,17 @@ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName
89116
}
90117

91118
/**
92-
* Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing using the
93-
* given {@literal name}. In case of an {@literal empty} or {@literal null} String the default
94-
* {@link JsonTypeInfo.Id#CLASS} will be used.
119+
* Creates {@link GenericJackson2JsonRedisSerializer} initialized with an {@link ObjectMapper} configured for
120+
* default typing using the given {@link String name} along with the given, required {@link JacksonObjectReader}
121+
* and {@link JacksonObjectWriter} used to read/write {@link Object Objects} de/serialized as JSON.
122+
* <p>
123+
* In case {@link String name} is {@literal empty} or {@literal null}, then {@link JsonTypeInfo.Id#CLASS}
124+
* will be used.
95125
*
96-
* @param classPropertyTypeName name of the JSON property holding type information. Can be {@literal null}.
97-
* @param reader the {@link JacksonObjectReader} function to read objects using {@link ObjectMapper}.
98-
* @param writer the {@link JacksonObjectWriter} function to write objects using {@link ObjectMapper}.
126+
* @param classPropertyTypeName {@link String name} of the JSON property holding type information;
127+
* can be {@literal null}.
128+
* @param reader {@link JacksonObjectReader} function to read objects using {@link ObjectMapper}.
129+
* @param writer {@link JacksonObjectWriter} function to write objects using {@link ObjectMapper}.
99130
* @see ObjectMapper#activateDefaultTypingAsProperty(PolymorphicTypeValidator, DefaultTyping, String)
100131
* @see ObjectMapper#activateDefaultTyping(PolymorphicTypeValidator, DefaultTyping, As)
101132
* @since 3.0
@@ -105,19 +136,17 @@ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName
105136

106137
this(new ObjectMapper(), reader, writer, classPropertyTypeName);
107138

108-
// simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here since we need
109-
// the type hint embedded for deserialization using the default typing feature.
110-
registerNullValueSerializer(mapper, classPropertyTypeName);
139+
registerNullValueSerializer(this.mapper, classPropertyTypeName);
111140

112-
StdTypeResolverBuilder typer = new TypeResolverBuilder(DefaultTyping.EVERYTHING,
113-
mapper.getPolymorphicTypeValidator());
114-
typer = typer.init(JsonTypeInfo.Id.CLASS, null);
115-
typer = typer.inclusion(JsonTypeInfo.As.PROPERTY);
141+
StdTypeResolverBuilder typer = TypeResolverBuilder.forEverything(this.mapper)
142+
.init(JsonTypeInfo.Id.CLASS, null)
143+
.inclusion(JsonTypeInfo.As.PROPERTY);
116144

117145
if (StringUtils.hasText(classPropertyTypeName)) {
118146
typer = typer.typeProperty(classPropertyTypeName);
119147
}
120-
mapper.setDefaultTyping(typer);
148+
149+
this.mapper.setDefaultTyping(typer);
121150
}
122151

123152
/**
@@ -143,58 +172,34 @@ public GenericJackson2JsonRedisSerializer(ObjectMapper mapper) {
143172
*/
144173
public GenericJackson2JsonRedisSerializer(ObjectMapper mapper, JacksonObjectReader reader,
145174
JacksonObjectWriter writer) {
175+
146176
this(mapper, reader, writer, null);
147177
}
148178

149179
private GenericJackson2JsonRedisSerializer(ObjectMapper mapper, JacksonObjectReader reader,
150180
JacksonObjectWriter writer, @Nullable String typeHintPropertyName) {
151181

152-
Assert.notNull(mapper, "ObjectMapper must not be null");
153-
Assert.notNull(reader, "Reader must not be null");
154-
Assert.notNull(writer, "Writer must not be null");
155-
156-
this.mapper = mapper;
157-
this.reader = reader;
158-
this.writer = writer;
182+
this.mapper = RedisAssertions.requireObject(mapper, "ObjectMapper must not be null");
183+
this.reader = RedisAssertions.requireObject(reader, "Reader must not be null");
184+
this.writer = RedisAssertions.requireObject(writer, "Writer must not be null");
159185

160-
this.defaultTypingEnabled = Lazy.of(() -> mapper.getSerializationConfig().getDefaultTyper(null) != null);
186+
this.defaultTypingEnabled = Lazy.of(() -> mapper.getSerializationConfig()
187+
.getDefaultTyper(null) != null);
161188

162-
Supplier<String> typeHintPropertyNameSupplier;
163-
164-
if (typeHintPropertyName == null) {
189+
this.typeResolver = new TypeResolver(Lazy.of(mapper::getTypeFactory),
190+
newTypeHintPropertyNameSupplier(mapper, typeHintPropertyName, this.defaultTypingEnabled));
191+
}
165192

166-
typeHintPropertyNameSupplier = Lazy.of(() -> {
167-
if (defaultTypingEnabled.get()) {
168-
return null;
169-
}
193+
private Supplier<String> newTypeHintPropertyNameSupplier(ObjectMapper mapper, @Nullable String typeHintPropertyName,
194+
Lazy<Boolean> defaultTypingEnabled) {
170195

171-
return mapper.getDeserializationConfig().getDefaultTyper(null)
196+
return typeHintPropertyName != null ? () -> typeHintPropertyName
197+
: Lazy.of(() -> defaultTypingEnabled.get() ? null
198+
: mapper.getDeserializationConfig().getDefaultTyper(null)
172199
.buildTypeDeserializer(mapper.getDeserializationConfig(),
173200
mapper.getTypeFactory().constructType(Object.class), Collections.emptyList())
174-
.getPropertyName();
175-
176-
}).or("@class");
177-
} else {
178-
typeHintPropertyNameSupplier = () -> typeHintPropertyName;
179-
}
180-
181-
this.typeResolver = new TypeResolver(Lazy.of(mapper::getTypeFactory), typeHintPropertyNameSupplier);
182-
}
183-
184-
/**
185-
* Register {@link NullValueSerializer} in the given {@link ObjectMapper} with an optional
186-
* {@code classPropertyTypeName}. This method should be called by code that customizes
187-
* {@link GenericJackson2JsonRedisSerializer} by providing an external {@link ObjectMapper}.
188-
*
189-
* @param objectMapper the object mapper to customize.
190-
* @param classPropertyTypeName name of the type property. Defaults to {@code @class} if {@literal null}/empty.
191-
* @since 2.2
192-
*/
193-
public static void registerNullValueSerializer(ObjectMapper objectMapper, @Nullable String classPropertyTypeName) {
194-
195-
// simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here since we need
196-
// the type hint embedded for deserialization using the default typing feature.
197-
objectMapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName)));
201+
.getPropertyName())
202+
.or("@class");
198203
}
199204

200205
@Override
@@ -206,8 +211,9 @@ public byte[] serialize(@Nullable Object source) throws SerializationException {
206211

207212
try {
208213
return writer.write(mapper, source);
209-
} catch (IOException e) {
210-
throw new SerializationException("Could not write JSON: " + e.getMessage(), e);
214+
} catch (IOException cause) {
215+
String message = String.format("Could not write JSON: %s", cause.getMessage());
216+
throw new SerializationException(message, cause);
211217
}
212218
}
213219

@@ -217,17 +223,24 @@ public Object deserialize(@Nullable byte[] source) throws SerializationException
217223
}
218224

219225
/**
220-
* @param source can be {@literal null}.
221-
* @param type must not be {@literal null}.
222-
* @return {@literal null} for empty source.
223-
* @throws SerializationException
226+
* Deserialized the array of bytes containing {@literal JSON} as an {@link Object} of the given,
227+
* required {@link Class type}.
228+
*
229+
* @param source array of bytes containing the {@literal JSON} to deserialize; can be {@literal null}.
230+
* @param type {@link Class type} of {@link Object} from which the {@literal JSON} will be deserialized;
231+
* must not be {@literal null}.
232+
* @return {@literal null} for an empty source, or an {@link Object} of the given {@link Class type}
233+
* deserialized from the array of bytes containing {@literal JSON}.
234+
* @throws IllegalArgumentException if the given {@link Class type} is {@literal null}.
235+
* @throws SerializationException if the array of bytes cannot be deserialized as an instance of
236+
* the given {@link Class type}
224237
*/
225238
@Nullable
226239
@SuppressWarnings("unchecked")
227240
public <T> T deserialize(@Nullable byte[] source, Class<T> type) throws SerializationException {
228241

229-
Assert.notNull(type,
230-
"Deserialization type must not be null Please provide Object.class to make use of Jackson2 default typing.");
242+
Assert.notNull(type, "Deserialization type must not be null;"
243+
+ " Please provide Object.class to make use of Jackson2 default typing.");
231244

232245
if (SerializationUtils.isEmpty(source)) {
233246
return null;
@@ -292,7 +305,9 @@ protected JavaType resolveType(byte[] source, Class<?> type) throws IOException
292305
*/
293306
private static class NullValueSerializer extends StdSerializer<NullValue> {
294307

308+
@Serial
295309
private static final long serialVersionUID = 1999052150548658808L;
310+
296311
private final String classIdentifier;
297312

298313
/**
@@ -305,17 +320,19 @@ private static class NullValueSerializer extends StdSerializer<NullValue> {
305320
}
306321

307322
@Override
308-
public void serialize(NullValue value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
323+
public void serialize(NullValue value, JsonGenerator jsonGenerator, SerializerProvider provider)
324+
throws IOException {
309325

310-
jgen.writeStartObject();
311-
jgen.writeStringField(classIdentifier, NullValue.class.getName());
312-
jgen.writeEndObject();
326+
jsonGenerator.writeStartObject();
327+
jsonGenerator.writeStringField(classIdentifier, NullValue.class.getName());
328+
jsonGenerator.writeEndObject();
313329
}
314330

315331
@Override
316-
public void serializeWithType(NullValue value, JsonGenerator gen, SerializerProvider serializers,
317-
TypeSerializer typeSer) throws IOException {
318-
serialize(value, gen, serializers);
332+
public void serializeWithType(NullValue value, JsonGenerator jsonGenerator, SerializerProvider serializers,
333+
TypeSerializer typeSerializer) throws IOException {
334+
335+
serialize(value, jsonGenerator, serializers);
319336
}
320337
}
321338

@@ -329,8 +346,12 @@ public void serializeWithType(NullValue value, JsonGenerator gen, SerializerProv
329346
*/
330347
private static class TypeResolverBuilder extends ObjectMapper.DefaultTypeResolverBuilder {
331348

332-
public TypeResolverBuilder(DefaultTyping t, PolymorphicTypeValidator ptv) {
333-
super(t, ptv);
349+
static TypeResolverBuilder forEverything(ObjectMapper mapper) {
350+
return new TypeResolverBuilder(DefaultTyping.EVERYTHING, mapper.getPolymorphicTypeValidator());
351+
}
352+
353+
public TypeResolverBuilder(DefaultTyping typing, PolymorphicTypeValidator polymorphicTypeValidator) {
354+
super(typing, polymorphicTypeValidator);
334355
}
335356

336357
@Override
@@ -343,25 +364,25 @@ public ObjectMapper.DefaultTypeResolverBuilder withDefaultImpl(Class<?> defaultI
343364
* Boolean, Integer, Double) will never use typing; that is both due to them being concrete and final, and since
344365
* actual serializers and deserializers will also ignore any attempts to enforce typing.
345366
*/
346-
public boolean useForType(JavaType t) {
367+
public boolean useForType(JavaType javaType) {
347368

348-
if (t.isJavaLangObject()) {
369+
if (javaType.isJavaLangObject()) {
349370
return true;
350371
}
351372

352-
t = resolveArrayOrWrapper(t);
373+
javaType = resolveArrayOrWrapper(javaType);
353374

354-
if (t.isEnumType() || ClassUtils.isPrimitiveOrWrapper(t.getRawClass())) {
375+
if (javaType.isEnumType() || ClassUtils.isPrimitiveOrWrapper(javaType.getRawClass())) {
355376
return false;
356377
}
357378

358-
if (t.isFinal() && !KotlinDetector.isKotlinType(t.getRawClass())
359-
&& t.getRawClass().getPackageName().startsWith("java")) {
379+
if (javaType.isFinal() && !KotlinDetector.isKotlinType(javaType.getRawClass())
380+
&& javaType.getRawClass().getPackageName().startsWith("java")) {
360381
return false;
361382
}
362383

363384
// [databind#88] Should not apply to JSON tree models:
364-
return !TreeNode.class.isAssignableFrom(t.getRawClass());
385+
return !TreeNode.class.isAssignableFrom(javaType.getRawClass());
365386
}
366387

367388
private JavaType resolveArrayOrWrapper(JavaType type) {

0 commit comments

Comments
 (0)