Skip to content

Commit bd093eb

Browse files
committed
Runtime resolution of JMS reply destination
Add JmsResponse that can be used as return type of any JMS listener method to indicate not only the response but also the actual destination to which the reply should be sent. Issue: SPR-13133
1 parent 210e10c commit bd093eb

File tree

4 files changed

+370
-7
lines changed

4 files changed

+370
-7
lines changed

spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ protected void handleResult(Object result, Message request, Session session) {
242242
try {
243243
Message response = buildMessage(session, result);
244244
postProcessResponse(request, response);
245-
Destination destination = getResponseDestination(request, response, session);
245+
Destination destination = getResponseDestination(request, response, session, result);
246246
sendResponse(session, destination, response);
247247
}
248248
catch (Exception ex) {
@@ -266,21 +266,24 @@ protected void handleResult(Object result, Message request, Session session) {
266266
* @see #setMessageConverter
267267
*/
268268
protected Message buildMessage(Session session, Object result) throws JMSException {
269+
Object content = (result instanceof JmsResponse
270+
? ((JmsResponse) result).getResponse() : result);
271+
269272
MessageConverter converter = getMessageConverter();
270273
if (converter != null) {
271-
if (result instanceof org.springframework.messaging.Message) {
272-
return this.messagingMessageConverter.toMessage(result, session);
274+
if (content instanceof org.springframework.messaging.Message) {
275+
return this.messagingMessageConverter.toMessage(content, session);
273276
}
274277
else {
275-
return converter.toMessage(result, session);
278+
return converter.toMessage(content, session);
276279
}
277280
}
278281
else {
279-
if (!(result instanceof Message)) {
282+
if (!(content instanceof Message)) {
280283
throw new MessageConversionException(
281-
"No MessageConverter specified - cannot handle message [" + result + "]");
284+
"No MessageConverter specified - cannot handle message [" + content + "]");
282285
}
283-
return (Message) result;
286+
return (Message) content;
284287
}
285288
}
286289

@@ -302,6 +305,18 @@ protected void postProcessResponse(Message request, Message response) throws JMS
302305
response.setJMSCorrelationID(correlation);
303306
}
304307

308+
private Destination getResponseDestination(Message request, Message response, Session session, Object result)
309+
throws JMSException {
310+
if (result instanceof JmsResponse) {
311+
JmsResponse jmsResponse = (JmsResponse) result;
312+
Destination destination = jmsResponse.resolveDestination(getDestinationResolver(), session);
313+
if (destination != null) {
314+
return destination;
315+
}
316+
}
317+
return getResponseDestination(request, response, session);
318+
}
319+
305320
/**
306321
* Determine a response destination for the given message.
307322
* <p>The default implementation first checks the JMS Reply-To
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Copyright 2002-2015 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+
* http://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.jms.listener.adapter;
18+
19+
import javax.jms.Destination;
20+
import javax.jms.JMSException;
21+
import javax.jms.Session;
22+
23+
import org.springframework.jms.support.destination.DestinationResolver;
24+
import org.springframework.util.Assert;
25+
26+
/**
27+
* Return type of any JMS listener method used to indicate the actual response destination
28+
* alongside the response itself. Typically used when said destination needs to be
29+
* computed at runtime.
30+
* <p>
31+
* The example below sends a response with the content of the {@code result} argument to
32+
* the {@code queueOut Queue}:
33+
*
34+
* <pre class="code">
35+
* package com.acme.foo;
36+
*
37+
* public class MyService {
38+
* &#064;JmsListener
39+
* public JmsResponse process(String msg) {
40+
* // process incoming message
41+
* return JmsResponse.forQueue(result, "queueOut");
42+
* }
43+
* }</pre>
44+
*
45+
* If the destination does not need to be computed at runtime,
46+
* {@link org.springframework.messaging.handler.annotation.SendTo @SendTo} is the
47+
* recommended declarative approach.
48+
*
49+
* @author Stephane Nicoll
50+
* @since 4.2
51+
* @see org.springframework.jms.annotation.JmsListener
52+
* @see org.springframework.messaging.handler.annotation.SendTo
53+
*/
54+
public class JmsResponse {
55+
56+
private final Object response;
57+
58+
private final Object destination;
59+
60+
/**
61+
* Create a new instance
62+
* @param response the content of the result
63+
* @param destination the destination
64+
*/
65+
protected JmsResponse(Object response, Object destination) {
66+
Assert.notNull(response, "Result must not be null");
67+
this.response = response;
68+
this.destination = destination;
69+
}
70+
71+
/**
72+
* Create a {@link JmsResponse} targeting the queue with the specified name.
73+
*/
74+
public static JmsResponse forQueue(Object result, String queueName) {
75+
Assert.notNull(queueName, "Queue name must not be null");
76+
return new JmsResponse(result, new DestinationNameHolder(queueName, false));
77+
}
78+
79+
/**
80+
* Create a {@link JmsResponse} targeting the topic with the specified name.
81+
*/
82+
public static JmsResponse forTopic(Object result, String topicName) {
83+
Assert.notNull(topicName, "Topic name must not be null");
84+
return new JmsResponse(result, new DestinationNameHolder(topicName, true));
85+
}
86+
87+
/**
88+
* Create a {@link JmsResponse} targeting the specified {@link Destination}.
89+
*/
90+
public static JmsResponse forDestination(Object result, Destination destination) {
91+
Assert.notNull(destination, "Destination must not be null");
92+
return new JmsResponse(result, destination);
93+
}
94+
95+
96+
public Object getResponse() {
97+
return response;
98+
}
99+
100+
public Destination resolveDestination(DestinationResolver destinationResolver, Session session)
101+
throws JMSException {
102+
103+
if (this.destination instanceof Destination) {
104+
return (Destination) this.destination;
105+
}
106+
if (this.destination instanceof DestinationNameHolder) {
107+
DestinationNameHolder nameHolder = (DestinationNameHolder) this.destination;
108+
return destinationResolver.resolveDestinationName(session,
109+
nameHolder.destinationName, nameHolder.pubSubDomain);
110+
}
111+
return null;
112+
}
113+
114+
@Override
115+
public String toString() {
116+
return "JmsResponse{" + "response=" + this.response + ", destination=" + this.destination + '}';
117+
}
118+
119+
120+
/**
121+
* Internal class combining a destination name
122+
* and its target destination type (queue or topic).
123+
*/
124+
protected static class DestinationNameHolder {
125+
private final String destinationName;
126+
127+
private final boolean pubSubDomain;
128+
129+
public DestinationNameHolder(String destinationName, boolean pubSubDomain) {
130+
this.destinationName = destinationName;
131+
this.pubSubDomain = pubSubDomain;
132+
}
133+
134+
@Override
135+
public String toString() {
136+
return this.destinationName + "{" + "pubSubDomain=" + this.pubSubDomain + '}';
137+
}
138+
}
139+
140+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2002-2015 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+
* http://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.jms.listener.adapter;
18+
19+
import javax.jms.Destination;
20+
import javax.jms.JMSException;
21+
import javax.jms.Session;
22+
23+
import org.junit.Rule;
24+
import org.junit.Test;
25+
import org.junit.rules.ExpectedException;
26+
27+
import org.springframework.jms.support.destination.DestinationResolver;
28+
29+
import static org.junit.Assert.assertSame;
30+
import static org.mockito.BDDMockito.given;
31+
import static org.mockito.Mockito.mock;
32+
33+
/**
34+
* @author Stephane Nicoll
35+
*/
36+
public class JmsResponseTests {
37+
38+
@Rule
39+
public final ExpectedException thrown = ExpectedException.none();
40+
41+
@Test
42+
public void destinationDoesNotUseDestinationResolver() throws JMSException {
43+
Destination destination = mock(Destination.class);
44+
Destination actual = JmsResponse.forDestination("foo", destination).resolveDestination(null, null);
45+
assertSame(destination, actual);
46+
}
47+
48+
@Test
49+
public void resolveDestinationForQueue() throws JMSException {
50+
Session session = mock(Session.class);
51+
DestinationResolver destinationResolver = mock(DestinationResolver.class);
52+
Destination destination = mock(Destination.class);
53+
54+
given(destinationResolver.resolveDestinationName(session, "myQueue", false)).willReturn(destination);
55+
JmsResponse jmsResponse = JmsResponse.forQueue("foo", "myQueue");
56+
Destination actual = jmsResponse.resolveDestination(destinationResolver, session);
57+
assertSame(destination, actual);
58+
}
59+
60+
@Test
61+
public void createWithNulResponse() {
62+
thrown.expect(IllegalArgumentException.class);
63+
JmsResponse.forQueue(null, "myQueue");
64+
}
65+
66+
@Test
67+
public void createWithNullQueueName() {
68+
thrown.expect(IllegalArgumentException.class);
69+
JmsResponse.forQueue("foo", null);
70+
}
71+
72+
@Test
73+
public void createWithNullTopicName() {
74+
thrown.expect(IllegalArgumentException.class);
75+
JmsResponse.forTopic("foo", null);
76+
}
77+
78+
@Test
79+
public void createWithNulDestination() {
80+
thrown.expect(IllegalArgumentException.class);
81+
JmsResponse.forDestination("foo", null);
82+
}
83+
84+
}

0 commit comments

Comments
 (0)