diff --git a/spring-jms/src/main/java/org/springframework/jms/config/MethodJmsListenerEndpoint.java b/spring-jms/src/main/java/org/springframework/jms/config/MethodJmsListenerEndpoint.java index 8bde24d028..2b80bf2850 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/MethodJmsListenerEndpoint.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/MethodJmsListenerEndpoint.java @@ -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 { */ 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 { 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 diff --git a/spring-jms/src/test/java/org/springframework/jms/config/MethodJmsListenerEndpointTests.java b/spring-jms/src/test/java/org/springframework/jms/config/MethodJmsListenerEndpointTests.java index c87711073c..dd76d590b6 100644 --- a/spring-jms/src/test/java/org/springframework/jms/config/MethodJmsListenerEndpointTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/config/MethodJmsListenerEndpointTests.java @@ -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 { 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 { 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 { 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 { 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 { 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 { 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 { } + @SendTo("defaultReply") static class JmsEndpointSampleBean { private final Map invocations = new HashMap(); @@ -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); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/SendTo.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/SendTo.java index ee4ce4152e..0a5a3f2753 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/SendTo.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/SendTo.java @@ -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; * convey the destination to use for the reply. In that case, that destination * should take precedence. * + *

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 { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java index 0f18a56718..227844d9dd 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java @@ -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 @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 } } 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 } } + 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; diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java index 277f770130..965f70baea 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandlerTests.java @@ -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 { 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 { 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 { 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 { 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 { 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 { 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 {} diff --git a/src/asciidoc/integration.adoc b/src/asciidoc/integration.adoc index 5120f82e4e..337b582ba3 100644 --- a/src/asciidoc/integration.adoc +++ b/src/asciidoc/integration.adoc @@ -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: diff --git a/src/asciidoc/web-websocket.adoc b/src/asciidoc/web-websocket.adoc index 9a2b8087dc..dfeb28df69 100644 --- a/src/asciidoc/web-websocket.adoc +++ b/src/asciidoc/web-websocket.adoc @@ -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 diff --git a/src/asciidoc/whats-new.adoc b/src/asciidoc/whats-new.adoc index 59b95d88ea..54e050b7bd 100644 --- a/src/asciidoc/whats-new.adoc +++ b/src/asciidoc/whats-new.adoc @@ -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.