Skip to content

Commit a112557

Browse files
committed
Support SendTo at class-level
Issue: SPR-13578
1 parent 73a7943 commit a112557

File tree

8 files changed

+152
-41
lines changed

8 files changed

+152
-41
lines changed

spring-jms/src/main/java/org/springframework/jms/config/MethodJmsListenerEndpoint.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 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.
@@ -157,7 +157,7 @@ protected MessagingMessageListenerAdapter createMessageListenerInstance() {
157157
*/
158158
protected String getDefaultResponseDestination() {
159159
Method specificMethod = getMostSpecificMethod();
160-
SendTo ann = AnnotationUtils.getAnnotation(specificMethod, SendTo.class);
160+
SendTo ann = getSendTo(specificMethod);
161161
if (ann != null) {
162162
Object[] destinations = ann.value();
163163
if (destinations.length != 1) {
@@ -169,6 +169,16 @@ protected String getDefaultResponseDestination() {
169169
return null;
170170
}
171171

172+
private SendTo getSendTo(Method specificMethod) {
173+
SendTo ann = AnnotationUtils.getAnnotation(specificMethod, SendTo.class);
174+
if (ann != null) {
175+
return ann;
176+
}
177+
else {
178+
return AnnotationUtils.getAnnotation(specificMethod.getDeclaringClass(), SendTo.class);
179+
}
180+
}
181+
172182
/**
173183
* Resolve the specified value if possible.
174184
* @see ConfigurableBeanFactory#resolveEmbeddedValue

spring-jms/src/test/java/org/springframework/jms/config/MethodJmsListenerEndpointTests.java

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 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.
@@ -266,7 +266,7 @@ public void processAndReplyWithSendToQueue() throws JMSException {
266266
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
267267
MessagingMessageListenerAdapter listener = createInstance(this.factory,
268268
getListenerMethod(methodName, String.class), container);
269-
processAndReplyWithSendTo(listener, false);
269+
processAndReplyWithSendTo(listener, "replyDestination", false);
270270
assertListenerMethodInvocation(sample, methodName);
271271
}
272272

@@ -278,7 +278,7 @@ public void processFromTopicAndReplyWithSendToQueue() throws JMSException {
278278
container.setReplyPubSubDomain(false);
279279
MessagingMessageListenerAdapter listener = createInstance(this.factory,
280280
getListenerMethod(methodName, String.class), container);
281-
processAndReplyWithSendTo(listener, false);
281+
processAndReplyWithSendTo(listener, "replyDestination", false);
282282
assertListenerMethodInvocation(sample, methodName);
283283
}
284284

@@ -289,7 +289,7 @@ public void processAndReplyWithSendToTopic() throws JMSException {
289289
container.setPubSubDomain(true);
290290
MessagingMessageListenerAdapter listener = createInstance(this.factory,
291291
getListenerMethod(methodName, String.class), container);
292-
processAndReplyWithSendTo(listener, true);
292+
processAndReplyWithSendTo(listener, "replyDestination", true);
293293
assertListenerMethodInvocation(sample, methodName);
294294
}
295295

@@ -300,11 +300,19 @@ public void processFromQueueAndReplyWithSendToTopic() throws JMSException {
300300
container.setReplyPubSubDomain(true);
301301
MessagingMessageListenerAdapter listener = createInstance(this.factory,
302302
getListenerMethod(methodName, String.class), container);
303-
processAndReplyWithSendTo(listener, true);
303+
processAndReplyWithSendTo(listener, "replyDestination", true);
304304
assertListenerMethodInvocation(sample, methodName);
305305
}
306306

307-
private void processAndReplyWithSendTo(MessagingMessageListenerAdapter listener, boolean pubSubDomain) throws JMSException {
307+
@Test
308+
public void processAndReplyWithDefaultSendTo() throws JMSException {
309+
MessagingMessageListenerAdapter listener = createDefaultInstance(String.class);
310+
processAndReplyWithSendTo(listener, "defaultReply", false);
311+
assertDefaultListenerMethodInvocation();
312+
}
313+
314+
private void processAndReplyWithSendTo(MessagingMessageListenerAdapter listener,
315+
String replyDestinationName, boolean pubSubDomain) throws JMSException {
308316
String body = "echo text";
309317
String correlationId = "link-1234";
310318
Destination replyDestination = new Destination() {};
@@ -314,7 +322,7 @@ private void processAndReplyWithSendTo(MessagingMessageListenerAdapter listener,
314322
QueueSender queueSender = mock(QueueSender.class);
315323
Session session = mock(Session.class);
316324

317-
given(destinationResolver.resolveDestinationName(session, "replyDestination", pubSubDomain))
325+
given(destinationResolver.resolveDestinationName(session, replyDestinationName, pubSubDomain))
318326
.willReturn(replyDestination);
319327
given(session.createTextMessage(body)).willReturn(reply);
320328
given(session.createProducer(replyDestination)).willReturn(queueSender);
@@ -324,7 +332,7 @@ private void processAndReplyWithSendTo(MessagingMessageListenerAdapter listener,
324332
inputMessage.setJMSCorrelationID(correlationId);
325333
listener.onMessage(inputMessage, session);
326334

327-
verify(destinationResolver).resolveDestinationName(session, "replyDestination", pubSubDomain);
335+
verify(destinationResolver).resolveDestinationName(session, replyDestinationName, pubSubDomain);
328336
verify(reply).setJMSCorrelationID(correlationId);
329337
verify(queueSender).send(reply);
330338
verify(queueSender).close();
@@ -470,6 +478,7 @@ private Method getTestMethod() {
470478
}
471479

472480

481+
@SendTo("defaultReply")
473482
static class JmsEndpointSampleBean {
474483

475484
private final Map<String, Boolean> invocations = new HashMap<String, Boolean>();
@@ -549,6 +558,11 @@ public String processAndReplyWithSendTo(String content) {
549558
return content;
550559
}
551560

561+
public String processAndReplyWithDefaultSendTo(String content) {
562+
invocations.put("processAndReplyWithDefaultSendTo", true);
563+
return content;
564+
}
565+
552566
@SendTo("")
553567
public String emptySendTo(String content) {
554568
invocations.put("emptySendTo", true);

spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/SendTo.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2016 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.
@@ -32,10 +32,15 @@
3232
* convey the destination to use for the reply. In that case, that destination
3333
* should take precedence.
3434
*
35+
* <p>The annotation may also be placed at class-level if the provider supports
36+
* it to indicate that all related methods should use this destination if none
37+
* is specified otherwise.
38+
*
3539
* @author Rossen Stoyanchev
40+
* @author Stephane Nicoll
3641
* @since 4.0
3742
*/
38-
@Target(ElementType.METHOD)
43+
@Target({ElementType.METHOD, ElementType.TYPE})
3944
@Retention(RetentionPolicy.RUNTIME)
4045
@Documented
4146
public @interface SendTo {

spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 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.
@@ -133,6 +133,7 @@ public MessageHeaderInitializer getHeaderInitializer() {
133133
@Override
134134
public boolean supportsReturnType(MethodParameter returnType) {
135135
if (returnType.getMethodAnnotation(SendTo.class) != null ||
136+
AnnotationUtils.getAnnotation(returnType.getDeclaringClass(), SendTo.class) != null ||
136137
returnType.getMethodAnnotation(SendToUser.class) != null) {
137138
return true;
138139
}
@@ -174,7 +175,7 @@ public void handleReturnValue(Object returnValue, MethodParameter returnType, Me
174175
}
175176
}
176177
else {
177-
SendTo sendTo = returnType.getMethodAnnotation(SendTo.class);
178+
SendTo sendTo = getSendTo(returnType);
178179
String[] destinations = getTargetDestinations(sendTo, message, this.defaultDestinationPrefix);
179180
for (String destination : destinations) {
180181
destination = this.placeholderHelper.replacePlaceholders(destination, varResolver);
@@ -183,6 +184,16 @@ public void handleReturnValue(Object returnValue, MethodParameter returnType, Me
183184
}
184185
}
185186

187+
private SendTo getSendTo(MethodParameter returnType) {
188+
SendTo sendTo = returnType.getMethodAnnotation(SendTo.class);
189+
if (sendTo != null && !ObjectUtils.isEmpty((sendTo.value()))) {
190+
return sendTo;
191+
}
192+
else {
193+
return AnnotationUtils.getAnnotation(returnType.getDeclaringClass(), SendTo.class);
194+
}
195+
}
196+
186197
@SuppressWarnings("unchecked")
187198
private PlaceholderResolver initVarResolver(MessageHeaders headers) {
188199
String name = DestinationVariableMethodArgumentResolver.DESTINATION_TEMPLATE_VARIABLES_HEADER;

spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java

Lines changed: 85 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 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.
@@ -88,6 +88,9 @@ public class SendToMethodReturnValueHandlerTests {
8888
private MethodParameter sendToUserDefaultDestReturnType;
8989
private MethodParameter sendToUserSingleSessionDefaultDestReturnType;
9090
private MethodParameter jsonViewReturnType;
91+
private MethodParameter defaultNoAnnotation;
92+
private MethodParameter defaultEmptyAnnotation;
93+
private MethodParameter defaultOverrideAnnotation;
9194

9295

9396
@Before
@@ -129,6 +132,15 @@ public void setup() throws Exception {
129132

130133
method = this.getClass().getDeclaredMethod("handleAndSendToJsonView");
131134
this.jsonViewReturnType = new SynthesizingMethodParameter(method, -1);
135+
136+
method = TestBean.class.getDeclaredMethod("handleNoAnnotation");
137+
this.defaultNoAnnotation = new SynthesizingMethodParameter(method, -1);
138+
139+
method = TestBean.class.getDeclaredMethod("handleAndSendToDefaultDestination");
140+
this.defaultEmptyAnnotation = new SynthesizingMethodParameter(method, -1);
141+
142+
method = TestBean.class.getDeclaredMethod("handleAndSendToOverride");
143+
this.defaultOverrideAnnotation = new SynthesizingMethodParameter(method, -1);
132144
}
133145

134146

@@ -138,23 +150,22 @@ public void supportsReturnType() throws Exception {
138150
assertTrue(this.handler.supportsReturnType(this.sendToUserReturnType));
139151
assertFalse(this.handler.supportsReturnType(this.noAnnotationsReturnType));
140152
assertTrue(this.handlerAnnotationNotRequired.supportsReturnType(this.noAnnotationsReturnType));
153+
154+
assertTrue(this.handler.supportsReturnType(this.defaultNoAnnotation));
155+
assertTrue(this.handler.supportsReturnType(this.defaultEmptyAnnotation));
156+
assertTrue(this.handler.supportsReturnType(this.defaultOverrideAnnotation));
141157
}
142158

143159
@Test
144160
public void sendToNoAnnotations() throws Exception {
145161
given(this.messageChannel.send(any(Message.class))).willReturn(true);
146162

147-
Message<?> inputMessage = createInputMessage("sess1", "sub1", "/app", "/dest", null);
163+
String sessionId = "sess1";
164+
Message<?> inputMessage = createInputMessage(sessionId, "sub1", "/app", "/dest", null);
148165
this.handler.handleReturnValue(PAYLOAD, this.noAnnotationsReturnType, inputMessage);
149166

150167
verify(this.messageChannel, times(1)).send(this.messageCaptor.capture());
151-
152-
SimpMessageHeaderAccessor accessor = getCapturedAccessor(0);
153-
assertEquals("sess1", accessor.getSessionId());
154-
assertEquals("/topic/dest", accessor.getDestination());
155-
assertEquals(MIME_TYPE, accessor.getContentType());
156-
assertNull("Subscription id should not be copied", accessor.getSubscriptionId());
157-
assertEquals(this.noAnnotationsReturnType, accessor.getHeader(SimpMessagingTemplate.CONVERSION_HINT_HEADER));
168+
assertResponse(this.noAnnotationsReturnType, sessionId, 0, "/topic/dest");
158169
}
159170

160171
@Test
@@ -166,20 +177,8 @@ public void sendTo() throws Exception {
166177
this.handler.handleReturnValue(PAYLOAD, this.sendToReturnType, inputMessage);
167178

168179
verify(this.messageChannel, times(2)).send(this.messageCaptor.capture());
169-
170-
SimpMessageHeaderAccessor accessor = getCapturedAccessor(0);
171-
assertEquals(sessionId, accessor.getSessionId());
172-
assertEquals("/dest1", accessor.getDestination());
173-
assertEquals(MIME_TYPE, accessor.getContentType());
174-
assertNull("Subscription id should not be copied", accessor.getSubscriptionId());
175-
assertEquals(this.sendToReturnType, accessor.getHeader(SimpMessagingTemplate.CONVERSION_HINT_HEADER));
176-
177-
accessor = getCapturedAccessor(1);
178-
assertEquals(sessionId, accessor.getSessionId());
179-
assertEquals("/dest2", accessor.getDestination());
180-
assertEquals(MIME_TYPE, accessor.getContentType());
181-
assertNull("Subscription id should not be copied", accessor.getSubscriptionId());
182-
assertEquals(this.sendToReturnType, accessor.getHeader(SimpMessagingTemplate.CONVERSION_HINT_HEADER));
180+
assertResponse(this.sendToReturnType, sessionId, 0, "/dest1");
181+
assertResponse(this.sendToReturnType, sessionId, 1, "/dest2");
183182
}
184183

185184
@Test
@@ -191,13 +190,54 @@ public void sendToDefaultDestination() throws Exception {
191190
this.handler.handleReturnValue(PAYLOAD, this.sendToDefaultDestReturnType, inputMessage);
192191

193192
verify(this.messageChannel, times(1)).send(this.messageCaptor.capture());
193+
assertResponse(this.sendToDefaultDestReturnType, sessionId, 0, "/topic/dest");
194+
}
194195

195-
SimpMessageHeaderAccessor accessor = getCapturedAccessor(0);
196+
@Test
197+
public void sendToClassDefaultNoAnnotation() throws Exception {
198+
given(this.messageChannel.send(any(Message.class))).willReturn(true);
199+
200+
String sessionId = "sess1";
201+
Message<?> inputMessage = createInputMessage(sessionId, "sub1", null, null, null);
202+
this.handler.handleReturnValue(PAYLOAD, this.defaultNoAnnotation, inputMessage);
203+
204+
verify(this.messageChannel, times(1)).send(this.messageCaptor.capture());
205+
assertResponse(this.defaultNoAnnotation, sessionId, 0, "/dest-default");
206+
}
207+
208+
@Test
209+
public void sendToClassDefaultEmptyAnnotation() throws Exception {
210+
given(this.messageChannel.send(any(Message.class))).willReturn(true);
211+
212+
String sessionId = "sess1";
213+
Message<?> inputMessage = createInputMessage(sessionId, "sub1", null, null, null);
214+
this.handler.handleReturnValue(PAYLOAD, this.defaultEmptyAnnotation, inputMessage);
215+
216+
verify(this.messageChannel, times(1)).send(this.messageCaptor.capture());
217+
assertResponse(this.defaultEmptyAnnotation, sessionId, 0, "/dest-default");
218+
}
219+
220+
@Test
221+
public void sendToClassDefaultOverride() throws Exception {
222+
given(this.messageChannel.send(any(Message.class))).willReturn(true);
223+
224+
String sessionId = "sess1";
225+
Message<?> inputMessage = createInputMessage(sessionId, "sub1", null, null, null);
226+
this.handler.handleReturnValue(PAYLOAD, this.defaultOverrideAnnotation, inputMessage);
227+
228+
verify(this.messageChannel, times(2)).send(this.messageCaptor.capture());
229+
assertResponse(this.defaultOverrideAnnotation, sessionId, 0, "/dest3");
230+
assertResponse(this.defaultOverrideAnnotation, sessionId, 1, "/dest4");
231+
}
232+
233+
private void assertResponse(MethodParameter methodParameter, String sessionId,
234+
int index, String destination) {
235+
SimpMessageHeaderAccessor accessor = getCapturedAccessor(index);
196236
assertEquals(sessionId, accessor.getSessionId());
197-
assertEquals("/topic/dest", accessor.getDestination());
237+
assertEquals(destination, accessor.getDestination());
198238
assertEquals(MIME_TYPE, accessor.getContentType());
199239
assertNull("Subscription id should not be copied", accessor.getSubscriptionId());
200-
assertEquals(this.sendToDefaultDestReturnType, accessor.getHeader(SimpMessagingTemplate.CONVERSION_HINT_HEADER));
240+
assertEquals(methodParameter, accessor.getHeader(SimpMessagingTemplate.CONVERSION_HINT_HEADER));
201241
}
202242

203243
@Test
@@ -497,6 +537,25 @@ public JacksonViewBean handleAndSendToJsonView() {
497537
return payload;
498538
}
499539

540+
@SendTo("/dest-default")
541+
private static class TestBean {
542+
543+
public String handleNoAnnotation() {
544+
return PAYLOAD;
545+
}
546+
547+
@SendTo
548+
public String handleAndSendToDefaultDestination() {
549+
return PAYLOAD;
550+
}
551+
552+
@SendTo({"/dest3", "/dest4"})
553+
public String handleAndSendToOverride() {
554+
return PAYLOAD;
555+
}
556+
557+
}
558+
500559

501560
private interface MyJacksonView1 {}
502561
private interface MyJacksonView2 {}

src/asciidoc/integration.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2796,6 +2796,9 @@ as follow to automatically send a response:
27962796
}
27972797
----
27982798

2799+
TIP: If you have several `@JmsListener`-annotated methods, you can also place the `@SendTo`
2800+
annotation at class-level to share a default reply destination.
2801+
27992802
If you need to set additional headers in a transport-independent manner, you could return a
28002803
`Message` instead, something like:
28012804

src/asciidoc/web-websocket.adoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1359,7 +1359,8 @@ The return value from an `@MessageMapping` method is converted with a
13591359
of a new message that is then sent, by default, to the `"brokerChannel"` with
13601360
the same destination as the client message but using the prefix `"/topic"` by
13611361
default. An `@SendTo` message level annotation can be used to specify any
1362-
other destination instead.
1362+
other destination instead. It can also be set a class-level to share a common
1363+
destination.
13631364

13641365
An `@SubscribeMapping` annotation can also be used to map subscription requests
13651366
to `@Controller` methods. It is supported on the method level, but can also be

0 commit comments

Comments
 (0)