Introduce SimpleDestinationResolver as new default for common setups

Closes gh-35456
This commit is contained in:
Juergen Hoeller 2025-10-02 18:28:59 +02:00
parent c5a79fdf99
commit 1333669e2c
10 changed files with 238 additions and 30 deletions

View File

@ -86,6 +86,8 @@ public interface JmsClient {
* Provide an operation handle for the given JMS destination.
* @param destination the JMS {@code Destination} object
* @return a reusable operation handle bound to the destination
* @see jakarta.jms.Queue
* @see jakarta.jms.Topic
*/
OperationSpec destination(Destination destination);
@ -93,7 +95,8 @@ public interface JmsClient {
* Provide an operation handle for the specified JMS destination.
* @param destinationName a name resolving to a JMS {@code Destination}
* @return a reusable operation handle bound to the destination
* @see org.springframework.jms.support.destination.DestinationResolver
* @see org.springframework.jms.support.destination.SimpleDestinationResolver
* @see JmsTemplate#setDestinationResolver
*/
OperationSpec destination(String destinationName);
@ -138,6 +141,7 @@ public interface JmsClient {
return new DefaultJmsClientBuilder(jmsTemplate);
}
/**
* A mutable builder for creating a {@link JmsClient}.
*/
@ -163,7 +167,6 @@ public interface JmsClient {
* Build the {@code JmsClient} instance.
*/
JmsClient build();
}

View File

@ -60,7 +60,7 @@ import org.springframework.util.ClassUtils;
* the "sessionTransacted" and "sessionAcknowledgeMode" bean properties.
*
* <p>This template uses a
* {@link org.springframework.jms.support.destination.DynamicDestinationResolver}
* {@link org.springframework.jms.support.destination.SimpleDestinationResolver}
* and a {@link org.springframework.jms.support.converter.SimpleMessageConverter}
* as default strategies for resolving a destination name or converting a message,
* respectively. These defaults can be overridden through the "destinationResolver"

View File

@ -39,7 +39,7 @@ import org.springframework.jms.support.converter.MessagingMessageConverter;
import org.springframework.jms.support.converter.SimpleMessageConverter;
import org.springframework.jms.support.converter.SmartMessageConverter;
import org.springframework.jms.support.destination.DestinationResolver;
import org.springframework.jms.support.destination.DynamicDestinationResolver;
import org.springframework.jms.support.destination.SimpleDestinationResolver;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
@ -61,7 +61,7 @@ public abstract class AbstractAdaptableMessageListener
private @Nullable Object defaultResponseDestination;
private DestinationResolver destinationResolver = new DynamicDestinationResolver();
private DestinationResolver destinationResolver = new SimpleDestinationResolver();
private @Nullable MessageConverter messageConverter = new SimpleMessageConverter();
@ -113,9 +113,9 @@ public abstract class AbstractAdaptableMessageListener
/**
* Set the DestinationResolver that should be used to resolve response
* destination names for this adapter.
* <p>The default resolver is a DynamicDestinationResolver. Specify a
* <p>The default resolver is a SimpleDestinationResolver. Specify a
* JndiDestinationResolver for resolving destination names as JNDI locations.
* @see org.springframework.jms.support.destination.DynamicDestinationResolver
* @see org.springframework.jms.support.destination.SimpleDestinationResolver
* @see org.springframework.jms.support.destination.JndiDestinationResolver
*/
public void setDestinationResolver(DestinationResolver destinationResolver) {

View File

@ -86,7 +86,8 @@ public class StandardJmsActivationSpecFactory implements JmsActivationSpecFactor
* able to work <i>without</i> an active JMS Session: for example,
* {@link org.springframework.jms.support.destination.JndiDestinationResolver}
* or {@link org.springframework.jms.support.destination.BeanFactoryDestinationResolver}
* but not {@link org.springframework.jms.support.destination.DynamicDestinationResolver}.
* but not {@link org.springframework.jms.support.destination.SimpleDestinationResolver}
* or {@link org.springframework.jms.support.destination.DynamicDestinationResolver}.
*/
public void setDestinationResolver(@Nullable DestinationResolver destinationResolver) {
this.destinationResolver = destinationResolver;

View File

@ -30,13 +30,13 @@ import org.jspecify.annotations.Nullable;
*
* <p>The default {@link DestinationResolver} implementation used by
* {@link org.springframework.jms.core.JmsTemplate} instances is the
* {@link DynamicDestinationResolver} class. Consider using the
* {@link SimpleDestinationResolver} class. Consider using the
* {@link JndiDestinationResolver} for more advanced scenarios.
*
* @author Juergen Hoeller
* @since 1.1
* @see org.springframework.jms.core.JmsTemplate#setDestinationResolver
* @see org.springframework.jms.support.destination.DynamicDestinationResolver
* @see org.springframework.jms.support.destination.SimpleDestinationResolver
* @see org.springframework.jms.support.destination.JndiDestinationResolver
*/
@FunctionalInterface

View File

@ -26,11 +26,12 @@ import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
* Simple {@link DestinationResolver} implementation resolving destination names
* as dynamic destinations.
* A basic {@link DestinationResolver} implementation freshly resolving
* destination names as dynamic destinations against a given {@link Session}.
*
* @author Juergen Hoeller
* @since 1.1
* @see SimpleDestinationResolver
* @see jakarta.jms.Session#createQueue
* @see jakarta.jms.Session#createTopic
*/

View File

@ -55,7 +55,7 @@ public abstract class JmsDestinationAccessor extends JmsAccessor {
public static final long RECEIVE_TIMEOUT_INDEFINITE_WAIT = 0;
private DestinationResolver destinationResolver = new DynamicDestinationResolver();
private DestinationResolver destinationResolver = new SimpleDestinationResolver();
private boolean pubSubDomain = false;
@ -63,9 +63,9 @@ public abstract class JmsDestinationAccessor extends JmsAccessor {
/**
* Set the {@link DestinationResolver} that is to be used to resolve
* {@link jakarta.jms.Destination} references for this accessor.
* <p>The default resolver is a DynamicDestinationResolver. Specify a
* <p>The default resolver is a SimpleDestinationResolver. Specify a
* JndiDestinationResolver for resolving destination names as JNDI locations.
* @see org.springframework.jms.support.destination.DynamicDestinationResolver
* @see org.springframework.jms.support.destination.SimpleDestinationResolver
* @see org.springframework.jms.support.destination.JndiDestinationResolver
*/
public void setDestinationResolver(DestinationResolver destinationResolver) {

View File

@ -0,0 +1,85 @@
/*
* Copyright 2002-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.jms.support.destination;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import jakarta.jms.JMSException;
import jakarta.jms.Queue;
import jakarta.jms.Session;
import jakarta.jms.Topic;
/**
* A simple {@link DestinationResolver} implementation for {@link Session}-based
* destination resolution, caching {@link Queue} and {@link Topic} instances per
* queue/topic name. In that sense, the destinations themselves also need to be
* "simple": not Session-specific and therefore stable across an entire JMS setup.
*
* <p>This is the default resolver used by {@link org.springframework.jms.core.JmsClient}
* and also {@link org.springframework.jms.core.JmsTemplate} and listener containers,
* as of 7.0. For enforcing fresh resolution on every call, you may explicitly set a
* {@link DynamicDestinationResolver} instead.
*
* @author Juergen Hoeller
* @since 7.0
* @see DynamicDestinationResolver
* @see jakarta.jms.Session#createQueue
* @see jakarta.jms.Session#createTopic
*/
public class SimpleDestinationResolver extends DynamicDestinationResolver implements CachingDestinationResolver {
private final Map<String, Topic> topicCache = new ConcurrentHashMap<>(4);
private final Map<String, Queue> queueCache = new ConcurrentHashMap<>(4);
@Override
protected Topic resolveTopic(Session session, String topicName) throws JMSException {
Topic topic = this.topicCache.get(topicName);
if (topic != null) {
return topic;
}
topic = super.resolveTopic(session, topicName);
Topic existing = this.topicCache.putIfAbsent(topicName, topic);
return (existing != null ? existing : topic);
}
@Override
protected Queue resolveQueue(Session session, String queueName) throws JMSException {
Queue queue = this.queueCache.get(queueName);
if (queue != null) {
return queue;
}
queue = super.resolveQueue(session, queueName);
Queue existing = this.queueCache.putIfAbsent(queueName, queue);
return (existing != null ? existing : queue);
}
@Override
public void removeFromCache(String destinationName) {
this.topicCache.remove(destinationName);
this.queueCache.remove(destinationName);
}
@Override
public void clearCache() {
this.topicCache.clear();
this.queueCache.clear();
}
}

View File

@ -34,49 +34,67 @@ import static org.mockito.Mockito.mock;
/**
* @author Rick Evans
* @author Juergen Hoeller
*/
class DynamicDestinationResolverTests {
private static final String DESTINATION_NAME = "foo";
private final DynamicDestinationResolver resolver = new DynamicDestinationResolver();
@Test
void resolveWithPubSubTopicSession() throws Exception {
Topic expectedDestination = new StubTopic();
Topic expectedDestination1 = new StubTopic();
Topic expectedDestination2 = new StubTopic();
TopicSession session = mock();
given(session.createTopic(DESTINATION_NAME)).willReturn(expectedDestination);
testResolveDestination(session, expectedDestination, true);
given(session.createTopic(DESTINATION_NAME)).willReturn(expectedDestination1);
testResolveDestination(session, expectedDestination1, true);
given(session.createTopic(DESTINATION_NAME)).willReturn(expectedDestination2);
testResolveDestination(session, expectedDestination2, true);
}
@Test
void resolveWithPubSubVanillaSession() throws Exception {
Topic expectedDestination = new StubTopic();
Topic expectedDestination1 = new StubTopic();
Topic expectedDestination2 = new StubTopic();
Session session = mock();
given(session.createTopic(DESTINATION_NAME)).willReturn(expectedDestination);
testResolveDestination(session, expectedDestination, true);
given(session.createTopic(DESTINATION_NAME)).willReturn(expectedDestination1);
testResolveDestination(session, expectedDestination1, true);
given(session.createTopic(DESTINATION_NAME)).willReturn(expectedDestination2);
testResolveDestination(session, expectedDestination2, true);
}
@Test
void resolveWithPointToPointQueueSession() throws Exception {
Queue expectedDestination = new StubQueue();
Queue expectedDestination1 = new StubQueue();
Queue expectedDestination2 = new StubQueue();
QueueSession session = mock();
given(session.createQueue(DESTINATION_NAME)).willReturn(expectedDestination);
testResolveDestination(session, expectedDestination, false);
given(session.createQueue(DESTINATION_NAME)).willReturn(expectedDestination1);
testResolveDestination(session, expectedDestination1, false);
given(session.createQueue(DESTINATION_NAME)).willReturn(expectedDestination2);
testResolveDestination(session, expectedDestination2, false);
}
@Test
void resolveWithPointToPointVanillaSession() throws Exception {
Queue expectedDestination = new StubQueue();
Queue expectedDestination1 = new StubQueue();
Queue expectedDestination2 = new StubQueue();
Session session = mock();
given(session.createQueue(DESTINATION_NAME)).willReturn(expectedDestination);
testResolveDestination(session, expectedDestination, false);
given(session.createQueue(DESTINATION_NAME)).willReturn(expectedDestination1);
testResolveDestination(session, expectedDestination1, false);
given(session.createQueue(DESTINATION_NAME)).willReturn(expectedDestination2);
testResolveDestination(session, expectedDestination2, false);
}
private static void testResolveDestination(Session session, Destination expectedDestination, boolean isPubSub) throws JMSException {
DynamicDestinationResolver resolver = new DynamicDestinationResolver();
private void testResolveDestination(Session session, Destination expected, boolean isPubSub) throws JMSException {
Destination destination = resolver.resolveDestinationName(session, DESTINATION_NAME, isPubSub);
assertThat(destination).isNotNull();
assertThat(destination).isSameAs(expectedDestination);
assertThat(destination).isSameAs(expected);
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright 2002-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.jms.support.destination;
import jakarta.jms.Destination;
import jakarta.jms.JMSException;
import jakarta.jms.Queue;
import jakarta.jms.QueueSession;
import jakarta.jms.Session;
import jakarta.jms.Topic;
import jakarta.jms.TopicSession;
import org.junit.jupiter.api.Test;
import org.springframework.jms.StubQueue;
import org.springframework.jms.StubTopic;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* @author Juergen Hoeller
* @since 7.0
*/
class SimpleDestinationResolverTests {
private static final String DESTINATION_NAME = "foo";
private final SimpleDestinationResolver resolver = new SimpleDestinationResolver();
@Test
void resolveWithPubSubTopicSession() throws Exception {
Topic expectedDestination1 = new StubTopic();
Topic expectedDestination2 = new StubTopic();
TopicSession session = mock();
given(session.createTopic(DESTINATION_NAME)).willReturn(expectedDestination1);
testResolveDestination(session, expectedDestination1, true);
given(session.createTopic(DESTINATION_NAME)).willReturn(expectedDestination2);
testResolveDestination(session, expectedDestination1, true);
}
@Test
void resolveWithPubSubVanillaSession() throws Exception {
Topic expectedDestination1 = new StubTopic();
Topic expectedDestination2 = new StubTopic();
Session session = mock();
given(session.createTopic(DESTINATION_NAME)).willReturn(expectedDestination1);
testResolveDestination(session, expectedDestination1, true);
given(session.createTopic(DESTINATION_NAME)).willReturn(expectedDestination2);
testResolveDestination(session, expectedDestination1, true);
}
@Test
void resolveWithPointToPointQueueSession() throws Exception {
Queue expectedDestination1 = new StubQueue();
Queue expectedDestination2 = new StubQueue();
QueueSession session = mock();
given(session.createQueue(DESTINATION_NAME)).willReturn(expectedDestination1);
testResolveDestination(session, expectedDestination1, false);
given(session.createQueue(DESTINATION_NAME)).willReturn(expectedDestination2);
testResolveDestination(session, expectedDestination1, false);
}
@Test
void resolveWithPointToPointVanillaSession() throws Exception {
Queue expectedDestination1 = new StubQueue();
Queue expectedDestination2 = new StubQueue();
Session session = mock();
given(session.createQueue(DESTINATION_NAME)).willReturn(expectedDestination1);
testResolveDestination(session, expectedDestination1, false);
given(session.createQueue(DESTINATION_NAME)).willReturn(expectedDestination2);
testResolveDestination(session, expectedDestination1, false);
}
private void testResolveDestination(Session session, Destination expected, boolean isPubSub) throws JMSException {
Destination destination = resolver.resolveDestinationName(session, DESTINATION_NAME, isPubSub);
assertThat(destination).isNotNull();
assertThat(destination).isSameAs(expected);
}
}