Skip to content

Commit c52d736

Browse files
committed
GH-1519: Fix @sendto on @KafkaHandler
Resolves #1519 An empty `@SendTo` on a `@KafkaListener` method means send the reply to the `KafkaHeaders.REPLY_TOPIC` header. This default was not applied for class-level `@KafkaListener`s. **backport to 2.4.x, 2.3.x, 2.2.x** (I will do the back ports, because I expect conflicts).
1 parent 724ac8a commit c52d736

File tree

4 files changed

+103
-16
lines changed

4 files changed

+103
-16
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2020 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.listener.adapter;
18+
19+
import org.springframework.expression.ParserContext;
20+
import org.springframework.expression.common.TemplateParserContext;
21+
import org.springframework.kafka.support.KafkaHeaders;
22+
23+
/**
24+
* Utilities for listener adapters.
25+
*
26+
* @author Gary Russell
27+
* @since 2.3.13
28+
*
29+
*/
30+
public final class AdapterUtils {
31+
32+
/**
33+
* Parser context for runtime SpEL using ! as the template prefix.
34+
* @since 2.3.13
35+
*/
36+
public static final ParserContext PARSER_CONTEXT = new TemplateParserContext("!{", "}");
37+
38+
private AdapterUtils() {
39+
}
40+
41+
/**
42+
* Return the default expression when no SendTo value is present.
43+
* @return the expression.
44+
* @since 2.3.13
45+
*/
46+
public static String getDefaultReplyTopicExpression() {
47+
return PARSER_CONTEXT.getExpressionPrefix() + "source.headers['"
48+
+ KafkaHeaders.REPLY_TOPIC + "']" + PARSER_CONTEXT.getExpressionSuffix();
49+
}
50+
51+
}

