Skip to content

Commit 7d79406

Browse files
artembilangaryrussell
authored andcommitted
GH-1129: Add JacksonMimeTypeModule (#1130)
* GH-1129: Add JacksonMimeTypeModule Fixes #1129 Introduce a `JacksonMimeTypeModule` with a simple `MimeTypeSerializer` for the proper inter-platform `MimeType` objects serialization. Essentially we call its `toString()` which is enough to carry mime-type info over the network. Register this module in the `JacksonUtils.enhancedObjectMapper()` which is used from the `DefaultKafkaHeaderMapper`. Such a module can be registered as a bean in the application context and will be automatically discovered by Spring Boot auto-configuration for Jackson * Modify a `DefaultKafkaHeaderMapper` to not deal with `MimeType` directly any more since it is covered by the `JacksonMimeTypeModule` even if `MimeType` is a part of collection like it is in case of mapped HTTP headers. * Modify `DefaultKafkaHeaderMapperTests` to check that `MimeType` is properly serialized into its string representation even if it in the collection * * Add package protected ctor into `MimeTypeSerializer` * Document a `JacksonMimeTypeModule` * * Fix unused imports * * Parse a `MimeType` header from its `TextNode` in the `MimeTypeJsonDeserializer`
1 parent 4ac551d commit 7d79406

File tree

6 files changed

+98
-38
lines changed

6 files changed

+98
-38
lines changed

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

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import com.fasterxml.jackson.databind.ObjectMapper;
4343
import com.fasterxml.jackson.databind.deser.std.StdNodeBasedDeserializer;
4444
import com.fasterxml.jackson.databind.module.SimpleModule;
45+
import com.fasterxml.jackson.databind.node.TextNode;
4546
import com.fasterxml.jackson.databind.type.TypeFactory;
4647

4748
/**
@@ -66,12 +67,6 @@ public class DefaultKafkaHeaderMapper extends AbstractKafkaHeaderMapper {
6667
"org.springframework.util"
6768
);
6869

69-
private static final List<String> DEFAULT_TO_STRING_CLASSES =
70-
Arrays.asList(
71-
"org.springframework.util.MimeType",
72-
"org.springframework.http.MediaType"
73-
);
74-
7570
/**
7671
* Header name for java types of other headers.
7772
*/
@@ -81,7 +76,7 @@ public class DefaultKafkaHeaderMapper extends AbstractKafkaHeaderMapper {
8176

8277
private final Set<String> trustedPackages = new LinkedHashSet<>(DEFAULT_TRUSTED_PACKAGES);
8378

84-
private final Set<String> toStringClasses = new LinkedHashSet<>(DEFAULT_TO_STRING_CLASSES);
79+
private final Set<String> toStringClasses = new LinkedHashSet<>();
8580

8681
/**
8782
* Construct an instance with the default object mapper and default header patterns
@@ -269,8 +264,8 @@ public void toHeaders(Headers source, final Map<String, Object> headers) {
269264
}
270265
catch (IOException e) {
271266
logger.error(e, () ->
272-
"Could not decode json type: " + new String(header.value()) + " for key: "
273-
+ header.key());
267+
"Could not decode json type: " + new String(header.value()) + " for key: "
268+
+ header.key());
274269
headers.put(header.key(), header.value());
275270
}
276271
}
@@ -361,14 +356,19 @@ private class MimeTypeJsonDeserializer extends StdNodeBasedDeserializer<MimeType
361356

362357
@Override
363358
public MimeType convert(JsonNode root, DeserializationContext ctxt) throws IOException {
364-
JsonNode type = root.get("type");
365-
JsonNode subType = root.get("subtype");
366-
JsonNode parameters = root.get("parameters");
367-
Map<String, String> params =
368-
DefaultKafkaHeaderMapper.this.objectMapper.readValue(parameters.traverse(),
369-
TypeFactory.defaultInstance()
370-
.constructMapType(HashMap.class, String.class, String.class));
371-
return new MimeType(type.asText(), subType.asText(), params);
359+
if (root instanceof TextNode) {
360+
return MimeType.valueOf(root.asText());
361+
}
362+
else {
363+
JsonNode type = root.get("type");
364+
JsonNode subType = root.get("subtype");
365+
JsonNode parameters = root.get("parameters");
366+
Map<String, String> params =
367+
DefaultKafkaHeaderMapper.this.objectMapper.readValue(parameters.traverse(),
368+
TypeFactory.defaultInstance()
369+
.constructMapType(HashMap.class, String.class, String.class));
370+
return new MimeType(type.asText(), subType.asText(), params);
371+
}
372372
}
373373

374374
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 java.io.IOException;
20+
21+
import org.springframework.util.MimeType;
22+
23+
import com.fasterxml.jackson.core.JsonGenerator;
24+
import com.fasterxml.jackson.databind.JsonSerializer;
25+
import com.fasterxml.jackson.databind.SerializerProvider;
26+
import com.fasterxml.jackson.databind.module.SimpleModule;
27+
28+
/**
29+
* A {@link SimpleModule} extension for {@link MimeType} serialization.
30+
*
31+
* @author Artem Bilan
32+
*
33+
* @since 2.3
34+
*/
35+
public final class JacksonMimeTypeModule extends SimpleModule {
36+
37+
private static final long serialVersionUID = 1L;
38+
39+
public JacksonMimeTypeModule() {
40+
addSerializer(MimeType.class, new MimeTypeSerializer());
41+
}
42+
43+
/**
44+
* Simple {@link JsonSerializer} extension to represent a {@link MimeType} object in the
45+
* target JSON as a plain string.
46+
*/
47+
private static final class MimeTypeSerializer extends JsonSerializer<MimeType> {
48+
49+
MimeTypeSerializer() {
50+
super();
51+
}
52+
53+
@Override
54+
public void serialize(MimeType value, JsonGenerator generator, SerializerProvider serializers)
55+
throws IOException {
56+
57+
generator.writeString(value.toString());
58+
}
59+
60+
}
61+
62+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public static ObjectMapper enhancedObjectMapper(ClassLoader classLoader) {
6363

6464
@SuppressWarnings("unchecked")
6565
private static void registerWellKnownModulesIfAvailable(ObjectMapper objectMapper, ClassLoader classLoader) {
66+
objectMapper.registerModule(new JacksonMimeTypeModule());
6667
try {
6768
Class<? extends Module> jdk8Module = (Class<? extends Module>)
6869
ClassUtils.forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", classLoader);

spring-kafka/src/test/java/org/springframework/kafka/support/DefaultKafkaHeaderMapperTests.java

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,19 @@
1919
import static org.assertj.core.api.Assertions.assertThat;
2020
import static org.assertj.core.api.Assertions.entry;
2121

22-
import java.nio.charset.Charset;
22+
import java.nio.charset.StandardCharsets;
23+
import java.util.Arrays;
2324
import java.util.Collections;
2425
import java.util.HashMap;
26+
import java.util.List;
2527
import java.util.Map;
26-
import java.util.Set;
2728

2829
import org.apache.kafka.common.header.Headers;
2930
import org.apache.kafka.common.header.internals.RecordHeader;
3031
import org.apache.kafka.common.header.internals.RecordHeaders;
3132
import org.junit.Test;
3233

3334
import org.springframework.kafka.support.DefaultKafkaHeaderMapper.NonTrustedHeaderType;
34-
import org.springframework.kafka.test.utils.KafkaTestUtils;
3535
import org.springframework.messaging.Message;
3636
import org.springframework.messaging.MessageHeaders;
3737
import org.springframework.messaging.support.ExecutorSubscribableChannel;
@@ -42,6 +42,8 @@
4242

4343
/**
4444
* @author Gary Russell
45+
* @author Artem Bilan
46+
*
4547
* @since 1.3
4648
*
4749
*/
@@ -51,7 +53,7 @@ public class DefaultKafkaHeaderMapperTests {
5153
public void testTrustedAndNot() {
5254
DefaultKafkaHeaderMapper mapper = new DefaultKafkaHeaderMapper();
5355
mapper.addToStringClasses(Bar.class.getName());
54-
MimeType utf8Text = new MimeType(MimeTypeUtils.TEXT_PLAIN, Charset.forName("UTF-8"));
56+
MimeType utf8Text = new MimeType(MimeTypeUtils.TEXT_PLAIN, StandardCharsets.UTF_8);
5557
Message<String> message = MessageBuilder.withPayload("foo")
5658
.setHeader("foo", "bar".getBytes())
5759
.setHeader("baz", "qux")
@@ -60,7 +62,7 @@ public void testTrustedAndNot() {
6062
.setHeader(MessageHeaders.REPLY_CHANNEL, new ExecutorSubscribableChannel())
6163
.setHeader(MessageHeaders.ERROR_CHANNEL, "errors")
6264
.setHeader(MessageHeaders.CONTENT_TYPE, utf8Text)
63-
.setHeader("simpleContentType", MimeTypeUtils.TEXT_PLAIN)
65+
.setHeader("simpleContentType", MimeTypeUtils.TEXT_PLAIN_VALUE)
6466
.setHeader("customToString", new Bar("fiz"))
6567
.build();
6668
RecordHeaders recordHeaders = new RecordHeaders();
@@ -73,8 +75,8 @@ public void testTrustedAndNot() {
7375
assertThat(headers.get("baz")).isEqualTo("qux");
7476
assertThat(headers.get("fix")).isInstanceOf(NonTrustedHeaderType.class);
7577
assertThat(headers.get("linkedMVMap")).isInstanceOf(LinkedMultiValueMap.class);
76-
assertThat(headers.get(MessageHeaders.CONTENT_TYPE)).isEqualTo(utf8Text.toString());
77-
assertThat(headers.get("simpleContentType")).isEqualTo(MimeTypeUtils.TEXT_PLAIN.toString());
78+
assertThat(headers.get(MessageHeaders.CONTENT_TYPE)).isEqualTo(utf8Text);
79+
assertThat(headers.get("simpleContentType")).isEqualTo(MimeTypeUtils.TEXT_PLAIN_VALUE);
7880
assertThat(headers.get(MessageHeaders.REPLY_CHANNEL)).isNull();
7981
assertThat(headers.get(MessageHeaders.ERROR_CHANNEL)).isEqualTo("errors");
8082
assertThat(headers.get("customToString")).isEqualTo("Bar [field=fiz]");
@@ -94,7 +96,7 @@ public void testTrustedAndNot() {
9496
}
9597

9698
@Test
97-
public void testReserializedNonTrusted() {
99+
public void testDeserializedNonTrusted() {
98100
DefaultKafkaHeaderMapper mapper = new DefaultKafkaHeaderMapper();
99101
Message<String> message = MessageBuilder.withPayload("foo")
100102
.setHeader("fix", new Foo())
@@ -126,27 +128,19 @@ public void testReserializedNonTrusted() {
126128
}
127129

128130
@Test
129-
public void testMimeBackwardsCompat() {
131+
public void testMimeTypeInHeaders() {
130132
DefaultKafkaHeaderMapper mapper = new DefaultKafkaHeaderMapper();
131133
MessageHeaders headers = new MessageHeaders(
132-
Collections.singletonMap("foo", MimeType.valueOf("application/json")));
134+
Collections.singletonMap("foo",
135+
Arrays.asList(MimeType.valueOf("application/json"), MimeType.valueOf("text/plain"))));
133136

134137
RecordHeaders recordHeaders = new RecordHeaders();
135138
mapper.fromHeaders(headers, recordHeaders);
136139
Map<String, Object> receivedHeaders = new HashMap<>();
137140
mapper.toHeaders(recordHeaders, receivedHeaders);
138141
Object fooHeader = receivedHeaders.get("foo");
139-
assertThat(fooHeader).isInstanceOf(String.class);
140-
assertThat(fooHeader).isEqualTo("application/json");
141-
142-
KafkaTestUtils.getPropertyValue(mapper, "toStringClasses", Set.class).clear();
143-
recordHeaders = new RecordHeaders();
144-
mapper.fromHeaders(headers, recordHeaders);
145-
receivedHeaders = new HashMap<>();
146-
mapper.toHeaders(recordHeaders, receivedHeaders);
147-
fooHeader = receivedHeaders.get("foo");
148-
assertThat(fooHeader).isInstanceOf(MimeType.class);
149-
assertThat(fooHeader).isEqualTo(MimeType.valueOf("application/json"));
142+
assertThat(fooHeader).isInstanceOf(List.class);
143+
assertThat(fooHeader).asList().containsExactly("application/json", "text/plain");
150144
}
151145

152146
@Test

src/reference/asciidoc/kafka.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2450,6 +2450,8 @@ You can also extend them to implement some particular configuration logic in the
24502450
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.
24512451
Also such an instance is supplied with well-known modules for custom data types, such a Java time and Kotlin support.
24522452
See `JacksonUtils.enhancedObjectMapper()` JavaDocs for more information.
2453+
This method also registers a `org.springframework.kafka.support.JacksonMimeTypeModule` for `org.springframework.util.MimeType` objects serialization into the plain string for inter-platform compatibility over the network.
2454+
A `JacksonMimeTypeModule` can be registered as a bean in the application context and it will be auto-configured into https://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-mvc.html#howto-customize-the-jackson-objectmapper[Spring Boot `ObjectMapper` instance].
24532455

24542456
Also starting with version 2.3, the `JsonDeserializer` provides `TypeReference`-based constructors for better handling of target generic container types.
24552457

src/reference/asciidoc/whats-new.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ See <<streams-messaging>> and <<streams-integration>> for more information.
7171

7272
Now all the JSON-aware components are configured by default with a Jackson `ObjectMapper` produced by the `JacksonUtils.enhancedObjectMapper()`.
7373
The `JsonDeserializer` now provides `TypeReference`-based constructors for better handling of target generic container types.
74+
Also a `JacksonMimeTypeModule` has been introduced for serialization of `org.springframework.util.MimeType` to plain string.
7475
See its JavaDocs and <<serdes>> for more information.
7576

7677
A `ByteArrayJsonMessageConverter` has been provided as well as a new super class for all Json converters, `JsonMessageConverter`.

0 commit comments

Comments
 (0)