From 4631add6cfe2537cc6bd5a442fe74a6604a62207 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 22 Jun 2015 11:39:52 +0200 Subject: [PATCH] Add support for repeatable JmsListener Previously, a method could only declare one Jms endpoint so if several destinations share the exact same business logic, you'd still need one separate method declaration per destination. We now make sure that JmsListener is a repeatable annotation, introducing JmsListeners for pre Java8 use cases. Issue: SPR-13147 --- .../jms/annotation/JmsListener.java | 5 +- ...msListenerAnnotationBeanPostProcessor.java | 4 +- .../jms/annotation/JmsListeners.java | 44 ++++++++++++++++ .../AbstractJmsAnnotationDrivenTests.java | 50 +++++++++++++++++++ .../AnnotationDrivenNamespaceTests.java | 16 +++++- .../jms/annotation/EnableJmsTests.java | 14 ++++++ .../JmsListenerContainerTestFactory.java | 17 ++++--- ...otation-driven-jms-listener-repeatable.xml | 18 +++++++ .../annotation-driven-jms-listeners.xml | 18 +++++++ src/asciidoc/integration.adoc | 4 ++ 10 files changed, 180 insertions(+), 10 deletions(-) create mode 100644 spring-jms/src/main/java/org/springframework/jms/annotation/JmsListeners.java create mode 100644 spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-jms-listener-repeatable.xml create mode 100644 spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-jms-listeners.xml 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 4a9cf987b4..29cfaa10f7 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -18,6 +18,7 @@ package org.springframework.jms.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -70,11 +71,13 @@ import org.springframework.messaging.handler.annotation.MessageMapping; * @since 4.1 * @see EnableJms * @see JmsListenerAnnotationBeanPostProcessor + * @see JmsListeners */ @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @MessageMapping @Documented +@Repeatable(JmsListeners.class) public @interface JmsListener { /** diff --git a/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessor.java b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessor.java index ce245f609d..373f8443a4 100644 --- a/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessor.java +++ b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessor.java @@ -199,8 +199,8 @@ public class JmsListenerAnnotationBeanPostProcessor ReflectionUtils.doWithMethods(targetClass, new ReflectionUtils.MethodCallback() { @Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { - JmsListener jmsListener = AnnotationUtils.getAnnotation(method, JmsListener.class); - if (jmsListener != null) { + for (JmsListener jmsListener : + AnnotationUtils.getRepeatableAnnotations(method, JmsListener.class, JmsListeners.class)) { processJmsListener(jmsListener, method, bean); annotatedMethods.add(method); } diff --git a/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListeners.java b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListeners.java new file mode 100644 index 0000000000..f3d5266e52 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListeners.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2015 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 + * + * http://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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container annotation that aggregates several {@link JmsListener} annotations. + * + *

