Skip to content

Commit ae03a53

Browse files
garyrussellartembilan
authored andcommitted
Delegating(De)Serializer Improvements
Automatically handle "standard" types supported by the `Serdes`.
1 parent 0f43e34 commit ae03a53

File tree

5 files changed

+141
-14
lines changed

5 files changed

+141
-14
lines changed

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

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 the original author or authors.
2+
* Copyright 2019-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,33 +18,45 @@
1818

1919
import java.util.HashMap;
2020
import java.util.Map;
21+
import java.util.concurrent.ConcurrentHashMap;
2122

23+
import org.apache.kafka.common.header.Header;
2224
import org.apache.kafka.common.header.Headers;
2325
import org.apache.kafka.common.serialization.Deserializer;
26+
import org.apache.kafka.common.serialization.Serde;
27+
import org.apache.kafka.common.serialization.Serdes;
2428

29+
import org.springframework.core.log.LogAccessor;
2530
import org.springframework.lang.Nullable;
2631
import org.springframework.util.Assert;
2732
import org.springframework.util.ClassUtils;
2833
import org.springframework.util.StringUtils;
2934

3035
/**
3136
* A {@link Deserializer} that delegates to other deserializers based on a serialization
32-
* selector header.
37+
* selector header. It is not necessary to configure standard deserializers supported by
38+
* {@link Serdes}.
3339
*
3440
* @author Gary Russell
3541
* @since 2.3
3642
*
3743
*/
3844
public class DelegatingDeserializer implements Deserializer<Object> {
3945

46+
private static final LogAccessor LOGGER = new LogAccessor(DelegatingDeserializer.class);
47+
4048
/**
4149
* Name of the configuration property containing the serialization selector map with
4250
* format {@code selector:class,...}.
4351
*/
4452
public static final String SERIALIZATION_SELECTOR_CONFIG = DelegatingSerializer.SERIALIZATION_SELECTOR_CONFIG;
4553

4654

47-
private final Map<String, Deserializer<?>> delegates = new HashMap<>();
55+
private final Map<String, Deserializer<? extends Object>> delegates = new ConcurrentHashMap<>();
56+
57+
private final Map<String, Object> autoConfigs = new HashMap<>();
58+
59+
private boolean forKeys;
4860

4961
/**
5062
* Construct an instance that will be configured in {@link #configure(Map, boolean)}
@@ -57,7 +69,8 @@ public DelegatingDeserializer() {
5769
/**
5870
* Construct an instance with the supplied mapping of selectors to delegate
5971
* deserializers. The selector must be supplied in the
60-
* {@link DelegatingSerializer#SERIALIZATION_SELECTOR} header.
72+
* {@link DelegatingSerializer#SERIALIZATION_SELECTOR} header. It is not necessary to
73+
* configure standard deserializers supported by {@link Serdes}.
6174
* @param delegates the map of delegates.
6275
*/
6376
public DelegatingDeserializer(Map<String, Deserializer<?>> delegates) {
@@ -67,6 +80,8 @@ public DelegatingDeserializer(Map<String, Deserializer<?>> delegates) {
6780
@SuppressWarnings("unchecked")
6881
@Override
6982
public void configure(Map<String, ?> configs, boolean isKey) {
83+
this.autoConfigs.putAll(configs);
84+
this.forKeys = isKey;
7085
Object value = configs.get(SERIALIZATION_SELECTOR_CONFIG);
7186
if (value == null) {
7287
return;
@@ -151,13 +166,19 @@ public Object deserialize(String topic, byte[] data) {
151166

152167
@Override
153168
public Object deserialize(String topic, Headers headers, byte[] data) {
154-
byte[] value = headers.lastHeader(DelegatingSerializer.SERIALIZATION_SELECTOR).value();
169+
byte[] value = null;
170+
Header header = headers.lastHeader(DelegatingSerializer.SERIALIZATION_SELECTOR);
171+
if (header != null) {
172+
value = header.value();
173+
}
155174
if (value == null) {
156175
throw new IllegalStateException("No '" + DelegatingSerializer.SERIALIZATION_SELECTOR + "' header present");
157176
}
158177
String selector = new String(value).replaceAll("\"", "");
159-
@SuppressWarnings("unchecked")
160-
Deserializer<Object> deserializer = (Deserializer<Object>) this.delegates.get(selector);
178+
Deserializer<? extends Object> deserializer = this.delegates.get(selector);
179+
if (deserializer == null) {
180+
deserializer = trySerdes(selector);
181+
}
161182
if (deserializer == null) {
162183
return data;
163184
}
@@ -166,6 +187,25 @@ public Object deserialize(String topic, Headers headers, byte[] data) {
166187
}
167188
}
168189

190+
/*
191+
* Package for testing.
192+
*/
193+
@Nullable
194+
Deserializer<? extends Object> trySerdes(String key) {
195+
try {
196+
Class<?> clazz = ClassUtils.forName(key, ClassUtils.getDefaultClassLoader());
197+
Serde<? extends Object> serdeFrom = Serdes.serdeFrom(clazz);
198+
Deserializer<? extends Object> deserializer = serdeFrom.deserializer();
199+
deserializer.configure(this.autoConfigs, this.forKeys);
200+
this.delegates.put(key, deserializer);
201+
return deserializer;
202+
}
203+
catch (IllegalStateException | ClassNotFoundException | LinkageError e) {
204+
this.delegates.put(key, Serdes.serdeFrom(byte[].class).deserializer());
205+
return null;
206+
}
207+
}
208+
169209
@Override
170210
public void close() {
171211
this.delegates.values().forEach(deser -> deser.close());

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

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 the original author or authors.
2+
* Copyright 2019-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,25 +18,34 @@
1818

1919
import java.util.HashMap;
2020
import java.util.Map;
21+
import java.util.concurrent.ConcurrentHashMap;
2122

23+
import org.apache.kafka.common.header.Header;
2224
import org.apache.kafka.common.header.Headers;
25+
import org.apache.kafka.common.header.internals.RecordHeader;
26+
import org.apache.kafka.common.serialization.Serde;
27+
import org.apache.kafka.common.serialization.Serdes;
2328
import org.apache.kafka.common.serialization.Serializer;
2429

30+
import org.springframework.core.log.LogAccessor;
2531
import org.springframework.lang.Nullable;
2632
import org.springframework.util.Assert;
2733
import org.springframework.util.ClassUtils;
2834
import org.springframework.util.StringUtils;
2935

3036
/**
3137
* A {@link Serializer} that delegates to other serializers based on a serialization
32-
* selector header.
38+
* selector header. If the header is missing, and the type is supported by {@link Serdes}
39+
* we will delegate to that serializer type.
3340
*
3441
* @author Gary Russell
3542
* @since 2.3
3643
*
3744
*/
3845
public class DelegatingSerializer implements Serializer<Object> {
3946

47+
private static final LogAccessor LOGGER = new LogAccessor(DelegatingDeserializer.class);
48+
4049
/**
4150
* Name of the header containing the serialization selector.
4251
*/
@@ -48,7 +57,11 @@ public class DelegatingSerializer implements Serializer<Object> {
4857
*/
4958
public static final String SERIALIZATION_SELECTOR_CONFIG = "spring.kafka.serialization.selector.config";
5059

51-
private final Map<String, Serializer<?>> delegates = new HashMap<>();
60+
private final Map<String, Serializer<?>> delegates = new ConcurrentHashMap<>();
61+
62+
private final Map<String, Object> autoConfigs = new HashMap<>();
63+
64+
private boolean forKeys;
5265

5366
/**
5467
* Construct an instance that will be configured in {@link #configure(Map, boolean)}
@@ -61,7 +74,8 @@ public DelegatingSerializer() {
6174
/**
6275
* Construct an instance with the supplied mapping of selectors to delegate
6376
* serializers. The selector must be supplied in the
64-
* {@link DelegatingSerializer#SERIALIZATION_SELECTOR} header.
77+
* {@link DelegatingSerializer#SERIALIZATION_SELECTOR} header. It is not necessary to
78+
* configure standard serializers supported by {@link Serdes}.
6579
* @param delegates the map of delegates.
6680
*/
6781
public DelegatingSerializer(Map<String, Serializer<?>> delegates) {
@@ -71,6 +85,8 @@ public DelegatingSerializer(Map<String, Serializer<?>> delegates) {
7185
@SuppressWarnings("unchecked")
7286
@Override
7387
public void configure(Map<String, ?> configs, boolean isKey) {
88+
this.autoConfigs.putAll(configs);
89+
this.forKeys = isKey;
7490
Object value = configs.get(SERIALIZATION_SELECTOR_CONFIG);
7591
if (value == null) {
7692
return;
@@ -156,9 +172,24 @@ public byte[] serialize(String topic, Object data) {
156172

157173
@Override
158174
public byte[] serialize(String topic, Headers headers, Object data) {
159-
byte[] value = headers.lastHeader(SERIALIZATION_SELECTOR).value();
175+
byte[] value = null;
176+
Header header = headers.lastHeader(SERIALIZATION_SELECTOR);
177+
if (header != null) {
178+
value = header.value();
179+
}
160180
if (value == null) {
161-
throw new IllegalStateException("No '" + SERIALIZATION_SELECTOR + "' header present");
181+
value = trySerdes(data);
182+
if (value == null) {
183+
throw new IllegalStateException("No '" + SERIALIZATION_SELECTOR
184+
+ "' header present and type (" + data.getClass().getName()
185+
+ ") is not supported by Serdes");
186+
}
187+
try {
188+
headers.add(new RecordHeader(SERIALIZATION_SELECTOR, value));
189+
}
190+
catch (IllegalStateException e) {
191+
LOGGER.debug(e, () -> "Could not set header for type " + data.getClass());
192+
}
162193
}
163194
String selector = new String(value).replaceAll("\"", "");
164195
@SuppressWarnings("unchecked")
@@ -170,6 +201,24 @@ public byte[] serialize(String topic, Headers headers, Object data) {
170201
return serializer.serialize(topic, headers, data);
171202
}
172203

204+
/*
205+
* Package for testing.
206+
*/
207+
@Nullable
208+
byte[] trySerdes(Object data) {
209+
try {
210+
Serde<? extends Object> serdeFrom = Serdes.serdeFrom(data.getClass());
211+
Serializer<?> serializer = serdeFrom.serializer();
212+
serializer.configure(this.autoConfigs, this.forKeys);
213+
String key = data.getClass().getName();
214+
this.delegates.put(key, serializer);
215+
return key.getBytes();
216+
}
217+
catch (IllegalStateException e) {
218+
return null;
219+
}
220+
}
221+
173222
@Override
174223
public void close() {
175224
this.delegates.values().forEach(ser -> ser.close());

spring-kafka/src/test/java/org/springframework/kafka/support/serializer/DelegatingSerializationTests.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 the original author or authors.
2+
* Copyright 2019-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,9 @@
1717
package org.springframework.kafka.support.serializer;
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.mockito.Mockito.spy;
21+
import static org.mockito.Mockito.times;
22+
import static org.mockito.Mockito.verify;
2023

2124
import java.util.Collections;
2225
import java.util.HashMap;
@@ -93,6 +96,19 @@ private void doTest(DelegatingSerializer serializer, DelegatingDeserializer dese
9396
assertThat(serialized).isEqualTo(new byte[] { 'b', 'a', 'r' });
9497
assertThat(deserializer.deserialize("foo", headers, serialized)).isEqualTo("bar");
9598

99+
// implicit Serdes
100+
headers.remove(DelegatingSerializer.SERIALIZATION_SELECTOR);
101+
DelegatingSerializer spySe = spy(serializer);
102+
serialized = spySe.serialize("foo", headers, 42L);
103+
serialized = spySe.serialize("foo", headers, 42L);
104+
verify(spySe, times(1)).trySerdes(42L);
105+
assertThat(headers.lastHeader(DelegatingSerializer.SERIALIZATION_SELECTOR).value())
106+
.isEqualTo(Long.class.getName().getBytes());
107+
DelegatingDeserializer spyDe = spy(deserializer);
108+
assertThat(spyDe.deserialize("foo", headers, serialized)).isEqualTo(42L);
109+
spyDe.deserialize("foo", headers, serialized);
110+
verify(spyDe, times(1)).trySerdes(Long.class.getName());
111+
96112
// The DKHM will jsonize the value; test that we ignore the quotes
97113
MessageHeaders messageHeaders = new MessageHeaders(
98114
Collections.singletonMap(DelegatingSerializer.SERIALIZATION_SELECTOR, "string"));
@@ -102,6 +118,18 @@ private void doTest(DelegatingSerializer serializer, DelegatingDeserializer dese
102118
serialized = serializer.serialize("foo", headers, "bar");
103119
assertThat(serialized).isEqualTo(new byte[] { 'b', 'a', 'r' });
104120
assertThat(deserializer.deserialize("foo", headers, serialized)).isEqualTo("bar");
121+
122+
}
123+
124+
@Test
125+
void testBadIncomingOnlyOnce() {
126+
DelegatingDeserializer spy = spy(new DelegatingDeserializer());
127+
Headers headers = new RecordHeaders();
128+
headers.add(new RecordHeader(DelegatingSerializer.SERIALIZATION_SELECTOR, "junk".getBytes()));
129+
byte[] data = "foo".getBytes();
130+
assertThat(spy.deserialize("foo", headers, data)).isSameAs(data);
131+
spy.deserialize("foo", headers, data);
132+
verify(spy, times(1)).trySerdes("junk");
105133
}
106134

107135
}

src/reference/asciidoc/kafka.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3748,6 +3748,10 @@ Producers would then set the `DelegatingSerializer.SERIALIZATION_SELECTOR` heade
37483748

37493749
This technique supports sending different types to the same topic (or different topics).
37503750

3751+
NOTE: Starting with version 2.5.1, it is not necessary to set the header if the type (key or value) is one of the standard types supported by `Serdes` (`Long`, `Integer`, etc).
3752+
Instead, the serializer will set the header to the class name of the type.
3753+
It is not necessary to configure serializers or deserializers for these types, they will be created (once) dynamically.
3754+
37513755
For another technique to send different types to different topics, see <<routing-template>>.
37523756

37533757
[[retrying-deserialization]]

src/reference/asciidoc/whats-new.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ See <<string-serde>> for more information.
9494
The `JsonDeserializer` now has more flexibility to determine the deserialization type.
9595
See <<serdes-type-methods>> for more information.
9696

97+
[[x25-delegate-serde]]
98+
==== Delegating Serializer/Deserializer
99+
100+
The `DelegatingSerializer` can now handle "standard" types, when the outbound record has no header.
101+
See <<delegating-serialization>> for more information.
102+
97103
[[x25-testing]]
98104
==== Testing Changes
99105

0 commit comments

Comments
 (0)