Skip to content

Commit 1dc4575

Browse files
committed
GH-3930: Add Jackson 3 support; deprecate Jackson 2
Fixes #3930 Issue link: #3930 This commit introduces comprehensive Jackson 3 support across the Spring for Apache Kafka framework while maintaining backward compatibility by deprecating Jackson 2 components. BREAKING CHANGES: - All Jackson 2-based classes are now deprecated and will be removed in a future version - Default Jackson version updated from 2.19.2 to 2.19.1 for Jackson 2 - Added Jackson 3 BOM dependency (3.0.0-rc5) NEW FEATURES: - Added complete Jackson 3 counterparts for all existing Jackson 2 classes: * JsonKafkaHeaderMapper (replaces DefaultKafkaHeaderMapper) * JacksonJsonSerializer/Deserializer (replaces JsonSerializer/Deserializer) * JacksonJsonSerde (replaces JsonSerde) * JacksonJsonMessageConverter and subclasses (replaces JsonMessageConverter family) * JacksonProjectingMessageConverter (replaces ProjectingMessageConverter) * DefaultJacksonJavaTypeMapper (replaces DefaultJackson2JavaTypeMapper) * Jackson3Utils and Jackson3MimeTypeModule utilities INFRASTRUCTURE: - Enhanced JacksonPresent utility to detect Jackson 3 availability - Updated MessagingMessageConverter and BatchMessagingMessageConverter to prefer Jackson 3 - Modified RecordMessagingMessageListenerAdapter to handle new converter types - Updated all serialization/deserialization logic to use Jackson 3 APIs DOCUMENTATION: - Updated all documentation references from Jackson 2 to Jackson 3 classes - Corrected sample code and configuration examples - Updated method signatures and class references throughout documentation TESTING: - Migrated all test cases to use Jackson 3 equivalents - Updated test configurations and mock setups - Maintained test coverage for both Jackson 2 (deprecated) and Jackson 3 paths The framework now automatically detects and prefers Jackson 3 when available, falling back to Jackson 2 for backward compatibility. All existing applications will continue to work with Jackson 2, but new development should migrate to Jackson 3 classes. Signed-off-by: Soby Chacko <[email protected]>
1 parent 122d8ce commit 1dc4575

File tree

65 files changed

+3304
-212
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+3304
-212
lines changed

