diff --git a/spring-jms/src/main/java/org/springframework/jms/core/JmsClient.java b/spring-jms/src/main/java/org/springframework/jms/core/JmsClient.java index fb200c5d4e..e84dd350af 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/JmsClient.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/JmsClient.java @@ -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(); - } diff --git a/spring-jms/src/main/java/org/springframework/jms/core/JmsTemplate.java b/spring-jms/src/main/java/org/springframework/jms/core/JmsTemplate.java index 386c32c6c3..677d195226 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/JmsTemplate.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/JmsTemplate.java @@ -60,7 +60,7 @@ import org.springframework.util.ClassUtils; * the "sessionTransacted" and "sessionAcknowledgeMode" bean properties. * *

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" diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java index 49884e4d32..fa1296a31e 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java @@ -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. - *

The default resolver is a DynamicDestinationResolver. Specify a + *

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) { diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/StandardJmsActivationSpecFactory.java b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/StandardJmsActivationSpecFactory.java index 87c92ae52b..75f51e31cb 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/StandardJmsActivationSpecFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/StandardJmsActivationSpecFactory.java @@ -86,7 +86,8 @@ public class StandardJmsActivationSpecFactory implements JmsActivationSpecFactor * able to work without 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; diff --git a/spring-jms/src/main/java/org/springframework/jms/support/destination/DestinationResolver.java b/spring-jms/src/main/java/org/springframework/jms/support/destination/DestinationResolver.java index 0e6c5bb474..c58b098815 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/destination/DestinationResolver.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/destination/DestinationResolver.java @@ -30,13 +30,13 @@ import org.jspecify.annotations.Nullable; * *

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 diff --git a/spring-jms/src/main/java/org/springframework/jms/support/destination/DynamicDestinationResolver.java b/spring-jms/src/main/java/org/springframework/jms/support/destination/DynamicDestinationResolver.java index e1cf32f815..8fd0ce5dfb 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/destination/DynamicDestinationResolver.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/destination/DynamicDestinationResolver.java @@ -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 */ diff --git a/spring-jms/src/main/java/org/springframework/jms/support/destination/JmsDestinationAccessor.java b/spring-jms/src/main/java/org/springframework/jms/support/destination/JmsDestinationAccessor.java index 3c8cf7f553..192eadb1dc 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/destination/JmsDestinationAccessor.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/destination/JmsDestinationAccessor.java @@ -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. - *

The default resolver is a DynamicDestinationResolver. Specify a + *

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) { diff --git a/spring-jms/src/main/java/org/springframework/jms/support/destination/SimpleDestinationResolver.java b/spring-jms/src/main/java/org/springframework/jms/support/destination/SimpleDestinationResolver.java new file mode 100644 index 0000000000..80e40db2e8 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/support/destination/SimpleDestinationResolver.java @@ -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. + * + *

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 topicCache = new ConcurrentHashMap<>(4); + + private final Map 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(); + } + +} diff --git a/spring-jms/src/test/java/org/springframework/jms/support/destination/DynamicDestinationResolverTests.java b/spring-jms/src/test/java/org/springframework/jms/support/destination/DynamicDestinationResolverTests.java index 4f063b1b6c..93935e4166 100644 --- a/spring-jms/src/test/java/org/springframework/jms/support/destination/DynamicDestinationResolverTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/support/destination/DynamicDestinationResolverTests.java @@ -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); } } diff --git a/spring-jms/src/test/java/org/springframework/jms/support/destination/SimpleDestinationResolverTests.java b/spring-jms/src/test/java/org/springframework/jms/support/destination/SimpleDestinationResolverTests.java new file mode 100644 index 0000000000..648910c9a3 --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/support/destination/SimpleDestinationResolverTests.java @@ -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); + } + +}