Can be used natively, declaring several nested {@link JmsListener} annotations. + * Can also be used in conjunction with Java 8's support for repeatable annotations, + * where {@link JmsListener} can simply be declared several times on the same method, + * implicitly generating this container annotation. + * + * @author Stephane Nicoll + * @since 4.2 + * @see JmsListener + */ +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface JmsListeners { + + JmsListener[] value(); + +} diff --git a/spring-jms/src/test/java/org/springframework/jms/annotation/AbstractJmsAnnotationDrivenTests.java b/spring-jms/src/test/java/org/springframework/jms/annotation/AbstractJmsAnnotationDrivenTests.java index 612f8693bd..b58bfb4dd6 100644 --- a/spring-jms/src/test/java/org/springframework/jms/annotation/AbstractJmsAnnotationDrivenTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/annotation/AbstractJmsAnnotationDrivenTests.java @@ -73,6 +73,12 @@ public abstract class AbstractJmsAnnotationDrivenTests { @Test public abstract void jmsHandlerMethodFactoryConfiguration() throws JMSException; + @Test + public abstract void jmsListenerIsRepeatable(); + + @Test + public abstract void jmsListeners(); + /** * Test for {@link SampleBean} discovery. If a factory with the default name * is set, an endpoint will use it automatically @@ -234,6 +240,50 @@ public abstract class AbstractJmsAnnotationDrivenTests { } } + /** + * Test for {@link JmsListenerRepeatableBean} and {@link JmsListenersBean} that validates that the + * {@code @JmsListener} annotation is repeatable and generate one specific container per annotation. + */ + public void testJmsListenerRepeatable(ApplicationContext context) { + JmsListenerContainerTestFactory simpleFactory = + context.getBean("jmsListenerContainerFactory", JmsListenerContainerTestFactory.class); + assertEquals(2, simpleFactory.getListenerContainers().size()); + + MethodJmsListenerEndpoint first = (MethodJmsListenerEndpoint) + simpleFactory.getListenerContainer("first").getEndpoint(); + assertEquals("first", first.getId()); + assertEquals("myQueue", first.getDestination()); + assertEquals(null, first.getConcurrency()); + + MethodJmsListenerEndpoint second = (MethodJmsListenerEndpoint) + simpleFactory.getListenerContainer("second").getEndpoint(); + assertEquals("second", second.getId()); + assertEquals("anotherQueue", second.getDestination()); + assertEquals("2-10", second.getConcurrency()); + } + + @Component + static class JmsListenerRepeatableBean { + + @JmsListener(id = "first", destination = "myQueue") + @JmsListener(id = "second", destination = "anotherQueue", concurrency = "2-10") + public void repeatableHandle(String msg) { + } + + } + + @Component + static class JmsListenersBean { + + @JmsListeners({ + @JmsListener(id = "first", destination = "myQueue"), + @JmsListener(id = "second", destination = "anotherQueue", concurrency = "2-10") + }) + public void repeatableHandle(String msg) { + } + + } + static class TestValidator implements Validator { @Override diff --git a/spring-jms/src/test/java/org/springframework/jms/annotation/AnnotationDrivenNamespaceTests.java b/spring-jms/src/test/java/org/springframework/jms/annotation/AnnotationDrivenNamespaceTests.java index 22f8a334ea..74f7b7844b 100644 --- a/spring-jms/src/test/java/org/springframework/jms/annotation/AnnotationDrivenNamespaceTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/annotation/AnnotationDrivenNamespaceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -91,6 +91,20 @@ public class AnnotationDrivenNamespaceTests extends AbstractJmsAnnotationDrivenT testJmsHandlerMethodFactoryConfiguration(context); } + @Override + public void jmsListenerIsRepeatable() { + ApplicationContext context = new ClassPathXmlApplicationContext( + "annotation-driven-jms-listener-repeatable.xml", getClass()); + testJmsListenerRepeatable(context); + } + + @Override + public void jmsListeners() { + ApplicationContext context = new ClassPathXmlApplicationContext( + "annotation-driven-jms-listeners.xml", getClass()); + testJmsListenerRepeatable(context); + } + static class CustomJmsListenerConfigurer implements JmsListenerConfigurer { private MessageListener messageListener; diff --git a/spring-jms/src/test/java/org/springframework/jms/annotation/EnableJmsTests.java b/spring-jms/src/test/java/org/springframework/jms/annotation/EnableJmsTests.java index e4a2613d01..45d2877df8 100644 --- a/spring-jms/src/test/java/org/springframework/jms/annotation/EnableJmsTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/annotation/EnableJmsTests.java @@ -112,6 +112,20 @@ public class EnableJmsTests extends AbstractJmsAnnotationDrivenTests { testJmsHandlerMethodFactoryConfiguration(context); } + @Override + public void jmsListenerIsRepeatable() { + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( + EnableJmsDefaultContainerFactoryConfig.class, JmsListenerRepeatableBean.class); + testJmsListenerRepeatable(context); + } + + @Override + public void jmsListeners() { + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( + EnableJmsDefaultContainerFactoryConfig.class, JmsListenersBean.class); + testJmsListenerRepeatable(context); + } + @Test public void unknownFactory() { thrown.expect(BeanCreationException.class); diff --git a/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerTestFactory.java b/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerTestFactory.java index f67d976fcf..d51e06ee63 100644 --- a/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerTestFactory.java +++ b/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerTestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -17,25 +17,30 @@ package org.springframework.jms.config; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** - * * @author Stephane Nicoll */ public class JmsListenerContainerTestFactory implements JmsListenerContainerFactory { - private final List listenerContainers = - new ArrayList(); + private final Map listenerContainers = + new LinkedHashMap<>(); public List getListenerContainers() { - return listenerContainers; + return new ArrayList<>(this.listenerContainers.values()); + } + + public MessageListenerTestContainer getListenerContainer(String id) { + return this.listenerContainers.get(id); } @Override public MessageListenerTestContainer createListenerContainer(JmsListenerEndpoint endpoint) { MessageListenerTestContainer container = new MessageListenerTestContainer(endpoint); - this.listenerContainers.add(container); + this.listenerContainers.put(endpoint.getId(), container); return container; } diff --git a/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-jms-listener-repeatable.xml b/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-jms-listener-repeatable.xml new file mode 100644 index 0000000000..5769015ed6 --- /dev/null +++ b/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-jms-listener-repeatable.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-jms-listeners.xml b/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-jms-listeners.xml new file mode 100644 index 0000000000..69580e2a5b --- /dev/null +++ b/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-jms-listeners.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/asciidoc/integration.adoc b/src/asciidoc/integration.adoc index 89931ef3dd..f3a86a6d3f 100644 --- a/src/asciidoc/integration.adoc +++ b/src/asciidoc/integration.adoc @@ -2577,6 +2577,10 @@ provides). The annotated endpoint infrastructure creates a message listener container behind the scenes for each annotated method, using a `JmsListenerContainerFactory`. +TIP: `@JmsListener` is a _repeatable_ annotation so it is possible to associate several +JMS destinations to the same method by adding additional `@JmsListener` declaration on +it. For pre Java8 use cases, you can use `@JmsListeners`. + [[jms-annotated-support]] ==== Enable listener endpoint annotations