build.gradle

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ ext {
5555
awaitilityVersion = '4.3.0'
5656
hamcrestVersion = '3.0'
5757
hibernateValidationVersion = '9.0.1.Final'
58-
jacksonBomVersion = '2.19.2'
58+
jacksonBomVersion = '2.19.1'
59+
jackson3Version = '3.0.0-rc5'
5960
jaywayJsonPathVersion = '2.9.0'
6061
junit4Version = '4.13.2'
6162
junitJupiterVersion = '5.13.4'
@@ -110,6 +111,7 @@ allprojects {
110111

111112
imports {
112113
mavenBom "com.fasterxml.jackson:jackson-bom:$jacksonBomVersion"
114+
mavenBom "tools.jackson:jackson-bom:$jackson3Version"
113115
mavenBom "org.junit:junit-bom:$junitJupiterVersion"
114116
mavenBom "io.micrometer:micrometer-bom:$micrometerVersion"
115117
mavenBom "io.micrometer:micrometer-tracing-bom:$micrometerTracingVersion"
@@ -263,6 +265,12 @@ project ('spring-kafka') {
263265
exclude group: 'org.jetbrains.kotlin'
264266
}
265267

268+
optionalApi 'tools.jackson.core:jackson-databind'
269+
optionalApi 'tools.jackson.datatype:jackson-datatype-joda'
270+
optionalApi('tools.jackson.module:jackson-module-kotlin') {
271+
exclude group: 'org.jetbrains.kotlin'
272+
}
273+
266274
// Spring Data projection message binding support
267275
optionalApi ('org.springframework.data:spring-data-commons') {
268276
exclude group: 'org.springframework'

samples/sample-01/README.adoc

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

33
This sample demonstrates a simple producer and consumer; the producer sends objects of type `Foo1` and the consumer receives objects of type `Foo2` (the objects have the same field, `foo`).
44

5-
The producer uses a `JsonSerializer`; the consumer uses the `ByteArrayDeserializer`, together with a `JsonMessageConverter` which converts to the type of the listener method argument.
5+
The producer uses a `JacksonJsonSerializer`; the consumer uses the `ByteArrayDeserializer`, together with a `JacksonJsonMessageConverter` which converts to the type of the listener method argument.
66

77
Run the application and use curl to send some data:
88

@@ -31,4 +31,4 @@ Console:
3131
...
3232
2018-11-05 10:12:33.537 INFO 41635 --- [ fooGroup-0-C-1] com.example.Application : Received: Foo2 [foo=fail]
3333
2018-11-05 10:12:43.359 INFO 41635 --- [ dltGroup-0-C-1] com.example.Application : Received from DLT: {"foo":"fail"}
34-
----
34+
----

samples/sample-02/README.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This sample demonstrates a simple producer and a multi-method consumer; the producer sends objects of types `Foo1` and `Bar1` and the consumer receives objects of type `Foo2` and `Bar2` (the objects have the same field, `foo`).
44

5-
The producer uses a `JsonSerializer`; the consumer uses a `ByteArrayDeserializer`, together with a `ByteArrayJsonMessageConverter` which converts to the required type of the listener method argument.
5+
The producer uses a `JacksonJsonSerializer`; the consumer uses a `ByteArrayJacksonDeserializer`, together with a `ByteArrayJacksonJsonMessageConverter` which converts to the required type of the listener method argument.
66
We can't infer the type in this case (because the type is used to choose the method to call).
77
We therefore configure type mapping on the producer and consumer side.
88
See the `application.yml` for the producer side and the `converter` bean on the consumer side.

spring-kafka-docs/src/main/antora/modules/ROOT/pages/appendix/change-history.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1083,7 +1083,7 @@ Previously, they were mapped as JSON and only `MimeType` was decoded.
10831083
`MediaType` could not be decoded.
10841084
They are now simple strings for interoperability.
10851085

1086-
Also, the `DefaultKafkaHeaderMapper` has a new `addToStringClasses` method, allowing the specification of types that should be mapped by using `toString()` instead of JSON.
1086+
Also, the `JsonKafkaHeaderMapper` has a new `addToStringClasses` method, allowing the specification of types that should be mapped by using `toString()` instead of JSON.
10871087
See xref:kafka/headers.adoc[Message Headers] for more information.
10881088

10891089
[[cb-2-1-and-2-2-embedded-kafka-changes]]

spring-kafka-docs/src/main/antora/modules/ROOT/pages/kafka/annotation-error-handling.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ int delivery = ByteBuffer.wrap(record.headers()
563563
.getInt();
564564
----
565565

566-
When using `@KafkaListener` with the `DefaultKafkaHeaderMapper` or `SimpleKafkaHeaderMapper`, it can be obtained by adding `@Header(KafkaHeaders.DELIVERY_ATTEMPT) int delivery` as a parameter to the listener method.
566+
When using `@KafkaListener` with the `JsonKafkaHeaderMapper` or `SimpleKafkaHeaderMapper`, it can be obtained by adding `@Header(KafkaHeaders.DELIVERY_ATTEMPT) int delivery` as a parameter to the listener method.
567567

568568
To enable population of this header, set the container property `deliveryAttemptHeader` to `true`.
569569
It is disabled by default to avoid the (small) overhead of looking up the state for each record and adding the header.

spring-kafka-docs/src/main/antora/modules/ROOT/pages/kafka/headers.adoc

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public interface KafkaHeaderMapper {
3636

3737
The `SimpleKafkaHeaderMapper` maps raw headers as `byte[]`, with configuration options for conversion to `String` values.
3838

39-
The `DefaultKafkaHeaderMapper` maps the key to the `MessageHeaders` header name and, in order to support rich header types for outbound messages, JSON conversion is performed.
39+
The `JsonKafkaHeaderMapper` maps the key to the `MessageHeaders` header name and, in order to support rich header types for outbound messages, JSON conversion is performed.
4040
A +++"+++`special`+++"+++ header (with a key of `spring_json_header_types`) contains a JSON map of `<key>:<type>`.
4141
This header is used on the inbound side to provide appropriate conversion of each header value to the original type.
4242

@@ -48,19 +48,19 @@ The following listing shows a number of example mappings:
4848

4949
[source, java]
5050
----
51-
public DefaultKafkaHeaderMapper() { <1>
51+
public JsonKafkaHeaderMapper() { <1>
5252
...
5353
}
5454
55-
public DefaultKafkaHeaderMapper(ObjectMapper objectMapper) { <2>
55+
public JsonKafkaHeaderMapper(ObjectMapper objectMapper) { <2>
5656
...
5757
}
5858
59-
public DefaultKafkaHeaderMapper(String... patterns) { <3>
59+
public JsonKafkaHeaderMapper(String... patterns) { <3>
6060
...
6161
}
6262
63-
public DefaultKafkaHeaderMapper(ObjectMapper objectMapper, String... patterns) { <4>
63+
public JsonKafkaHeaderMapper(ObjectMapper objectMapper, String... patterns) { <4>
6464
...
6565
}
6666
----
@@ -95,7 +95,7 @@ The following test case illustrates this mechanism.
9595
----
9696
@Test
9797
public void testSpecificStringConvert() {
98-
DefaultKafkaHeaderMapper mapper = new DefaultKafkaHeaderMapper();
98+
JsonKafkaHeaderMapper mapper = new JsonKafkaHeaderMapper();
9999
Map<String, Boolean> rawMappedHeaders = new HashMap<>();
100100
rawMappedHeaders.put("thisOnesAString", true);
101101
rawMappedHeaders.put("thisOnesBytes", false);
@@ -126,10 +126,10 @@ To create a mapper for inbound mapping, use one of the static methods on the res
126126

127127
[source, java]
128128
----
129-
public static DefaultKafkaHeaderMapper forInboundOnlyWithMatchers(String... patterns) {
129+
public static JsonKafkaHeaderMapper forInboundOnlyWithMatchers(String... patterns) {
130130
}
131131
132-
public static DefaultKafkaHeaderMapper forInboundOnlyWithMatchers(ObjectMapper objectMapper, String... patterns) {
132+
public static JsonKafkaHeaderMapper forInboundOnlyWithMatchers(ObjectMapper objectMapper, String... patterns) {
133133
}
134134
135135
public static SimpleKafkaHeaderMapper forInboundOnlyWithMatchers(String... patterns) {
@@ -140,20 +140,20 @@ For example:
140140

141141
[source, java]
142142
----
143-
DefaultKafkaHeaderMapper inboundMapper = DefaultKafkaHeaderMapper.forInboundOnlyWithMatchers("!abc*", "*");
143+
JsonKafkaHeaderMapper inboundMapper = JsonKafkaHeaderMapper.forInboundOnlyWithMatchers("!abc*", "*");
144144
----
145145

146146
This will exclude all headers beginning with `abc` and include all others.
147147

148-
By default, the `DefaultKafkaHeaderMapper` is used in the `MessagingMessageConverter` and `BatchMessagingMessageConverter`, as long as Jackson is on the classpath.
148+
By default, the `JsonKafkaHeaderMapper` is used in the `MessagingMessageConverter` and `BatchMessagingMessageConverter`, as long as Jackson is on the classpath.
149149

150150
With the batch converter, the converted headers are available in the `KafkaHeaders.BATCH_CONVERTED_HEADERS` as a `List<Map<String, Object>>` where the map in a position of the list corresponds to the data position in the payload.
151151

152152
If there is no converter (either because Jackson is not present or it is explicitly set to `null`), the headers from the consumer record are provided unconverted in the `KafkaHeaders.NATIVE_HEADERS` header.
153153
This header is a `Headers` object (or a `List<Headers>` in the case of the batch converter), where the position in the list corresponds to the data position in the payload.
154154

155155
IMPORTANT: Certain types are not suitable for JSON serialization, and a simple `toString()` serialization might be preferred for these types.
156-
The `DefaultKafkaHeaderMapper` has a method called `addToStringClasses()` that lets you supply the names of classes that should be treated this way for outbound mapping.
156+
The `JsonKafkaHeaderMapper` has a method called `addToStringClasses()` that lets you supply the names of classes that should be treated this way for outbound mapping.
157157
During inbound mapping, they are mapped as `String`.
158158
By default, only `org.springframework.util.MimeType` and `org.springframework.http.MediaType` are mapped this way.
159159

@@ -170,7 +170,7 @@ When all applications are using 2.3 or higher, you can leave the property at its
170170
@Bean
171171
MessagingMessageConverter converter() {
172172
MessagingMessageConverter converter = new MessagingMessageConverter();
173-
DefaultKafkaHeaderMapper mapper = new DefaultKafkaHeaderMapper();
173+
JsonKafkaHeaderMapper mapper = new JsonKafkaHeaderMapper();
174174
mapper.setEncodeStrings(true);
175175
converter.setHeaderMapper(mapper);
176176
return converter;
@@ -187,16 +187,16 @@ Starting with 4.0, multi-value header mapping is supported, where the same logic
187187
By default, the `HeaderMapper` does **not** create multiple Kafka headers with the same name.
188188
Instead, when it encounters a collection value (e.g., a `List<byte[]>`), it serializes the entire collection into **one** Kafka header whose value is a JSON array.
189189

190-
* **Producer side:** `DefaultKafkaHeaderMapper` writes the JSON bytes, while `SimpleKafkaHeaderMapper` ignore it.
190+
* **Producer side:** `JsonKafkaHeaderMapper` writes the JSON bytes, while `SimpleKafkaHeaderMapper` ignore it.
191191
* **Consumer side:** the mapper exposes the header as a single value—the **last occurrence wins**; earlier duplicates are silently discarded.
192192

193193
Preserving each individual header requires explicit registration of patterns that designate the header as multi‑valued.
194194

195-
`DefaultKafkaHeaderMapper#setMultiValueHeaderPatterns(String... patterns)` accepts a list of patterns, which can be either wildcard expressions or exact header names.
195+
`JsonKafkaHeaderMapper#setMultiValueHeaderPatterns(String... patterns)` accepts a list of patterns, which can be either wildcard expressions or exact header names.
196196

197197
[source, java]
198198
----
199-
DefaultKafkaHeaderMapper mapper = new DefaultKafkaHeaderMapper();
199+
JsonKafkaHeaderMapper mapper = new JsonKafkaHeaderMapper();
200200
201201
// Explicit header names
202202
mapper.setMultiValueHeaderPatterns("test-multi-value1", "test-multi-value2");
@@ -214,5 +214,5 @@ NOTE: Regular expressions are *not* supported; only the +*+ wildcard is allowed
214214

215215
[IMPORTANT]
216216
====
217-
On the *Producer Side*, When `DefaultKafkaHeaderMapper` serializes a multi-value header, every element in that collection must be of a single Java type—mixing, for example, `String` and `byte[]` values under a single header key will lead to a conversion error.
217+
On the *Producer Side*, When `JsonKafkaHeaderMapper` serializes a multi-value header, every element in that collection must be of a single Java type—mixing, for example, `String` and `byte[]` values under a single header key will lead to a conversion error.
218218
====

spring-kafka-docs/src/main/antora/modules/ROOT/pages/kafka/receiving-messages/class-level-kafkalistener.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ static class MultiListenerBean {
3535
Starting with version 2.1.3, you can designate a `@KafkaHandler` method as the default method that is invoked if there is no match on other methods.
3636
At most, one method can be so designated.
3737
When using `@KafkaHandler` methods, the payload must have already been converted to the domain object (so the match can be performed).
38-
Use a custom deserializer, the `JsonDeserializer`, or the `JsonMessageConverter` with its `TypePrecedence` set to `TYPE_ID`.
38+
Use a custom deserializer, the `JacksonJsonDeserializer`, or the `JacksonJsonMessageConverter` with its `TypePrecedence` set to `TYPE_ID`.
3939
See xref:kafka/serdes.adoc[Serialization, Deserialization, and Message Conversion] for more information.
4040

4141
IMPORTANT: Due to some limitations in the way Spring resolves method arguments, a default `@KafkaHandler` cannot receive discrete headers; it must use the `ConsumerRecordMetadata` as discussed in xref:kafka/receiving-messages/listener-annotation.adoc#consumer-record-metadata[Consumer Record Metadata].

spring-kafka-docs/src/main/antora/modules/ROOT/pages/kafka/sending-messages.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ The `Header` contains a four-byte int (big-endian).
538538
The server must use this header to route the reply to the correct partition (`@KafkaListener` does this).
539539
In this case, though, the reply container must not use Kafka's group management feature and must be configured to listen on a fixed partition (by using a `TopicPartitionOffset` in its `ContainerProperties` constructor).
540540

541-
NOTE: The `DefaultKafkaHeaderMapper` requires Jackson to be on the classpath (for the `@KafkaListener`).
541+
NOTE: The `JsonKafkaHeaderMapper` requires Jackson to be on the classpath (for the `@KafkaListener`).
542542
If it is not available, the message converter has no header mapper, so you must configure a `MessagingMessageConverter` with a `SimpleKafkaHeaderMapper`, as shown earlier.
543543

544544
By default, 3 headers are used:

0 commit comments

Comments
 (0)