Skip to content

Commit c2a4c1e

Browse files
artembilangaryrussell
authored andcommitted
GH-680: Use enhanced ObjectMapper by default
Fixes #680 * Introduce `JacksonUtils` factory class and use its `enhancedObjectMapper()` whenever we rely on the default `ObjectMapper`. * The `JacksonUtils` instantiate `ObjectMapper` with `MapperFeature.DEFAULT_VIEW_INCLUSION` & `DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`, and also registers well-known data type modules found in classpath.
1 parent 9909bf6 commit c2a4c1e

File tree

9 files changed

+150
-38
lines changed

9 files changed

+150
-38
lines changed

spring-kafka/src/main/java/org/springframework/kafka/support/DefaultKafkaHeaderMapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public class DefaultKafkaHeaderMapper extends AbstractKafkaHeaderMapper {
9494
* @see #DefaultKafkaHeaderMapper(ObjectMapper)
9595
*/
9696
public DefaultKafkaHeaderMapper() {
97-
this(new ObjectMapper());
97+
this(JacksonUtils.enhancedObjectMapper());
9898
}
9999

100100
/**
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.kafka.support;
18+
19+
import org.springframework.beans.BeanUtils;
20+
import org.springframework.util.ClassUtils;
21+
22+
import com.fasterxml.jackson.databind.DeserializationFeature;
23+
import com.fasterxml.jackson.databind.MapperFeature;
24+
import com.fasterxml.jackson.databind.Module;
25+
import com.fasterxml.jackson.databind.ObjectMapper;
26+
27+
/**
28+
* The utilities for Jackson {@link ObjectMapper} instances.
29+
*
30+
* @author Artem Bilan
31+
*
32+
* @since 2.3
33+
*/
34+
public final class JacksonUtils {
35+
36+
private static final String UNUSED = "unused";
37+
38+
/**
39+
* Factory for {@link ObjectMapper} instances with registered well-known modules
40+
* and disabled {@link MapperFeature#DEFAULT_VIEW_INCLUSION} and
41+
* {@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES} features.
42+
* The {@link ClassUtils#getDefaultClassLoader()} is used for loading module classes.
43+
* @return the {@link ObjectMapper} instance.
44+
*/
45+
public static ObjectMapper enhancedObjectMapper() {
46+
return enhancedObjectMapper(ClassUtils.getDefaultClassLoader());
47+
}
48+
49+
/**
50+
* Factory for {@link ObjectMapper} instances with registered well-known modules
51+
* and disabled {@link MapperFeature#DEFAULT_VIEW_INCLUSION} and
52+
* {@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES} features.
53+
* @param classLoader the {@link ClassLoader} for modules to register.
54+
* @return the {@link ObjectMapper} instance.
55+
*/
56+
public static ObjectMapper enhancedObjectMapper(ClassLoader classLoader) {
57+
ObjectMapper objectMapper = new ObjectMapper();
58+
objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
59+
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
60+
registerWellKnownModulesIfAvailable(objectMapper, classLoader);
61+
return objectMapper;
62+
}
63+
64+
@SuppressWarnings("unchecked")
65+
private static void registerWellKnownModulesIfAvailable(ObjectMapper objectMapper, ClassLoader classLoader) {
66+
try {
67+
Class<? extends Module> jdk8Module = (Class<? extends Module>)
68+
ClassUtils.forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", classLoader);
69+
objectMapper.registerModule(BeanUtils.instantiateClass(jdk8Module));
70+
}
71+
catch (@SuppressWarnings(UNUSED) ClassNotFoundException ex) {
72+
// jackson-datatype-jdk8 not available
73+
}
74+
75+
try {
76+
Class<? extends Module> javaTimeModule = (Class<? extends Module>)
77+
ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", classLoader);
78+
objectMapper.registerModule(BeanUtils.instantiateClass(javaTimeModule));
79+
}
80+
catch (@SuppressWarnings(UNUSED) ClassNotFoundException ex) {
81+
// jackson-datatype-jsr310 not available
82+
}
83+
84+
// Joda-Time present?
85+
if (ClassUtils.isPresent("org.joda.time.LocalDate", classLoader)) {
86+
try {
87+
Class<? extends Module> jodaModule = (Class<? extends Module>)
88+
ClassUtils.forName("com.fasterxml.jackson.datatype.joda.JodaModule", classLoader);
89+
objectMapper.registerModule(BeanUtils.instantiateClass(jodaModule));
90+
}
91+
catch (@SuppressWarnings(UNUSED) ClassNotFoundException ex) {
92+
// jackson-datatype-joda not available
93+
}
94+
}
95+
96+
// Kotlin present?
97+
if (ClassUtils.isPresent("kotlin.Unit", classLoader)) {
98+
try {
99+
Class<? extends Module> kotlinModule = (Class<? extends Module>)
100+
ClassUtils.forName("com.fasterxml.jackson.module.kotlin.KotlinModule", classLoader);
101+
objectMapper.registerModule(BeanUtils.instantiateClass(kotlinModule));
102+
}
103+
catch (@SuppressWarnings(UNUSED) ClassNotFoundException ex) {
104+
//jackson-module-kotlin not available
105+
}
106+
}
107+
}
108+
109+
private JacksonUtils() {
110+
}
111+
112+
}

spring-kafka/src/main/java/org/springframework/kafka/support/converter/ProjectingMessageConverter.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.data.projection.ProjectionFactory;
2929
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
3030
import org.springframework.data.web.JsonProjectingMethodInterceptorFactory;
31+
import org.springframework.kafka.support.JacksonUtils;
3132
import org.springframework.kafka.support.KafkaNull;
3233
import org.springframework.messaging.Message;
3334
import org.springframework.util.Assert;
@@ -40,6 +41,7 @@
4041
* {@link ProjectionFactory} to bind incoming messages to projection interfaces.
4142
*
4243
* @author Oliver Gierke
44+
* @author Artem Bilan
4345
*
4446
* @since 2.1.1
4547
*/
@@ -49,6 +51,15 @@ public class ProjectingMessageConverter extends MessagingMessageConverter {
4951

5052
private final MessagingMessageConverter delegate;
5153

54+
/**
55+
* Creates a new {@link ProjectingMessageConverter} using a
56+
* {@link JacksonUtils#enhancedObjectMapper()} by default.
57+
* @since 2.3
58+
*/
59+
public ProjectingMessageConverter() {
60+
this(JacksonUtils.enhancedObjectMapper());
61+
}
62+
5263
/**
5364
* Creates a new {@link ProjectingMessageConverter} using the given {@link ObjectMapper}.
5465
* @param mapper must not be {@literal null}.
@@ -100,11 +111,11 @@ private static byte[] getAsByteArray(Object source) {
100111
Assert.notNull(source, "Source must not be null");
101112

102113
if (source instanceof String) {
103-
return String.class.cast(source).getBytes(StandardCharsets.UTF_8);
114+
return ((String) source).getBytes(StandardCharsets.UTF_8);
104115
}
105116

106117
if (source instanceof byte[]) {
107-
return byte[].class.cast(source);
118+
return (byte[]) source;
108119
}
109120

110121
throw new ConversionException(String.format("Unsupported payload type '%s'. Expected 'String' or 'byte[]'",

spring-kafka/src/main/java/org/springframework/kafka/support/converter/StringJsonMessageConverter.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,14 @@
2424
import org.apache.kafka.common.header.internals.RecordHeaders;
2525
import org.apache.kafka.common.utils.Bytes;
2626

27+
import org.springframework.kafka.support.JacksonUtils;
2728
import org.springframework.kafka.support.KafkaNull;
2829
import org.springframework.kafka.support.converter.Jackson2JavaTypeMapper.TypePrecedence;
2930
import org.springframework.messaging.Message;
3031
import org.springframework.util.Assert;
3132

3233
import com.fasterxml.jackson.core.JsonProcessingException;
33-
import com.fasterxml.jackson.databind.DeserializationFeature;
3434
import com.fasterxml.jackson.databind.JavaType;
35-
import com.fasterxml.jackson.databind.MapperFeature;
3635
import com.fasterxml.jackson.databind.ObjectMapper;
3736
import com.fasterxml.jackson.databind.type.TypeFactory;
3837

@@ -53,9 +52,7 @@ public class StringJsonMessageConverter extends MessagingMessageConverter {
5352
private Jackson2JavaTypeMapper typeMapper = new DefaultJackson2JavaTypeMapper();
5453

5554
public StringJsonMessageConverter() {
56-
this(new ObjectMapper());
57-
this.objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
58-
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
55+
this(JacksonUtils.enhancedObjectMapper());
5956
}
6057

6158
public StringJsonMessageConverter(ObjectMapper objectMapper) {
@@ -112,8 +109,8 @@ protected Object extractAndConvertValue(ConsumerRecord<?, ?> record, Type type)
112109
}
113110

114111
JavaType javaType = this.typeMapper.getTypePrecedence().equals(TypePrecedence.INFERRED)
115-
? TypeFactory.defaultInstance().constructType(type)
116-
: this.typeMapper.toJavaType(record.headers());
112+
? TypeFactory.defaultInstance().constructType(type)
113+
: this.typeMapper.toJavaType(record.headers());
117114
if (javaType == null) { // no headers
118115
javaType = TypeFactory.defaultInstance().constructType(type);
119116
}

spring-kafka/src/main/java/org/springframework/kafka/support/serializer/JsonDeserializer.java

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@
1919
import java.io.IOException;
2020
import java.util.Arrays;
2121
import java.util.Map;
22-
import java.util.function.Consumer;
2322

2423
import org.apache.kafka.common.errors.SerializationException;
2524
import org.apache.kafka.common.header.Headers;
2625
import org.apache.kafka.common.serialization.Deserializer;
2726

2827
import org.springframework.core.ResolvableType;
28+
import org.springframework.kafka.support.JacksonUtils;
2929
import org.springframework.kafka.support.converter.AbstractJavaTypeMapper;
3030
import org.springframework.kafka.support.converter.DefaultJackson2JavaTypeMapper;
3131
import org.springframework.kafka.support.converter.Jackson2JavaTypeMapper;
@@ -35,9 +35,7 @@
3535
import org.springframework.util.ClassUtils;
3636
import org.springframework.util.StringUtils;
3737

38-
import com.fasterxml.jackson.databind.DeserializationFeature;
3938
import com.fasterxml.jackson.databind.JavaType;
40-
import com.fasterxml.jackson.databind.MapperFeature;
4139
import com.fasterxml.jackson.databind.ObjectMapper;
4240
import com.fasterxml.jackson.databind.ObjectReader;
4341

@@ -152,10 +150,7 @@ public JsonDeserializer(Class<? super T> targetType) {
152150
* @since 2.2
153151
*/
154152
public JsonDeserializer(Class<? super T> targetType, boolean useHeadersIfPresent) {
155-
this(targetType, new ObjectMapper(), useHeadersIfPresent, om -> {
156-
om.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
157-
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
158-
});
153+
this(targetType, JacksonUtils.enhancedObjectMapper(), useHeadersIfPresent);
159154
}
160155

161156
/**
@@ -176,13 +171,9 @@ public JsonDeserializer(Class<? super T> targetType, ObjectMapper objectMapper)
176171
* type if not.
177172
* @since 2.2
178173
*/
179-
public JsonDeserializer(@Nullable Class<? super T> targetType, ObjectMapper objectMapper, boolean useHeadersIfPresent) {
180-
this(targetType, objectMapper, useHeadersIfPresent, om -> { });
181-
}
182-
183174
@SuppressWarnings("unchecked")
184-
private JsonDeserializer(@Nullable Class<? super T> targetType, ObjectMapper objectMapper, boolean useHeadersIfPresent,
185-
Consumer<ObjectMapper> configurer) {
175+
public JsonDeserializer(@Nullable Class<? super T> targetType, ObjectMapper objectMapper,
176+
boolean useHeadersIfPresent) {
186177

187178
Assert.notNull(objectMapper, "'objectMapper' must not be null.");
188179
this.objectMapper = objectMapper;
@@ -193,7 +184,6 @@ private JsonDeserializer(@Nullable Class<? super T> targetType, ObjectMapper obj
193184
Assert.isTrue(this.targetType != null || useHeadersIfPresent,
194185
"'targetType' cannot be null if 'useHeadersIfPresent' is false");
195186

196-
configurer.accept(this.objectMapper);
197187
if (this.targetType != null) {
198188
this.reader = this.objectMapper.readerFor(this.targetType);
199189
}

spring-kafka/src/main/java/org/springframework/kafka/support/serializer/JsonSerde.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,10 @@
2323
import org.apache.kafka.common.serialization.Serializer;
2424

2525
import org.springframework.core.ResolvableType;
26+
import org.springframework.kafka.support.JacksonUtils;
2627
import org.springframework.lang.Nullable;
2728
import org.springframework.util.Assert;
2829

29-
import com.fasterxml.jackson.databind.DeserializationFeature;
30-
import com.fasterxml.jackson.databind.MapperFeature;
3130
import com.fasterxml.jackson.databind.ObjectMapper;
3231

3332
/**
@@ -69,9 +68,7 @@ public JsonSerde(@Nullable Class<? super T> targetTypeArg, @Nullable ObjectMappe
6968
ObjectMapper objectMapper = objectMapperArg;
7069
Class<T> targetType = (Class<T>) targetTypeArg;
7170
if (objectMapper == null) {
72-
objectMapper = new ObjectMapper();
73-
objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
74-
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
71+
objectMapper = JacksonUtils.enhancedObjectMapper();
7572
}
7673
this.jsonSerializer = new JsonSerializer<>(objectMapper);
7774
if (targetType == null) {

spring-kafka/src/main/java/org/springframework/kafka/support/serializer/JsonSerializer.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.kafka.common.header.Headers;
2525
import org.apache.kafka.common.serialization.Serializer;
2626

27+
import org.springframework.kafka.support.JacksonUtils;
2728
import org.springframework.kafka.support.converter.AbstractJavaTypeMapper;
2829
import org.springframework.kafka.support.converter.DefaultJackson2JavaTypeMapper;
2930
import org.springframework.kafka.support.converter.Jackson2JavaTypeMapper;
@@ -32,8 +33,6 @@
3233
import org.springframework.util.ClassUtils;
3334
import org.springframework.util.StringUtils;
3435

35-
import com.fasterxml.jackson.databind.DeserializationFeature;
36-
import com.fasterxml.jackson.databind.MapperFeature;
3736
import com.fasterxml.jackson.databind.ObjectMapper;
3837

3938
/**
@@ -69,9 +68,7 @@ public class JsonSerializer<T> implements Serializer<T> {
6968
private boolean typeMapperExplicitlySet = false;
7069

7170
public JsonSerializer() {
72-
this(new ObjectMapper());
73-
this.objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
74-
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
71+
this(JacksonUtils.enhancedObjectMapper());
7572
}
7673

7774
public JsonSerializer(ObjectMapper objectMapper) {

src/reference/asciidoc/kafka.adoc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2299,8 +2299,11 @@ JsonDeserializer<Thing> thingDeserializer = new JsonDeserializer<>(Thing.class);
22992299
====
23002300

23012301
You can customize both `JsonSerializer` and `JsonDeserializer` with an `ObjectMapper`.
2302-
You can also extend them to implement some particular configuration logic in the
2303-
`configure(Map<String, ?> configs, boolean isKey)` method.
2302+
You can also extend them to implement some particular configuration logic in the `configure(Map<String, ?> configs, boolean isKey)` method.
2303+
2304+
Starting with version 2.3, all the JSON-aware components are configured by default with a `JacksonUtils.enhancedObjectMapper()` instance, which comes with the `MapperFeature.DEFAULT_VIEW_INCLUSION` and `DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES` features disabled.
2305+
Also such an instance is supplied with well-known modules for custom data types, such a Java time and Kotlin support.
2306+
See `JacksonUtils.enhancedObjectMapper()` JavaDocs for more information.
23042307

23052308
Starting with version 2.1, you can convey type information in record `Headers`, allowing the handling of multiple types.
23062309
In addition, you can configure the serializer and deserializer by using the following Kafka properties:

src/reference/asciidoc/whats-new.adoc

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
This section covers the changes made from version 2.2 to version 2.3.
44

5-
[[kafka-client-2.1]]
5+
[[kafka-client-2.2]]
66
==== Kafka Client Version
77

88
This version requires the 2.2.0 `kafka-clients` or higher.
99

1010
==== Listener Container Changes
1111

12-
Previously, error handlers received `ListenerExectionFailedException` (with the actual listener exception as the `cause`) when the listener was invoked using a listener adapter (such as `@KafkaListener` s).
12+
Previously, error handlers received `ListenerExecutionFailedException` (with the actual listener exception as the `cause`) when the listener was invoked using a listener adapter (such as `@KafkaListener` s).
1313
Exceptions thrown by native `GenericMessageListener` s were passed to the error handler unchanged.
1414
Now a `ListenerExecutionFailedException` is always the argument (with the actual listener exception as the `cause`), which provides access to the container's `group.id` property.
1515

@@ -40,3 +40,8 @@ See <<configuring-topics>> for more information.
4040

4141
You can now perform additional configuration of the `StreamsBuilderFactoryBean` created by `@EnableKafkaStreams`.
4242
See <<streams-config, Streams Configuration>> for more information.
43+
44+
==== JSON Components Changes
45+
46+
Now all the JSON-aware components are configured by default with a Jackson `ObjectMapper` produced by the `JacksonUtils.enhancedObjectMapper()`.
47+
See its JavaDocs and <<serdes>> for more information.

0 commit comments

Comments
 (0)