spring-kafka/src/main/java/org/springframework/kafka/listener/adapter/DelegatingInvocableHandler.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@
3333
import org.springframework.core.MethodParameter;
3434
import org.springframework.core.annotation.AnnotationUtils;
3535
import org.springframework.expression.Expression;
36-
import org.springframework.expression.ParserContext;
37-
import org.springframework.expression.common.TemplateParserContext;
3836
import org.springframework.expression.spel.standard.SpelExpressionParser;
3937
import org.springframework.kafka.KafkaException;
4038
import org.springframework.kafka.support.KafkaUtils;
@@ -59,8 +57,6 @@ public class DelegatingInvocableHandler {
5957

6058
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
6159

62-
private static final ParserContext PARSER_CONTEXT = new TemplateParserContext("!{", "}");
63-
6460
private final List<InvocableHandlerMethod> handlers;
6561

6662
private final ConcurrentMap<Class<?>, InvocableHandlerMethod> cachedHandlers = new ConcurrentHashMap<>();
@@ -178,16 +174,20 @@ protected InvocableHandlerMethod getHandlerForPayload(Class<? extends Object> pa
178174
private void setupReplyTo(InvocableHandlerMethod handler) {
179175
String replyTo = null;
180176
Method method = handler.getMethod();
177+
SendTo ann = null;
181178
if (method != null) {
182-
SendTo ann = AnnotationUtils.getAnnotation(method, SendTo.class);
179+
ann = AnnotationUtils.getAnnotation(method, SendTo.class);
183180
replyTo = extractSendTo(method.toString(), ann);
184181
}
185-
if (replyTo == null) {
186-
SendTo ann = AnnotationUtils.getAnnotation(this.bean.getClass(), SendTo.class);
182+
if (ann == null) {
183+
ann = AnnotationUtils.getAnnotation(this.bean.getClass(), SendTo.class);
187184
replyTo = extractSendTo(this.getBean().getClass().getSimpleName(), ann);
188185
}
186+
if (ann != null && replyTo == null) {
187+
replyTo = AdapterUtils.getDefaultReplyTopicExpression();
188+
}
189189
if (replyTo != null) {
190-
this.handlerSendTo.put(handler, PARSER.parseExpression(replyTo, PARSER_CONTEXT));
190+
this.handlerSendTo.put(handler, PARSER.parseExpression(replyTo, AdapterUtils.PARSER_CONTEXT));
191191
}
192192
this.handlerReturnsMessage.put(handler, KafkaUtils.returnTypeMessageOrCollectionOf(method));
193193
}

spring-kafka/src/main/java/org/springframework/kafka/listener/adapter/MessagingMessageListenerAdapter.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@
3939
import org.springframework.core.log.LogAccessor;
4040
import org.springframework.expression.BeanResolver;
4141
import org.springframework.expression.Expression;
42-
import org.springframework.expression.ParserContext;
4342
import org.springframework.expression.common.LiteralExpression;
44-
import org.springframework.expression.common.TemplateParserContext;
4543
import org.springframework.expression.spel.standard.SpelExpressionParser;
4644
import org.springframework.expression.spel.support.StandardEvaluationContext;
4745
import org.springframework.expression.spel.support.StandardTypeConverter;
@@ -84,8 +82,6 @@ public abstract class MessagingMessageListenerAdapter<K, V> implements ConsumerS
8482

8583
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
8684

87-
private static final ParserContext PARSER_CONTEXT = new TemplateParserContext("!{", "}");
88-
8985
/**
9086
* Message used when no conversion is needed.
9187
*/
@@ -199,11 +195,10 @@ public boolean isConversionNeeded() {
199195
public void setReplyTopic(String replyTopicParam) {
200196
String replyTopic = replyTopicParam;
201197
if (!StringUtils.hasText(replyTopic)) {
202-
replyTopic = PARSER_CONTEXT.getExpressionPrefix() + "source.headers['"
203-
+ KafkaHeaders.REPLY_TOPIC + "']" + PARSER_CONTEXT.getExpressionSuffix();
198+
replyTopic = AdapterUtils.getDefaultReplyTopicExpression();
204199
}
205-
if (replyTopic.contains(PARSER_CONTEXT.getExpressionPrefix())) {
206-
this.replyTopicExpression = PARSER.parseExpression(replyTopic, PARSER_CONTEXT);
200+
if (replyTopic.contains(AdapterUtils.PARSER_CONTEXT.getExpressionPrefix())) {
201+
this.replyTopicExpression = PARSER.parseExpression(replyTopic, AdapterUtils.PARSER_CONTEXT);
207202
}
208203
else {
209204
this.replyTopicExpression = new LiteralExpression(replyTopic);

spring-kafka/src/test/java/org/springframework/kafka/requestreply/ReplyingKafkaTemplateTests.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@
105105
ReplyingKafkaTemplateTests.D_REPLY, ReplyingKafkaTemplateTests.D_REQUEST,
106106
ReplyingKafkaTemplateTests.E_REPLY, ReplyingKafkaTemplateTests.E_REQUEST,
107107
ReplyingKafkaTemplateTests.F_REPLY, ReplyingKafkaTemplateTests.F_REQUEST,
108+
ReplyingKafkaTemplateTests.G_REPLY, ReplyingKafkaTemplateTests.G_REQUEST,
109+
ReplyingKafkaTemplateTests.I_REPLY, ReplyingKafkaTemplateTests.I_REQUEST,
108110
ReplyingKafkaTemplateTests.J_REPLY, ReplyingKafkaTemplateTests.J_REQUEST,
109111
ReplyingKafkaTemplateTests.K_REPLY, ReplyingKafkaTemplateTests.K_REQUEST })
110112
public class ReplyingKafkaTemplateTests {
@@ -137,6 +139,10 @@ public class ReplyingKafkaTemplateTests {
137139

138140
public static final String G_REQUEST = "gRequest";
139141

142+
public static final String I_REPLY = "iReply";
143+
144+
public static final String I_REQUEST = "iRequest";
145+
140146
public static final String J_REPLY = "jReply";
141147

142148
public static final String J_REQUEST = "jRequest";
@@ -246,6 +252,24 @@ public void testMultiListenerMessageReturn() throws Exception {
246252
}
247253
}
248254

255+
@Test
256+
public void testHandlerReturn() throws Exception {
257+
ReplyingKafkaTemplate<Integer, String, String> template = createTemplate(I_REPLY);
258+
try {
259+
template.setDefaultReplyTimeout(Duration.ofSeconds(30));
260+
ProducerRecord<Integer, String> record = new ProducerRecord<>(I_REQUEST, "foo");
261+
record.headers().add(new RecordHeader(KafkaHeaders.REPLY_TOPIC, I_REPLY.getBytes()));
262+
RequestReplyFuture<Integer, String, String> future = template.sendAndReceive(record);
263+
future.getSendFuture().get(10, TimeUnit.SECONDS); // send ok
264+
ConsumerRecord<Integer, String> consumerRecord = future.get(30, TimeUnit.SECONDS);
265+
assertThat(consumerRecord.value()).isEqualTo("FOO");
266+
}
267+
finally {
268+
template.stop();
269+
template.destroy();
270+
}
271+
}
272+
249273
@Test
250274
public void testGoodDefaultReplyHeaders() throws Exception {
251275
ReplyingKafkaTemplate<Integer, String, String> template = createTemplate(
@@ -633,6 +657,11 @@ public MultiMessageReturn mmr() {
633657
return new MultiMessageReturn();
634658
}
635659

660+
@Bean
661+
public HandlerReturn handlerReturn() {
662+
return new HandlerReturn();
663+
}
664+
636665
@KafkaListener(id = "def1", topics = { D_REQUEST, E_REQUEST, F_REQUEST })
637666
@SendTo // default REPLY_TOPIC header
638667
public String dListener1(String in) {
@@ -685,6 +714,7 @@ public Message<?> listen1(String in, @Header(KafkaHeaders.REPLY_TOPIC) byte[] re
685714

686715
}
687716

717+
688718
public static class BadDeser implements Deserializer<Object> {
689719

690720
@Override
@@ -699,4 +729,15 @@ public Object deserialize(String topic, Headers headers, byte[] data) {
699729

700730
}
701731

732+
@KafkaListener(topics = I_REQUEST, groupId = I_REQUEST)
733+
public static class HandlerReturn {
734+
735+
@KafkaHandler
736+
@SendTo
737+
public String listen1(String in) {
738+
return in.toUpperCase();
739+
}
740+
741+
}
742+
702743
}

0 commit comments

Comments
 (0)