Skip to content

Commit 2557338

Browse files
garyrussellartembilan
authored andcommitted
GH-1138: Support @repeatable user annotations
Resolves #1138
1 parent d0483b9 commit 2557338

File tree

3 files changed

+102
-70
lines changed

3 files changed

+102
-70
lines changed

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java

Lines changed: 9 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2019 the original author or authors.
2+
* Copyright 2014-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.amqp.rabbit.annotation;
1818

19+
import java.lang.reflect.AnnotatedElement;
1920
import java.lang.reflect.Method;
2021
import java.nio.charset.Charset;
2122
import java.nio.charset.StandardCharsets;
@@ -31,6 +32,7 @@
3132
import java.util.concurrent.ConcurrentHashMap;
3233
import java.util.concurrent.ConcurrentMap;
3334
import java.util.concurrent.atomic.AtomicInteger;
35+
import java.util.stream.Collectors;
3436

3537
import org.apache.commons.logging.Log;
3638
import org.apache.commons.logging.LogFactory;
@@ -69,7 +71,6 @@
6971
import org.springframework.context.expression.StandardBeanExpressionResolver;
7072
import org.springframework.core.Ordered;
7173
import org.springframework.core.annotation.AnnotationUtils;
72-
import org.springframework.core.annotation.MergedAnnotation;
7374
import org.springframework.core.annotation.MergedAnnotations;
7475
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
7576
import org.springframework.core.convert.ConversionService;
@@ -331,36 +332,11 @@ private TypeMetadata buildMetadata(Class<?> targetClass) {
331332
classLevelListeners.toArray(new RabbitListener[classLevelListeners.size()]));
332333
}
333334

334-
/*
335-
* AnnotationUtils.getRepeatableAnnotations does not look at interfaces
336-
*/
337-
private Collection<RabbitListener> findListenerAnnotations(Class<?> clazz) {
338-
Set<RabbitListener> listeners = new HashSet<>();
339-
RabbitListener ann = AnnotationUtils.findAnnotation(clazz, RabbitListener.class);
340-
if (ann != null) {
341-
listeners.add(ann);
342-
}
343-
RabbitListeners anns = AnnotationUtils.findAnnotation(clazz, RabbitListeners.class);
344-
if (anns != null) {
345-
Collections.addAll(listeners, anns.value());
346-
}
347-
return listeners;
348-
}
349-
350-
/*
351-
* AnnotationUtils.getRepeatableAnnotations does not look at interfaces
352-
*/
353-
private Collection<RabbitListener> findListenerAnnotations(Method method) {
354-
Set<RabbitListener> listeners = new HashSet<RabbitListener>();
355-
RabbitListener ann = AnnotationUtils.findAnnotation(method, RabbitListener.class);
356-
if (ann != null) {
357-
listeners.add(ann);
358-
}
359-
RabbitListeners anns = AnnotationUtils.findAnnotation(method, RabbitListeners.class);
360-
if (anns != null) {
361-
Collections.addAll(listeners, anns.value());
362-
}
363-
return listeners;
335+
private Collection<RabbitListener> findListenerAnnotations(AnnotatedElement element) {
336+
return MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY)
337+
.stream(RabbitListener.class)
338+
.map(ann -> ann.synthesize())
339+
.collect(Collectors.toList());
364340
}
365341

