-
Notifications
You must be signed in to change notification settings - Fork 1.7k
GH-3067: Spring Kafka support multiple headers with same key. #3874
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
c7e5e09
42010c6
dfdfb8d
865b26a
019efb7
065bedc
6aed15b
462fe25
b3f4374
34e6860
dd24248
13267dc
c9c360e
4a762bd
b5375d4
c945dd5
07a49df
732ae6a
585f356
667f76c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.LinkedHashSet; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
| import java.util.Map; | ||
|
|
@@ -79,6 +80,12 @@ public abstract class AbstractKafkaHeaderMapper implements KafkaHeaderMapper { | |
|
|
||
| private Charset charset = StandardCharsets.UTF_8; | ||
|
|
||
| private final List<HeaderMatcher> matchersForListValue = new ArrayList<>(); | ||
|
|
||
| private final Set<String> cachedHeadersForListValue = new LinkedHashSet<>(); | ||
|
|
||
| private final Set<String> cachedHeadersForSingleValue = new LinkedHashSet<>(); | ||
|
|
||
| /** | ||
| * Construct a mapper that will match the supplied patterns (outbound) and all headers | ||
| * (inbound). For outbound mapping, certain internal framework headers are never | ||
|
|
@@ -97,6 +104,20 @@ public AbstractKafkaHeaderMapper(String... patterns) { | |
| * @param patterns the patterns. | ||
| */ | ||
| protected AbstractKafkaHeaderMapper(boolean outbound, String... patterns) { | ||
| this(outbound, new ArrayList<>(), patterns); | ||
| } | ||
|
|
||
| /** | ||
| * Construct a mapper that will match the supplied patterns (outbound) and all headers | ||
| * (inbound). For outbound mapping, certain internal framework headers are never | ||
| * mapped. For inbound mapping, Headers that match the pattern specified in | ||
| * {@code patternsForListValue} will be appended to the values under the same key. | ||
| * | ||
|
||
| * @param outbound true for an outbound mapper. | ||
| * @param patternsForListValue the patterns for multiple values at the same key. | ||
|
||
| * @param patterns the patterns. | ||
| */ | ||
| protected AbstractKafkaHeaderMapper(boolean outbound, List<String> patternsForListValue, String... patterns) { | ||
| Assert.notNull(patterns, "'patterns' must not be null"); | ||
| this.outbound = outbound; | ||
| if (outbound) { | ||
|
|
@@ -123,6 +144,11 @@ protected AbstractKafkaHeaderMapper(boolean outbound, String... patterns) { | |
| for (String pattern : patterns) { | ||
| this.matchers.add(new SimplePatternBasedHeaderMatcher(pattern)); | ||
| } | ||
|
|
||
| for (String patternForListValue : patternsForListValue) { | ||
| this.matchersForListValue.add(new SimplePatternBasedHeaderMatcher(patternForListValue)); | ||
| } | ||
|
|
||
| } | ||
|
|
||
| /** | ||
|
|
@@ -287,6 +313,34 @@ private String mapRawIn(String header, byte[] value) { | |
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Check whether the header value should be mapped to multiple values. | ||
| * @param headerName the header name. | ||
| * @return True for multiple values at the same key. | ||
| */ | ||
| protected boolean isHeaderForListValue(String headerName) { | ||
|
||
| if (this.matchersForListValue.isEmpty()) { | ||
| return false; | ||
| } | ||
|
|
||
| if (this.cachedHeadersForSingleValue.contains(headerName)) { | ||
| return false; | ||
| } | ||
|
|
||
| if (this.cachedHeadersForListValue.contains(headerName)) { | ||
| return true; | ||
| } | ||
|
|
||
| for (HeaderMatcher headerMatcher : this.matchersForListValue) { | ||
| if (headerMatcher.matchHeader(headerName)) { | ||
| this.cachedHeadersForListValue.add(headerName); | ||
| return true; | ||
| } | ||
| } | ||
| this.cachedHeadersForSingleValue.add(headerName); | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * A matcher for headers. | ||
| * @since 2.3 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ | |
| import java.io.IOException; | ||
| import java.nio.ByteBuffer; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
|
|
@@ -48,6 +49,7 @@ | |
| * @author Gary Russell | ||
| * @author Artem Bilan | ||
| * @author Soby Chacko | ||
| * @author Sanghyeok An | ||
| * | ||
| * @since 1.3 | ||
| * | ||
|
|
@@ -102,6 +104,27 @@ public DefaultKafkaHeaderMapper() { | |
| this(JacksonUtils.enhancedObjectMapper()); | ||
| } | ||
|
|
||
| /** | ||
| * Construct an instance with the default object mapper and default header patterns | ||
| * for outbound headers and default header patterns for inbound multi-value headers; | ||
| * all inbound headers are mapped. The default pattern list is | ||
| * {@code "!id", "!timestamp" and "*"}. In addition, most of the headers in | ||
| * {@link KafkaHeaders} are never mapped as headers since they represent data in | ||
| * consumer/producer records. | ||
| * Headers that match the pattern specified in {@code patternsForListValue} will be | ||
| * appended to the values under the same key. | ||
| * | ||
| * @param patternsForListValue the patterns for multiple values at the same key. | ||
| * @see #DefaultKafkaHeaderMapper(ObjectMapper) | ||
| */ | ||
| public DefaultKafkaHeaderMapper(List<String> patternsForListValue) { | ||
| this(JacksonUtils.enhancedObjectMapper(), | ||
| patternsForListValue, | ||
| "!" + MessageHeaders.ID, | ||
| "!" + MessageHeaders.TIMESTAMP, | ||
| "*"); | ||
| } | ||
|
|
||
| /** | ||
| * Construct an instance with the provided object mapper and default header patterns | ||
| * for outbound headers; all inbound headers are mapped. The patterns are applied in | ||
|
|
@@ -148,11 +171,32 @@ public DefaultKafkaHeaderMapper(String... patterns) { | |
| * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) | ||
| */ | ||
| public DefaultKafkaHeaderMapper(ObjectMapper objectMapper, String... patterns) { | ||
| this(true, objectMapper, patterns); | ||
| this(true, objectMapper, new ArrayList<>(), patterns); | ||
| } | ||
|
|
||
| /** | ||
| * Construct an instance with the provided object mapper and the provided header | ||
| * patterns for outbound headers; all inbound headers are mapped. The patterns are | ||
| * applied in order, stopping on the first match (positive or negative). Patterns are | ||
| * negated by preceding them with "!". The patterns will replace the default patterns; | ||
| * you generally should not map the {@code "id" and "timestamp"} headers. Note: most | ||
| * of the headers in {@link KafkaHeaders} are never mapped as headers since they | ||
| * represent data in consumer/producer records. | ||
| * @param objectMapper the object mapper. | ||
| * @param patternsForListValue the patterns for multiple values at the same key. | ||
| * @param patterns the patterns. | ||
| * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String) | ||
| */ | ||
| public DefaultKafkaHeaderMapper(ObjectMapper objectMapper, List<String> patternsForListValue, String... patterns) { | ||
| this(true, objectMapper, patternsForListValue, patterns); | ||
| } | ||
|
|
||
| private DefaultKafkaHeaderMapper(boolean outbound, ObjectMapper objectMapper, String... patterns) { | ||
| super(outbound, patterns); | ||
| this(outbound, objectMapper, new ArrayList<>(), patterns); | ||
| } | ||
|
|
||
| private DefaultKafkaHeaderMapper(boolean outbound, ObjectMapper objectMapper, List<String> patternsForListValue, String... patterns) { | ||
| super(outbound, patternsForListValue, patterns); | ||
| Assert.notNull(objectMapper, "'objectMapper' must not be null"); | ||
| Assert.noNullElements(patterns, "'patterns' must not have null elements"); | ||
| this.objectMapper = objectMapper; | ||
|
|
@@ -324,12 +368,39 @@ else if (!(headerName.equals(JSON_TYPES)) && matchesForInbound(headerName)) { | |
| populateJsonValueHeader(header, requestedType, headers); | ||
| } | ||
| else { | ||
| headers.put(headerName, headerValueToAddIn(header)); | ||
| handleHeader(headerName, header, headers); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Handle non-reserved headers in {@link DefaultKafkaHeaderMapper}. | ||
| * @param headerName the header name. | ||
| * @param header the header instance. | ||
| * @param headers the target headers. | ||
| * @since 4.0.0 | ||
| */ | ||
|
|
||
| protected void handleHeader(String headerName, Header header, final Map<String, Object> headers) { | ||
|
||
| if (!this.isHeaderForListValue(headerName)) { | ||
| headers.put(headerName, headerValueToAddIn(header)); | ||
| } | ||
| else { | ||
| Object values = headers.getOrDefault(headerName, new ArrayList<>()); | ||
|
|
||
| if (values instanceof List) { | ||
|
||
| @SuppressWarnings("unchecked") | ||
| List<Object> castedValues = (List<Object>) values; | ||
| castedValues.add(headerValueToAddIn(header)); | ||
| headers.put(headerName, castedValues); | ||
| } | ||
| else { | ||
| headers.put(headerName, headerValueToAddIn(header)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private void populateJsonValueHeader(Header header, String requestedType, Map<String, Object> headers) { | ||
| Class<?> type = Object.class; | ||
| boolean trusted = false; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inline comment;
I added two caches for single-value and multi-value to optimize matching performance.
I assume that headers in
ConsumerRecordwill have lower cardinality.From this assume, we can save our CPU usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea is OK, but not perfect . It might be good to compare some performance and make sure it worth to sacrifice memory for these caches.
another optimization could be with eviction policy to avoid the case of “too many headers” and don’t keep too old headers in the cache.
with that in mind , I would suggest to consider caching as a separate bonus task.
It doesn’t feel crucial for the big picture in hands.