Skip to content

Commit 5fae3a9

Browse files
vipereyartembilan
authored andcommitted
GH-1125: Add FailedDeserializationInfo
Fixes #1125 Main ideas: * It does not enforces one single function to be set, it simply makes the new one to take precedence. * When setting the function/supplier via configuration, one single configuration key/value pair is enough. It tries to fit in any of the two possible options, otherwise, raises errors. * I'd personally rename `private BiFunction<byte[], Headers, T> failedDeserializationFunction;` to `failedDeserializationBiFunction`, respecting the name at setter level. However i read about avoiding sugar refactoring as rule of thumb. * Simplify and unify setFailedDeserializationFunction & recoverFromSupplier methods from ErrorHandlingDeserializer2 * deprecated since was in the wrong place * Javadocs for ErrorHandlingDeserializer2 and adocs syntax's amends. * Correct FailedDeserializationInfo & ErrorHandlingDeserializer2 copyright years * Correct FailedDeserializationInfo this invocation & ErrorHandlingDeserializer2 java docs headers *Polishing code style **Cherry-pick to 2.2.x** # Conflicts: # spring-kafka/src/main/java/org/springframework/kafka/support/serializer/ErrorHandlingDeserializer2.java # src/reference/asciidoc/whats-new.adoc
1 parent 020dfc9 commit 5fae3a9

File tree

4 files changed

+151
-37
lines changed

4 files changed

+151
-37
lines changed

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

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.io.ObjectOutputStream;
2222
import java.util.Map;
2323
import java.util.function.BiFunction;
24+
import java.util.function.Function;
2425

