Skip to content

Commit 3dc1fc3

Browse files
MaheshAravindVsobychacko
authored andcommitted
GH-3953: Ensure most specific serializer is used
Fixes: #3953 **Auto-cherry-pick to `3.3.x` & `3.2.x`** When there are multiple serializers available for that a type can be assigned to, the most specific one should be used Signed-off-by: Mahesh Aravind V <[email protected]>
1 parent cee1ceb commit 3dc1fc3

File tree

2 files changed

+90
-10
lines changed

2 files changed

+90
-10
lines changed

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

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616

1717
package org.springframework.kafka.support.serializer;
1818

19-
import java.util.LinkedHashMap;
20-
import java.util.Map;
19+
import java.util.*;
2120
import java.util.Map.Entry;
2221

2322
import org.apache.kafka.common.errors.SerializationException;
@@ -32,13 +31,27 @@
3231
* @author Gary Russell
3332
* @author Artem Bilan
3433
* @author Wang Zhiyang
34+
* @author Mahesh Aravind V
3535
*
3636
* @since 2.7.9
3737
*
3838
*/
3939
public class DelegatingByTypeSerializer implements Serializer<Object> {
4040

41-
private final Map<Class<?>, Serializer<?>> delegates = new LinkedHashMap<>();
41+
private static final Comparator<Class<?>> DELEGATES_ASSIGNABILITY_COMPARATOR =
42+
(type1, type2) -> {
43+
44+
if (type1.isAssignableFrom(type2)) {
45+
return 1;
46+
}
47+
if (type2.isAssignableFrom(type1)) {
48+
return -1;
49+
}
50+
51+
return 0;
52+
};
53+
54+
private final Map<Class<?>, Serializer<?>> delegates = new TreeMap<>(DELEGATES_ASSIGNABILITY_COMPARATOR);
4255

4356
private final boolean assignable;
4457

@@ -51,17 +64,23 @@ public DelegatingByTypeSerializer(Map<Class<?>, Serializer<?>> delegates) {
5164
}
5265

5366
/**
54-
* Construct an instance with the map of delegates; keys matched exactly or if the
55-
* target object is assignable to the key, depending on the assignable argument.
56-
* If assignable, entries are checked in the natural entry order so an ordered map
57-
* such as a {@link LinkedHashMap} is recommended.
58-
* @param delegates the delegates.
59-
* @param assignable whether the target is assignable to the key.
67+
* Construct an instance with the map of delegates.
68+
* If {@code assignable} is {@code false}, only exact key matches are considered.
69+
* If {@code assignable} is {@code true}, a delegate is selected if its key class
70+
* is assignable from the target object's class. When multiple matches are possible,
71+
* the most specific matching class is selected — that is, the closest match in the
72+
* class hierarchy.
73+
*
74+
* @param delegates the delegates
75+
* @param assignable true if {@link #findDelegate(Object, Map)} should consider assignability to
76+
* the key rather than an exact match.
77+
*
6078
* @since 2.8.3
6179
*/
6280
public DelegatingByTypeSerializer(Map<Class<?>, Serializer<?>> delegates, boolean assignable) {
6381
Assert.notNull(delegates, "'delegates' cannot be null");
6482
Assert.noNullElements(delegates.values(), "Serializers in delegates map cannot be null");
83+
6584
this.delegates.putAll(delegates);
6685
this.assignable = assignable;
6786
}
@@ -99,7 +118,7 @@ public byte[] serialize(String topic, Headers headers, Object data) {
99118
return delegate.serialize(topic, headers, data);
100119
}
101120

102-
private <T> Serializer<T> findDelegate(T data) {
121+
protected <T> Serializer<T> findDelegate(T data) {
103122
return findDelegate(data, this.delegates);
104123
}
105124

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2021-2025 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.LinkedHashMap;
20+
import java.util.Map;
21+
22+
import org.apache.kafka.common.serialization.Serializer;
23+
import org.junit.jupiter.api.Nested;
24+
import org.junit.jupiter.api.Test;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
import static org.mockito.Mockito.mock;
28+
29+
/**
30+
* @author Mahesh Aravind V
31+
*
32+
*/
33+
class DelegatingByTypeSerializerTests {
34+
@Nested
35+
class AssignableTest {
36+
@Test
37+
void shouldOrderDelegatesSoChildComesBeforeParent() {
38+
class Parent { }
39+
40+
class Child extends Parent { }
41+
42+
Serializer mockParentSerializer = mock(Serializer.class);
43+
Serializer mockChildSerializer = mock(Serializer.class);
44+
45+
// Using LinkedHashMap to ensure the order is always wrong
46+
Map<Class<?>, Serializer<?>> delegates = new LinkedHashMap<>();
47+
delegates.put(Parent.class, mockParentSerializer);
48+
delegates.put(Child.class, mockChildSerializer);
49+
50+
DelegatingByTypeSerializer serializer = new DelegatingByTypeSerializer(delegates, true);
51+
52+
53+
Serializer childSerializer = serializer.findDelegate(mock(Child.class));
54+
Serializer parentSerializer = serializer.findDelegate(mock(Parent.class));
55+
56+
57+
assertThat(childSerializer).isEqualTo(mockChildSerializer);
58+
assertThat(parentSerializer).isEqualTo(mockParentSerializer);
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)