From 04321d0577aca893913fd9db7619d9b836ae058b Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 30 Jan 2023 15:36:45 +0100 Subject: [PATCH] Revise generated default name for @JmsListener subscription The previous commit changed the generated default name for a JMS subscription to # -- for example: - org.example.MyListener#myListenerMethod However, the JMS spec does not guarantee that '#' is a supported character. This commit therefore changes '#' to '.' as the separator between the class name and method name -- for example: - org.example.MyListener.myListenerMethod This commit also introduces tests and documentation for these changes. See gh-29790 --- .../jms/annotation/JmsListener.java | 7 ++ .../MessagingMessageListenerAdapter.java | 38 +++++-- ...essageListenerAdapterIntegrationTests.java | 104 ++++++++++++++++++ 3 files changed, 139 insertions(+), 10 deletions(-) create mode 100644 spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterIntegrationTests.java diff --git a/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListener.java b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListener.java index 08544dd50c7..679ca7c9a5b 100644 --- a/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListener.java +++ b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListener.java @@ -79,6 +79,7 @@ import org.springframework.messaging.handler.annotation.MessageMapping; * composed annotations with attribute overrides. * * @author Stephane Nicoll + * @author Sam Brannen * @since 4.1 * @see EnableJms * @see JmsListenerAnnotationBeanPostProcessor @@ -113,6 +114,12 @@ public @interface JmsListener { /** * The name for the durable subscription, if any. + *

As of Spring Framework 6.0.5, if an explicit subscription name is not + * specified, a default subscription name will be generated based on the fully + * qualified name of the annotated listener method — for example, + * {@code "org.example.jms.ProductListener.processRequest"} for a + * {@code processRequest(...)} listener method in the + * {@code org.example.jms.ProductListener} class. */ String subscription() default ""; diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java index c7e029f16bf..bc39fbdfa0c 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java @@ -43,6 +43,10 @@ import org.springframework.util.Assert; * are provided as additional arguments so that these can be injected as * method arguments if necessary. * + *

As of Spring Framework 6.0.5, {@code MessagingMessageListenerAdapter} implements + * {@link SubscriptionNameProvider} in order to provide a meaningful default + * subscription name. See {@link #getSubscriptionName()} for details. + * * @author Stephane Nicoll * @author Sam Brannen * @since 4.1 @@ -70,16 +74,6 @@ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageLis return this.handlerMethod; } - @Override - public String getSubscriptionName() { - if (this.handlerMethod != null) { - return this.handlerMethod.getBeanType().getName() + "#" + this.handlerMethod.getMethod().getName(); - } - else { - return this.getClass().getName(); - } - } - @Override public void onMessage(jakarta.jms.Message jmsMessage, @Nullable Session session) throws JMSException { @@ -145,4 +139,28 @@ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageLis .build(); } + /** + * Generate a subscription name for this {@code MessageListener} adapter based + * on the following rules. + *

    + *
  • If the {@link #setHandlerMethod(InvocableHandlerMethod) handlerMethod} + * has been set, the generated subscription name takes the form of + * {@code handlerMethod.getBeanType().getName() + "." + handlerMethod.getMethod().getName()}.
  • + *
  • Otherwise, the generated subscription name is the result of invoking + * {@code getClass().getName()}, which aligns with the default behavior of + * {@link org.springframework.jms.listener.AbstractMessageListenerContainer}.
  • + *
+ * @since 6.0.5 + * @see SubscriptionNameProvider#getSubscriptionName() + */ + @Override + public String getSubscriptionName() { + if (this.handlerMethod != null) { + return this.handlerMethod.getBeanType().getName() + "." + this.handlerMethod.getMethod().getName(); + } + else { + return getClass().getName(); + } + } + } diff --git a/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterIntegrationTests.java b/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterIntegrationTests.java new file mode 100644 index 00000000000..ea7095ae299 --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterIntegrationTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2023 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.listener.adapter; + +import java.lang.reflect.Method; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.jms.listener.SimpleMessageListenerContainer; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Named.named; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/** + * Integration tests for {@link MessagingMessageListenerAdapter}. + * + *

These tests are similar to those in {@link MessagingMessageListenerAdapterTests}, + * except that these tests have a different scope and do not use mocks. + * + * @author Sam Brannen + * @since 6.0.5 + * @see MessagingMessageListenerAdapterTests + */ +class MessagingMessageListenerAdapterIntegrationTests { + + @ParameterizedTest + @MethodSource("subscriptionNames") + void defaultSubscriptionName(Method method, String subscriptionName) { + MessagingMessageListenerAdapter messageListenerAdaptor = new MessagingMessageListenerAdapter(); + InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(new CustomListener(), method); + messageListenerAdaptor.setHandlerMethod(handlerMethod); + + SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer(); + assertThat(listenerContainer.getSubscriptionName()).isNull(); + + listenerContainer.setMessageListener(messageListenerAdaptor); + assertThat(listenerContainer.getSubscriptionName()).isEqualTo(subscriptionName); + } + + + private static Stream subscriptionNames() { + String method1 = "toUpperCase"; + String method2 = "toUpperCase(java.lang.String)"; + String method3 = "toUpperCase(java.lang.String,int)"; + String method4 = "toUpperCase(java.lang.String[])"; + String expectedName = CustomListener.class.getName() + ".toUpperCase"; + return Stream.of( + arguments(named(method1, findMethod()), expectedName), + arguments(named(method2, findMethod(String.class)), expectedName), + arguments(named(method3, findMethod(String.class, String.class)), expectedName), + arguments(named(method4, findMethod(byte[].class)), expectedName)); + } + + private static Method findMethod(Class... paramTypes) { + return ReflectionUtils.findMethod(CustomListener.class, "toUpperCase", paramTypes); + } + + + @SuppressWarnings("unused") + private static class CustomListener { + + // @JmsListener(...) + String toUpperCase() { + return "ENIGMA"; + } + + // @JmsListener(...) + String toUpperCase(String input) { + return "ENIGMA"; + } + + // @JmsListener(...) + String toUpperCase(String input, String customHeader) { + return "ENIGMA"; + } + + // @JmsListener(...) + String toUpperCase(byte[] input) { + return "ENIGMA"; + } + + } + +}