2526
import org.apache.kafka.common.header.Headers;
2627
import org.apache.kafka.common.header.internals.RecordHeader;
@@ -38,6 +39,7 @@
3839
*
3940
* @author Gary Russell
4041
* @author Artem Bilan
42+
* @author Victor Perez Rey
4143
*
4244
* @since 2.2
4345
*
@@ -83,7 +85,7 @@ public class ErrorHandlingDeserializer2<T> implements ExtendedDeserializer<T> {
8385

8486
private boolean isKey;
8587

86-
private BiFunction<byte[], Headers, T> failedDeserializationFunction;
88+
private Function<FailedDeserializationInfo, T> failedDeserializationFunction;
8789

8890
public ErrorHandlingDeserializer2() {
8991
super();
@@ -93,7 +95,23 @@ public ErrorHandlingDeserializer2(Deserializer<T> delegate) {
9395
this.delegate = setupDelegate(delegate);
9496
}
9597

98+
/**
99+
* Provide an alternative supplying mechanism when deserialization fails.
100+
* @param failedDeserializationFunction the {@link BiFunction} to use.
101+
* @deprecated since 2.2.8 in favor of {@link #setFailedDeserializationFunction(Function)}.
102+
*/
103+
@Deprecated
96104
public void setFailedDeserializationFunction(BiFunction<byte[], Headers, T> failedDeserializationFunction) {
105+
setFailedDeserializationFunction((failed) ->
106+
failedDeserializationFunction.apply(failed.getData(), failed.getHeaders()));
107+
}
108+
109+
/**
110+
* Provide an alternative supplying mechanism when deserialization fails.
111+
* @param failedDeserializationFunction the {@link Function} to use.
112+
* @since 2.2.8
113+
*/
114+
public void setFailedDeserializationFunction(Function<FailedDeserializationInfo, T> failedDeserializationFunction) {
97115
this.failedDeserializationFunction = failedDeserializationFunction;
98116
}
99117

@@ -159,9 +177,9 @@ private void setupFunction(Map<String, ?> configs, String configKey) {
159177
try {
160178
Object value = configs.get(configKey);
161179
Class<?> clazz = value instanceof Class ? (Class<?>) value : ClassUtils.forName((String) value, null);
162-
Assert.isTrue(BiFunction.class.isAssignableFrom(clazz), "'function' must be a 'BiFunction ', not a "
180+
Assert.isTrue(Function.class.isAssignableFrom(clazz), "'function' must be a 'Function ', not a "
163181
+ clazz.getName());
164-
this.failedDeserializationFunction = (BiFunction<byte[], Headers, T>) clazz.newInstance();
182+
this.failedDeserializationFunction = (Function<FailedDeserializationInfo, T>) clazz.newInstance();
165183
}
166184
catch (ClassNotFoundException | LinkageError | InstantiationException | IllegalAccessException e) {
167185
throw new IllegalStateException(e);
@@ -175,9 +193,7 @@ public T deserialize(String topic, byte[] data) {
175193
return this.delegate.deserialize(topic, data);
176194
}
177195
catch (Exception e) {
178-
return this.failedDeserializationFunction != null
179-
? this.failedDeserializationFunction.apply(data, null)
180-
: null;
196+
return recoverFromSupplier(topic, null, data, e);
181197
}
182198
}
183199

@@ -188,9 +204,18 @@ public T deserialize(String topic, Headers headers, byte[] data) {
188204
}
189205
catch (Exception e) {
190206
deserializationException(headers, data, e);
191-
return this.failedDeserializationFunction != null
192-
? this.failedDeserializationFunction.apply(data, headers)
193-
: null;
207+
return recoverFromSupplier(topic, headers, data, e);
208+
}
209+
}
210+
211+
private T recoverFromSupplier(String topic, Headers headers, byte[] data, Exception exception) {
212+
if (this.failedDeserializationFunction != null) {
213+
FailedDeserializationInfo failedDeserializationInfo =
214+
new FailedDeserializationInfo(topic, headers, data, this.isForKey, exception);
215+
return this.failedDeserializationFunction.apply(failedDeserializationInfo);
216+
}
217+
else {
218+
return null;
194219
}
195220
}
196221

@@ -210,8 +235,8 @@ private void deserializationException(Headers headers, byte[] data, Exception e)
210235
try (ObjectOutputStream oos = new ObjectOutputStream(stream)) {
211236
exception = new DeserializationException("failed to deserialize",
212237
data, this.isKey, new RuntimeException("Could not deserialize type "
213-
+ e.getClass().getName() + " with message " + e.getMessage()
214-
+ " failure: " + ex.getMessage()));
238+
+ e.getClass().getName() + " with message " + e.getMessage()
239+
+ " failure: " + ex.getMessage()));
215240
oos.writeObject(exception);
216241
}
217242
catch (IOException ex2) {
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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.serializer;
18+
19+
import java.util.Arrays;
20+
21+
import org.apache.kafka.common.header.Headers;
22+
23+
/**
24+
* Class containing all the contextual information around a deserialization error.
25+
*
26+
* @author Victor Perez Rey
27+
*
28+
* @since 2.2.8
29+
*/
30+
public class FailedDeserializationInfo {
31+
32+
private final String topic;
33+
34+
private final Headers headers;
35+
36+
private final byte[] data;
37+
38+
private final boolean isForKey;
39+
40+
private final Exception exception;
41+
42+
/**
43+
* Construct an instance with the contextual information.
44+
* @param topic topic associated with the data.
45+
* @param headers headers associated with the record; may be empty.
46+
* @param data serialized bytes; may be null.
47+
* @param isForKey true for a key deserializer, false otherwise.
48+
* @param exception exception causing the deserialization error.
49+
*/
50+
public FailedDeserializationInfo(String topic, Headers headers, byte[] data, boolean isForKey,
51+
Exception exception) {
52+
53+
this.topic = topic;
54+
this.headers = headers;
55+
this.data = data;
56+
this.isForKey = isForKey;
57+
this.exception = exception;
58+
}
59+
60+
public String getTopic() {
61+
return this.topic;
62+
}
63+
64+
public Headers getHeaders() {
65+
return this.headers;
66+
}
67+
68+
public byte[] getData() {
69+
return this.data;
70+
}
71+
72+
public boolean isForKey() {
73+
return this.isForKey;
74+
}
75+
76+
public Exception getException() {
77+
return this.exception;
78+
}
79+
80+
@Override
81+
public String toString() {
82+
return "FailedDeserializationInfo{" +
83+
"topic='" + this.topic + '\'' +
84+
", headers=" + this.headers +
85+
", data=" + Arrays.toString(this.data) +
86+
", isForKey=" + this.isForKey +
87+
", exception=" + this.exception +
88+
'}';
89+
}
90+
91+
}

spring-kafka/src/test/java/org/springframework/kafka/annotation/BatchListenerConversion2Tests.java

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@
2222
import java.util.Map;
2323
import java.util.concurrent.CountDownLatch;
2424
import java.util.concurrent.TimeUnit;
25-
import java.util.function.BiFunction;
25+
import java.util.function.Function;
2626

2727
import org.apache.kafka.clients.consumer.ConsumerConfig;
2828
import org.apache.kafka.clients.producer.ProducerConfig;
29-
import org.apache.kafka.common.header.Headers;
3029
import org.apache.kafka.common.serialization.StringSerializer;
3130
import org.junit.ClassRule;
3231
import org.junit.Test;
@@ -43,21 +42,21 @@
4342
import org.springframework.kafka.core.ProducerFactory;
4443
import org.springframework.kafka.support.converter.BytesJsonMessageConverter;
4544
import org.springframework.kafka.support.serializer.ErrorHandlingDeserializer2;
45+
import org.springframework.kafka.support.serializer.FailedDeserializationInfo;
4646
import org.springframework.kafka.support.serializer.JsonDeserializer;
4747
import org.springframework.kafka.test.rule.EmbeddedKafkaRule;
4848
import org.springframework.kafka.test.utils.KafkaTestUtils;
4949
import org.springframework.test.annotation.DirtiesContext;
50-
import org.springframework.test.context.ContextConfiguration;
51-
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
50+
import org.springframework.test.context.junit4.SpringRunner;
5251

5352
/**
5453
* @author Gary Russell
54+
* @author Victor Perez Rey
5555
*
5656
* @since 2.1.1
5757
*
5858
*/
59-
@ContextConfiguration
60-
@RunWith(SpringJUnit4ClassRunner.class)
59+
@RunWith(SpringRunner.class)
6160
@DirtiesContext
6261
public class BatchListenerConversion2Tests {
6362

@@ -117,8 +116,7 @@ public Map<String, Object> consumerConfigs() {
117116

118117
@Bean
119118
public KafkaTemplate<Integer, String> template() {
120-
KafkaTemplate<Integer, String> kafkaTemplate = new KafkaTemplate<>(producerFactory());
121-
return kafkaTemplate;
119+
return new KafkaTemplate<>(producerFactory());
122120
}
123121

124122
@Bean
@@ -197,23 +195,23 @@ public String toString() {
197195

198196
public static class BadFoo extends Foo {
199197

200-
private final byte[] failedDecode;
198+
private final FailedDeserializationInfo failedDeserializationInfo;
201199

202-
public BadFoo(byte[] failedDecode) {
203-
this.failedDecode = failedDecode;
200+
public BadFoo(FailedDeserializationInfo failedDeserializationInfo) {
201+
this.failedDeserializationInfo = failedDeserializationInfo;
204202
}
205203

206-
public byte[] getFailedDecode() {
207-
return this.failedDecode;
204+
public FailedDeserializationInfo getFailedDeserializationInfo() {
205+
return failedDeserializationInfo;
208206
}
209207

210208
}
211209

212-
public static class FailedFooProvider implements BiFunction<byte[], Headers, Foo> {
210+
public static class FailedFooProvider implements Function<FailedDeserializationInfo, Foo> {
213211

214212
@Override
215-
public Foo apply(byte[] t, Headers u) {
216-
return new BadFoo(t);
213+
public Foo apply(FailedDeserializationInfo failedDeserializationInfo) {
214+
return new BadFoo(failedDeserializationInfo);
217215
}
218216

219217
}

src/reference/asciidoc/kafka.adoc

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2286,9 +2286,9 @@ If the delegate fails to deserialize the record content, the `ErrorHandlingDeser
22862286
When you use a record-level `MessageListener`, if the `ConsumerRecord` contains a `DeserializationException` header for either the key or value, the container's `ErrorHandler` is called with the failed `ConsumerRecord`.
22872287
The record is not passed to the listener.
22882288

2289-
Alternatively, you can configure the `ErrorHandlingDeserializer2` to create a custom value by providing a `failedDeserializationFunction`, which is a `BiConsumer<byte[], Headers, T>`.
2289+
Alternatively, you can configure the `ErrorHandlingDeserializer2` to create a custom value by providing a `failedDeserializationFunction`, which is a `Function<FailedDeserializationInfo, T>`.
22902290
This function is invoked to create an instance of `T`, which is passed to the listener in the usual fashion.
2291-
The raw record value and headers are provided to the function.
2291+
An object of type `FailedDeserializationInfo`, which contains all the contextual information is provided to the function.
22922292
You can find the `DeserializationException` (as a serialized Java object) in headers.
22932293
See the https://docs.spring.io/spring-kafka/api/org/springframework/kafka/support/serializer/ErrorHandlingDeserializer2.html[Javadoc] for the `ErrorHandlingDeserializer2` for more information.
22942294

@@ -2323,23 +2323,23 @@ The following example uses a `failedDeserializationFunction`.
23232323
----
23242324
public class BadFoo extends Foo {
23252325
2326-
private final byte[] failedDecode;
2326+
private final FailedDeserializationInfo failedDeserializationInfo;
23272327
2328-
public BadFoo(byte[] failedDecode) {
2329-
this.failedDecode = failedDecode;
2328+
public BadFoo(FailedDeserializationInfo failedDeserializationInfo) {
2329+
this.failedDeserializationInfo = failedDeserializationInfo;
23302330
}
23312331
2332-
public byte[] getFailedDecode() {
2333-
return this.failedDecode;
2332+
public FailedDeserializationInfo getFailedDeserializationInfo() {
2333+
return this.failedDeserializationInfo;
23342334
}
23352335
23362336
}
23372337
2338-
public class FailedFooProvider implements BiFunction<byte[], Headers, Foo> {
2338+
public class FailedFooProvider implements Function<FailedDeserializationInfo, Foo> {
23392339
23402340
@Override
2341-
public Foo apply(byte[] t, Headers u) {
2342-
return new BadFoo(t);
2341+
public Foo apply(FailedDeserializationInfo info) {
2342+
return new BadFoo(info);
23432343
}
23442344
23452345
}

0 commit comments

Comments
 (0)