366342
private void processMultiMethodListeners(RabbitListener[] classLevelListeners, Method[] multiMethods,
@@ -424,10 +400,9 @@ private Method checkProxy(Method methodArg, Object bean) {
424400
return method;
425401
}
426402

427-
protected void processListener(MethodRabbitListenerEndpoint endpoint, RabbitListener rabbitListenerArg, Object bean,
403+
protected void processListener(MethodRabbitListenerEndpoint endpoint, RabbitListener rabbitListener, Object bean,
428404
Object target, String beanName) {
429405

430-
RabbitListener rabbitListener = synthesizeIfPossible(endpoint, rabbitListenerArg, target);
431406
endpoint.setBean(bean);
432407
endpoint.setMessageHandlerMethodFactory(this.messageHandlerMethodFactory);
433408
endpoint.setId(getEndpointId(rabbitListener));
@@ -481,33 +456,6 @@ else if (errorHandler instanceof String) {
481456
this.registrar.registerEndpoint(endpoint, factory);
482457
}
483458

484-
private RabbitListener synthesizeIfPossible(MethodRabbitListenerEndpoint endpoint, RabbitListener rabbitListenerArg,
485-
Object target) {
486-
487-
RabbitListener rabbitListener = rabbitListenerArg;
488-
MergedAnnotation<RabbitListener> mergedAnnotation = MergedAnnotation.missing();
489-
/*
490-
* Synthesize the actual annotation to handle meta-annotations and aliasing. Note
491-
* that only single @RabbitListener annotations can be meta-annotated.
492-
*/
493-
if (endpoint instanceof MultiMethodRabbitListenerEndpoint) {
494-
if (AnnotationUtils.findAnnotation((Class<?>) target, RabbitListeners.class) == null) {
495-
mergedAnnotation = MergedAnnotations.from((Class<?>) target, SearchStrategy.TYPE_HIERARCHY)
496-
.get(RabbitListener.class);
497-
}
498-
}
499-
else {
500-
if (AnnotationUtils.findAnnotation(endpoint.getMethod(), RabbitListeners.class) == null) {
501-
mergedAnnotation = MergedAnnotations.from(endpoint.getMethod(), SearchStrategy.TYPE_HIERARCHY)
502-
.get(RabbitListener.class);
503-
}
504-
}
505-
if (!MergedAnnotation.missing().equals(mergedAnnotation)) {
506-
rabbitListener = mergedAnnotation.synthesize();
507-
}
508-
return rabbitListener;
509-
}
510-
511459
private void resolveAckMode(MethodRabbitListenerEndpoint endpoint, RabbitListener rabbitListener) {
512460
String ackModeAttr = rabbitListener.ackMode();
513461
if (StringUtils.hasText(ackModeAttr)) {

spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import static org.assertj.core.api.Assertions.assertThat;
2020

2121
import java.lang.annotation.ElementType;
22+
import java.lang.annotation.Repeatable;
2223
import java.lang.annotation.Retention;
2324
import java.lang.annotation.RetentionPolicy;
2425
import java.lang.annotation.Target;
@@ -123,12 +124,38 @@ public void metaAnnotationIsDiscovered() {
123124
Config.class, MetaAnnotationTestBean.class);
124125

125126
RabbitListenerContainerTestFactory factory = context.getBean(RabbitListenerContainerTestFactory.class);
126-
assertThat(factory.getListenerContainers().size()).as("one container should have been registered").isEqualTo(1);
127+
assertThat(factory.getListenerContainers().size()).as("one container should have been registered").isEqualTo(2);
128+
RabbitListenerEndpoint endpoint = factory.getListenerContainers().get(0).getEndpoint();
129+
assertThat(((AbstractRabbitListenerEndpoint) endpoint).getQueueNames()
130+
.iterator()
131+
.next())
132+
.isEqualTo("metaTestQueue1");
133+
endpoint = factory.getListenerContainers().get(1).getEndpoint();
134+
assertThat(((AbstractRabbitListenerEndpoint) endpoint).getQueueNames()
135+
.iterator()
136+
.next())
137+
.isEqualTo("metaTestQueue2");
138+
139+
context.close();
140+
}
141+
142+
@Test
143+
public void metaAnnotationIsDiscoveredClassLevel() {
144+
ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(
145+
Config.class, MetaAnnotationTestBean2.class);
146+
147+
RabbitListenerContainerTestFactory factory = context.getBean(RabbitListenerContainerTestFactory.class);
148+
assertThat(factory.getListenerContainers().size()).as("one container should have been registered").isEqualTo(2);
127149
RabbitListenerEndpoint endpoint = factory.getListenerContainers().get(0).getEndpoint();
128150
assertThat(((AbstractRabbitListenerEndpoint) endpoint).getQueueNames()
129151
.iterator()
130152
.next())
131-
.isEqualTo("metaTestQueue");
153+
.isEqualTo("metaTestQueue3");
154+
endpoint = factory.getListenerContainers().get(1).getEndpoint();
155+
assertThat(((AbstractRabbitListenerEndpoint) endpoint).getQueueNames()
156+
.iterator()
157+
.next())
158+
.isEqualTo("metaTestQueue4");
132159

133160
context.close();
134161
}
@@ -334,22 +361,44 @@ public void handleIt(String body) {
334361
@Component
335362
static class MetaAnnotationTestBean {
336363

337-
@FooListener("metaTestQueue")
364+
@FooListener("metaTestQueue1")
365+
@FooListener("metaTestQueue2")
366+
public void handleIt(String body) {
367+
}
368+
369+
}
370+
371+
@Component
372+
@FooListener("metaTestQueue3")
373+
@FooListener("metaTestQueue4")
374+
static class MetaAnnotationTestBean2 {
375+
376+
@RabbitHandler
338377
public void handleIt(String body) {
339378
}
379+
340380
}
341381

342382

343-
@RabbitListener
344-
@Target(ElementType.METHOD)
383+
@RabbitListener(autoStartup = "false")
384+
@Target({ ElementType.METHOD, ElementType.TYPE })
345385
@Retention(RetentionPolicy.RUNTIME)
386+
@Repeatable(FooListeners.class)
346387
static @interface FooListener {
347388

348389
@AliasFor(annotation = RabbitListener.class, attribute = "queues")
349390
String[] value() default {};
350391

351392
}
352393

394+
@Target({ ElementType.METHOD, ElementType.TYPE })
395+
@Retention(RetentionPolicy.RUNTIME)
396+
static @interface FooListeners {
397+
398+
FooListener[] value();
399+
400+
}
401+
353402
@Component
354403
static class MultipleQueueNamesTestBean {
355404

src/reference/asciidoc/amqp.adoc

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2166,9 +2166,44 @@ public class MetaListener {
21662166

21672167
In the preceding example, each listener created by the `@MyAnonFanoutListener` annotation binds an anonymous, auto-delete
21682168
queue to the fanout exchange, `metaFanout`.
2169-
The meta-annotation mechanism is simple because attributes on the user-defined annotation are *not* examined - so
2170-
you cannot override settings from the meta-annotation.
2171-
You can use normal `@Bean` definitions when you need more advanced configuration.
2169+
Starting with version 2.2.3, `@AliasFor` is supported to allow overriding properties on the meta-annotated annotation.
2170+
Also, user annotations can now be `@Repeatable`, allowing multiple containers to be created for a method.
2171+
2172+
====
2173+
[source, java]
2174+
----
2175+
@Component
2176+
static class MetaAnnotationTestBean {
2177+
2178+
@MyListener("queue1")
2179+
@MyListener("queue2")
2180+
public void handleIt(String body) {
2181+
}
2182+
2183+
}
2184+
2185+
2186+
@RabbitListener
2187+
@Target(ElementType.METHOD)
2188+
@Retention(RetentionPolicy.RUNTIME)
2189+
@Repeatable(MyListeners.class)
2190+
static @interface MyListener {
2191+
2192+
@AliasFor(annotation = RabbitListener.class, attribute = "queues")
2193+
String[] value() default {};
2194+
2195+
}
2196+
2197+
@Target(ElementType.METHOD)
2198+
@Retention(RetentionPolicy.RUNTIME)
2199+
static @interface MyListeners {
2200+
2201+
MyListener[] value();
2202+
2203+
}
2204+
----
2205+
====
2206+
21722207

21732208
[[async-annotation-driven-enable]]
21742209
====== Enable Listener Endpoint Annotations

0 commit comments

Comments
 (0)