Browse Source

Support SendTo at class-level

Issue: SPR-13578
pull/955/head
Stephane Nicoll 9 years ago
parent
commit
a112557dc4
  1. 14
      spring-jms/src/main/java/org/springframework/jms/config/MethodJmsListenerEndpoint.java
  2. 30
      spring-jms/src/test/java/org/springframework/jms/config/MethodJmsListenerEndpointTests.java
  3. 9
      spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/SendTo.java
  4. 15
      spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java
  5. 111
      spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java
  6. 3
      src/asciidoc/integration.adoc
  7. 3
      src/asciidoc/web-websocket.adoc
  8. 8
      src/asciidoc/whats-new.adoc

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

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -157,7 +157,7 @@ public class MethodJmsListenerEndpoint extends AbstractJmsListenerEndpoint { @@ -157,7 +157,7 @@ public class MethodJmsListenerEndpoint extends AbstractJmsListenerEndpoint {
*/
protected String getDefaultResponseDestination() {
Method specificMethod = getMostSpecificMethod();
SendTo ann = AnnotationUtils.getAnnotation(specificMethod, SendTo.class);
SendTo ann = getSendTo(specificMethod);
if (ann != null) {
Object[] destinations = ann.value();
if (destinations.length != 1) {
@ -169,6 +169,16 @@ public class MethodJmsListenerEndpoint extends AbstractJmsListenerEndpoint { @@ -169,6 +169,16 @@ public class MethodJmsListenerEndpoint extends AbstractJmsListenerEndpoint {
return null;
}
private SendTo getSendTo(Method specificMethod) {
SendTo ann = AnnotationUtils.getAnnotation(specificMethod, SendTo.class);
if (ann != null) {
return ann;
}
else {
return AnnotationUtils.getAnnotation(specificMethod.getDeclaringClass(), SendTo.class);
}
}
/**
* Resolve the specified value if possible.
* @see ConfigurableBeanFactory#resolveEmbeddedValue

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

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -266,7 +266,7 @@ public class MethodJmsListenerEndpointTests { @@ -266,7 +266,7 @@ public class MethodJmsListenerEndpointTests {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
MessagingMessageListenerAdapter listener = createInstance(this.factory,
getListenerMethod(methodName, String.class), container);
processAndReplyWithSendTo(listener, false);
processAndReplyWithSendTo(listener, "replyDestination", false);
assertListenerMethodInvocation(sample, methodName);
}
@ -278,7 +278,7 @@ public class MethodJmsListenerEndpointTests { @@ -278,7 +278,7 @@ public class MethodJmsListenerEndpointTests {
container.setReplyPubSubDomain(false);
MessagingMessageListenerAdapter listener = createInstance(this.factory,
getListenerMethod(methodName, String.class), container);
processAndReplyWithSendTo(listener, false);
processAndReplyWithSendTo(listener, "replyDestination", false);
assertListenerMethodInvocation(sample, methodName);
}
@ -289,7 +289,7 @@ public class MethodJmsListenerEndpointTests { @@ -289,7 +289,7 @@ public class MethodJmsListenerEndpointTests {
container.setPubSubDomain(true);
MessagingMessageListenerAdapter listener = createInstance(this.factory,
getListenerMethod(methodName, String.class), container);
processAndReplyWithSendTo(listener, true);
processAndReplyWithSendTo(listener, "replyDestination", true);
assertListenerMethodInvocation(sample, methodName);
}
@ -300,11 +300,19 @@ public class MethodJmsListenerEndpointTests { @@ -300,11 +300,19 @@ public class MethodJmsListenerEndpointTests {
container.setReplyPubSubDomain(true);
MessagingMessageListenerAdapter listener = createInstance(this.factory,
getListenerMethod(methodName, String.class), container);
processAndReplyWithSendTo(listener, true);
processAndReplyWithSendTo(listener, "replyDestination", true);
assertListenerMethodInvocation(sample, methodName);
}
private void processAndReplyWithSendTo(MessagingMessageListenerAdapter listener, boolean pubSubDomain) throws JMSException {
@Test
public void processAndReplyWithDefaultSendTo() throws JMSException {
MessagingMessageListenerAdapter listener = createDefaultInstance(String.class);
processAndReplyWithSendTo(listener, "defaultReply", false);
assertDefaultListenerMethodInvocation();
}
private void processAndReplyWithSendTo(MessagingMessageListenerAdapter listener,
String replyDestinationName, boolean pubSubDomain) throws JMSException {
String body = "echo text";
String correlationId = "link-1234";
Destination replyDestination = new Destination() {};
@ -314,7 +322,7 @@ public class MethodJmsListenerEndpointTests { @@ -314,7 +322,7 @@ public class MethodJmsListenerEndpointTests {
QueueSender queueSender = mock(QueueSender.class);
Session session = mock(Session.class);
given(destinationResolver.resolveDestinationName(session, "replyDestination", pubSubDomain))
given(destinationResolver.resolveDestinationName(session, replyDestinationName, pubSubDomain))
.willReturn(replyDestination);
given(session.createTextMessage(body)).willReturn(reply);
given(session.createProducer(replyDestination)).willReturn(queueSender);
@ -324,7 +332,7 @@ public class MethodJmsListenerEndpointTests { @@ -324,7 +332,7 @@ public class MethodJmsListenerEndpointTests {
inputMessage.setJMSCorrelationID(correlationId);
listener.onMessage(inputMessage, session);
verify(destinationResolver).resolveDestinationName(session, "replyDestination", pubSubDomain);
verify(destinationResolver).resolveDestinationName(session, replyDestinationName, pubSubDomain);
verify(reply).setJMSCorrelationID(correlationId);
verify(queueSender).send(reply);
verify(queueSender).close();
@ -470,6 +478,7 @@ public class MethodJmsListenerEndpointTests { @@ -470,6 +478,7 @@ public class MethodJmsListenerEndpointTests {
}
@SendTo("defaultReply")
static class JmsEndpointSampleBean {
private final Map<String, Boolean> invocations = new HashMap<String, Boolean>();
@ -549,6 +558,11 @@ public class MethodJmsListenerEndpointTests { @@ -549,6 +558,11 @@ public class MethodJmsListenerEndpointTests {
return content;
}
public String processAndReplyWithDefaultSendTo(String content) {
invocations.put("processAndReplyWithDefaultSendTo", true);
return content;
}
@SendTo("")
public String emptySendTo(String content) {
invocations.put("emptySendTo", true);

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

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -32,10 +32,15 @@ import org.springframework.messaging.Message; @@ -32,10 +32,15 @@ import org.springframework.messaging.Message;
* convey the destination to use for the reply. In that case, that destination
* should take precedence.
*
* <p>The annotation may also be placed at class-level if the provider supports
* it to indicate that all related methods should use this destination if none
* is specified otherwise.
*
* @author Rossen Stoyanchev
* @author Stephane Nicoll
* @since 4.0
*/
@Target(ElementType.METHOD)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SendTo {

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

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -133,6 +133,7 @@ public class SendToMethodReturnValueHandler implements HandlerMethodReturnValueH @@ -133,6 +133,7 @@ public class SendToMethodReturnValueHandler implements HandlerMethodReturnValueH
@Override
public boolean supportsReturnType(MethodParameter returnType) {
if (returnType.getMethodAnnotation(SendTo.class) != null ||
AnnotationUtils.getAnnotation(returnType.getDeclaringClass(), SendTo.class) != null ||
returnType.getMethodAnnotation(SendToUser.class) != null) {
return true;
}
@ -174,7 +175,7 @@ public class SendToMethodReturnValueHandler implements HandlerMethodReturnValueH @@ -174,7 +175,7 @@ public class SendToMethodReturnValueHandler implements HandlerMethodReturnValueH
}
}
else {
SendTo sendTo = returnType.getMethodAnnotation(SendTo.class);
SendTo sendTo = getSendTo(returnType);
String[] destinations = getTargetDestinations(sendTo, message, this.defaultDestinationPrefix);
for (String destination : destinations) {
destination = this.placeholderHelper.replacePlaceholders(destination, varResolver);
@ -183,6 +184,16 @@ public class SendToMethodReturnValueHandler implements HandlerMethodReturnValueH @@ -183,6 +184,16 @@ public class SendToMethodReturnValueHandler implements HandlerMethodReturnValueH
}
}
private SendTo getSendTo(MethodParameter returnType) {
SendTo sendTo = returnType.getMethodAnnotation(SendTo.class);
if (sendTo != null && !ObjectUtils.isEmpty((sendTo.value()))) {
return sendTo;
}
else {
return AnnotationUtils.getAnnotation(returnType.getDeclaringClass(), SendTo.class);
}
}
@SuppressWarnings("unchecked")
private PlaceholderResolver initVarResolver(MessageHeaders headers) {
String name = DestinationVariableMethodArgumentResolver.DESTINATION_TEMPLATE_VARIABLES_HEADER;

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

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -88,6 +88,9 @@ public class SendToMethodReturnValueHandlerTests { @@ -88,6 +88,9 @@ public class SendToMethodReturnValueHandlerTests {
private MethodParameter sendToUserDefaultDestReturnType;
private MethodParameter sendToUserSingleSessionDefaultDestReturnType;
private MethodParameter jsonViewReturnType;
private MethodParameter defaultNoAnnotation;
private MethodParameter defaultEmptyAnnotation;
private MethodParameter defaultOverrideAnnotation;
@Before
@ -129,6 +132,15 @@ public class SendToMethodReturnValueHandlerTests { @@ -129,6 +132,15 @@ public class SendToMethodReturnValueHandlerTests {
method = this.getClass().getDeclaredMethod("handleAndSendToJsonView");
this.jsonViewReturnType = new SynthesizingMethodParameter(method, -1);
method = TestBean.class.getDeclaredMethod("handleNoAnnotation");
this.defaultNoAnnotation = new SynthesizingMethodParameter(method, -1);
method = TestBean.class.getDeclaredMethod("handleAndSendToDefaultDestination");
this.defaultEmptyAnnotation = new SynthesizingMethodParameter(method, -1);
method = TestBean.class.getDeclaredMethod("handleAndSendToOverride");
this.defaultOverrideAnnotation = new SynthesizingMethodParameter(method, -1);
}
@ -138,23 +150,22 @@ public class SendToMethodReturnValueHandlerTests { @@ -138,23 +150,22 @@ public class SendToMethodReturnValueHandlerTests {
assertTrue(this.handler.supportsReturnType(this.sendToUserReturnType));
assertFalse(this.handler.supportsReturnType(this.noAnnotationsReturnType));
assertTrue(this.handlerAnnotationNotRequired.supportsReturnType(this.noAnnotationsReturnType));
assertTrue(this.handler.supportsReturnType(this.defaultNoAnnotation));
assertTrue(this.handler.supportsReturnType(this.defaultEmptyAnnotation));
assertTrue(this.handler.supportsReturnType(this.defaultOverrideAnnotation));
}
@Test
public void sendToNoAnnotations() throws Exception {
given(this.messageChannel.send(any(Message.class))).willReturn(true);
Message<?> inputMessage = createInputMessage("sess1", "sub1", "/app", "/dest", null);
String sessionId = "sess1";
Message<?> inputMessage = createInputMessage(sessionId, "sub1", "/app", "/dest", null);
this.handler.handleReturnValue(PAYLOAD, this.noAnnotationsReturnType, inputMessage);
verify(this.messageChannel, times(1)).send(this.messageCaptor.capture());
SimpMessageHeaderAccessor accessor = getCapturedAccessor(0);
assertEquals("sess1", accessor.getSessionId());
assertEquals("/topic/dest", accessor.getDestination());
assertEquals(MIME_TYPE, accessor.getContentType());
assertNull("Subscription id should not be copied", accessor.getSubscriptionId());
assertEquals(this.noAnnotationsReturnType, accessor.getHeader(SimpMessagingTemplate.CONVERSION_HINT_HEADER));
assertResponse(this.noAnnotationsReturnType, sessionId, 0, "/topic/dest");
}
@Test
@ -166,20 +177,8 @@ public class SendToMethodReturnValueHandlerTests { @@ -166,20 +177,8 @@ public class SendToMethodReturnValueHandlerTests {
this.handler.handleReturnValue(PAYLOAD, this.sendToReturnType, inputMessage);
verify(this.messageChannel, times(2)).send(this.messageCaptor.capture());
SimpMessageHeaderAccessor accessor = getCapturedAccessor(0);
assertEquals(sessionId, accessor.getSessionId());
assertEquals("/dest1", accessor.getDestination());
assertEquals(MIME_TYPE, accessor.getContentType());
assertNull("Subscription id should not be copied", accessor.getSubscriptionId());
assertEquals(this.sendToReturnType, accessor.getHeader(SimpMessagingTemplate.CONVERSION_HINT_HEADER));
accessor = getCapturedAccessor(1);
assertEquals(sessionId, accessor.getSessionId());
assertEquals("/dest2", accessor.getDestination());
assertEquals(MIME_TYPE, accessor.getContentType());
assertNull("Subscription id should not be copied", accessor.getSubscriptionId());
assertEquals(this.sendToReturnType, accessor.getHeader(SimpMessagingTemplate.CONVERSION_HINT_HEADER));
assertResponse(this.sendToReturnType, sessionId, 0, "/dest1");
assertResponse(this.sendToReturnType, sessionId, 1, "/dest2");
}
@Test
@ -191,13 +190,54 @@ public class SendToMethodReturnValueHandlerTests { @@ -191,13 +190,54 @@ public class SendToMethodReturnValueHandlerTests {
this.handler.handleReturnValue(PAYLOAD, this.sendToDefaultDestReturnType, inputMessage);
verify(this.messageChannel, times(1)).send(this.messageCaptor.capture());
assertResponse(this.sendToDefaultDestReturnType, sessionId, 0, "/topic/dest");
}
SimpMessageHeaderAccessor accessor = getCapturedAccessor(0);
@Test
public void sendToClassDefaultNoAnnotation() throws Exception {
given(this.messageChannel.send(any(Message.class))).willReturn(true);
String sessionId = "sess1";
Message<?> inputMessage = createInputMessage(sessionId, "sub1", null, null, null);
this.handler.handleReturnValue(PAYLOAD, this.defaultNoAnnotation, inputMessage);
verify(this.messageChannel, times(1)).send(this.messageCaptor.capture());
assertResponse(this.defaultNoAnnotation, sessionId, 0, "/dest-default");
}
@Test
public void sendToClassDefaultEmptyAnnotation() throws Exception {
given(this.messageChannel.send(any(Message.class))).willReturn(true);
String sessionId = "sess1";
Message<?> inputMessage = createInputMessage(sessionId, "sub1", null, null, null);
this.handler.handleReturnValue(PAYLOAD, this.defaultEmptyAnnotation, inputMessage);
verify(this.messageChannel, times(1)).send(this.messageCaptor.capture());
assertResponse(this.defaultEmptyAnnotation, sessionId, 0, "/dest-default");
}
@Test
public void sendToClassDefaultOverride() throws Exception {
given(this.messageChannel.send(any(Message.class))).willReturn(true);
String sessionId = "sess1";
Message<?> inputMessage = createInputMessage(sessionId, "sub1", null, null, null);
this.handler.handleReturnValue(PAYLOAD, this.defaultOverrideAnnotation, inputMessage);
verify(this.messageChannel, times(2)).send(this.messageCaptor.capture());
assertResponse(this.defaultOverrideAnnotation, sessionId, 0, "/dest3");
assertResponse(this.defaultOverrideAnnotation, sessionId, 1, "/dest4");
}
private void assertResponse(MethodParameter methodParameter, String sessionId,
int index, String destination) {
SimpMessageHeaderAccessor accessor = getCapturedAccessor(index);
assertEquals(sessionId, accessor.getSessionId());
assertEquals("/topic/dest", accessor.getDestination());
assertEquals(destination, accessor.getDestination());
assertEquals(MIME_TYPE, accessor.getContentType());
assertNull("Subscription id should not be copied", accessor.getSubscriptionId());
assertEquals(this.sendToDefaultDestReturnType, accessor.getHeader(SimpMessagingTemplate.CONVERSION_HINT_HEADER));
assertEquals(methodParameter, accessor.getHeader(SimpMessagingTemplate.CONVERSION_HINT_HEADER));
}
@Test
@ -497,6 +537,25 @@ public class SendToMethodReturnValueHandlerTests { @@ -497,6 +537,25 @@ public class SendToMethodReturnValueHandlerTests {
return payload;
}
@SendTo("/dest-default")
private static class TestBean {
public String handleNoAnnotation() {
return PAYLOAD;
}
@SendTo
public String handleAndSendToDefaultDestination() {
return PAYLOAD;
}
@SendTo({"/dest3", "/dest4"})
public String handleAndSendToOverride() {
return PAYLOAD;
}
}
private interface MyJacksonView1 {}
private interface MyJacksonView2 {}

3
src/asciidoc/integration.adoc

@ -2796,6 +2796,9 @@ as follow to automatically send a response: @@ -2796,6 +2796,9 @@ as follow to automatically send a response:
}
----
TIP: If you have several `@JmsListener`-annotated methods, you can also place the `@SendTo`
annotation at class-level to share a default reply destination.
If you need to set additional headers in a transport-independent manner, you could return a
`Message` instead, something like:

3
src/asciidoc/web-websocket.adoc

@ -1359,7 +1359,8 @@ The return value from an `@MessageMapping` method is converted with a @@ -1359,7 +1359,8 @@ The return value from an `@MessageMapping` method is converted with a
of a new message that is then sent, by default, to the `"brokerChannel"` with
the same destination as the client message but using the prefix `"/topic"` by
default. An `@SendTo` message level annotation can be used to specify any
other destination instead.
other destination instead. It can also be set a class-level to share a common
destination.
An `@SubscribeMapping` annotation can also be used to map subscription requests
to `@Controller` methods. It is supported on the method level, but can also be

8
src/asciidoc/whats-new.adoc

@ -650,12 +650,20 @@ Spring 4.3 also improves the caching abstraction as follows: @@ -650,12 +650,20 @@ Spring 4.3 also improves the caching abstraction as follows:
* `ConcurrentMapCacheManager` and `ConcurrentMapCache` now support the serialization
of cache entries via a new `storeByValue` attribute.
=== JMS Improvements
* `@SendTo` can now be specified at class-level to share a common reply destination.
=== Web Improvements
* New `@RestControllerAdvice` annotation combines `@ControllerAdvice` with `@ResponseBody`.
* `@ResponseStatus` can be used on a controller type is inherited for all method.
* `AsyncRestTemplate` supports request interception.
=== WebSocket Messaging Improvements
* `@SendTo` can now be specified at class-level to share a common destination.
=== Testing Improvements
* The JUnit support in the _Spring TestContext Framework_ now requires JUnit 4.12 or higher.

Loading…
Cancel
Save