Skip to content

Commit f649d11

Browse files
garyrussellartembilan
authored andcommitted
AMQP-799: Add default @RabbitHandler support
JIRA: https://jira.spring.io/browse/AMQP-799 Support the designation of a single `@RabbitHandler` as the default if there is no match.
1 parent 84ca73e commit f649d11

File tree

6 files changed

+105
-15
lines changed

6 files changed

+105
-15
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015 the original author or authors.
2+
* Copyright 2015-2018 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.
@@ -48,4 +48,12 @@
4848
@Documented
4949
public @interface RabbitHandler {
5050

51+
/**
52+
* When true, designate that this is the default fallback method if the payload type
53+
* matches no other {@link RabbitHandler} method. Only one method can be so designated.
54+
* @return true if this is the default method.
55+
* @since 2.0.3
56+
*/
57+
boolean isDefault() default false;
58+
5159
}

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2017 the original author or authors.
2+
* Copyright 2014-2018 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.
@@ -342,11 +342,20 @@ private Collection<RabbitListener> findListenerAnnotations(Method method) {
342342
private void processMultiMethodListeners(RabbitListener[] classLevelListeners, Method[] multiMethods,
343343
Object bean, String beanName) {
344344
List<Method> checkedMethods = new ArrayList<Method>();
345+
Method defaultMethod = null;
345346
for (Method method : multiMethods) {
346-
checkedMethods.add(checkProxy(method, bean));
347+
Method checked = checkProxy(method, bean);
348+
if (AnnotationUtils.findAnnotation(method, RabbitHandler.class).isDefault()) {
349+
final Method toAssert = defaultMethod;
350+
Assert.state(toAssert == null, () -> "Only one @RabbitHandler can be marked 'isDefault', found: "
351+
+ toAssert.toString() + " and " + method.toString());
352+
defaultMethod = checked;
353+
}
354+
checkedMethods.add(checked);
347355
}
348356
for (RabbitListener classLevelListener : classLevelListeners) {
349-
MultiMethodRabbitListenerEndpoint endpoint = new MultiMethodRabbitListenerEndpoint(checkedMethods, bean);
357+
MultiMethodRabbitListenerEndpoint endpoint =
358+
new MultiMethodRabbitListenerEndpoint(checkedMethods, defaultMethod, bean);
350359
endpoint.setBeanFactory(this.beanFactory);
351360
processListener(endpoint, classLevelListener, bean, bean.getClass(), beanName);
352361
}

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015-2016 the original author or authors.
2+
* Copyright 2015-2018 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.
@@ -23,6 +23,7 @@
2323
import org.springframework.amqp.rabbit.listener.adapter.DelegatingInvocableHandler;
2424
import org.springframework.amqp.rabbit.listener.adapter.HandlerAdapter;
2525
import org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter;
26+
import org.springframework.lang.Nullable;
2627
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
2728

2829
/**
@@ -34,22 +35,46 @@ public class MultiMethodRabbitListenerEndpoint extends MethodRabbitListenerEndpo
3435

3536
private final List<Method> methods;
3637

38+
private final Method defaultMethod;
39+
3740
private DelegatingInvocableHandler delegatingHandler;
3841

42+
/**
43+
* Construct an instance for the provided methods and bean.
44+
* @param methods the methods.
45+
* @param bean the bean.
46+
*/
3947
public MultiMethodRabbitListenerEndpoint(List<Method> methods, Object bean) {
48+
this(methods, null, bean);
49+
}
50+
51+
/**
52+
* Construct an instance for the provided methods, default method and bean.
53+
* @param methods the methods.
54+
* @param defaultMethod the default method.
55+
* @param bean the bean.
56+
* @since 2.0.3
57+
*/
58+
public MultiMethodRabbitListenerEndpoint(List<Method> methods, @Nullable Method defaultMethod, Object bean) {
4059
this.methods = methods;
60+
this.defaultMethod = defaultMethod;
4161
setBean(bean);
4262
}
4363

4464
@Override
4565
protected HandlerAdapter configureListenerAdapter(MessagingMessageListenerAdapter messageListener) {
4666
List<InvocableHandlerMethod> invocableHandlerMethods = new ArrayList<InvocableHandlerMethod>();
67+
InvocableHandlerMethod defaultHandler = null;
4768
for (Method method : this.methods) {
48-
invocableHandlerMethods.add(getMessageHandlerMethodFactory()
49-
.createInvocableHandlerMethod(getBean(), method));
69+
InvocableHandlerMethod handler = getMessageHandlerMethodFactory()
70+
.createInvocableHandlerMethod(getBean(), method);
71+
invocableHandlerMethods.add(handler);
72+
if (method.equals(this.defaultMethod)) {
73+
defaultHandler = handler;
74+
}
5075
}
51-
this.delegatingHandler = new DelegatingInvocableHandler(invocableHandlerMethods, getBean(), getResolver(),
52-
getBeanExpressionContext());
76+
this.delegatingHandler = new DelegatingInvocableHandler(invocableHandlerMethods, defaultHandler,
77+
getBean(), getResolver(), getBeanExpressionContext());
5378
return new HandlerAdapter(this.delegatingHandler);
5479
}
5580

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015-2017 the original author or authors.
2+
* Copyright 2015-2018 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.
@@ -36,6 +36,7 @@
3636
import org.springframework.expression.ParserContext;
3737
import org.springframework.expression.common.TemplateParserContext;
3838
import org.springframework.expression.spel.standard.SpelExpressionParser;
39+
import org.springframework.lang.Nullable;
3940
import org.springframework.messaging.Message;
4041
import org.springframework.messaging.handler.annotation.Header;
4142
import org.springframework.messaging.handler.annotation.Payload;
@@ -64,6 +65,8 @@ public class DelegatingInvocableHandler {
6465
private final ConcurrentMap<Class<?>, InvocableHandlerMethod> cachedHandlers =
6566
new ConcurrentHashMap<Class<?>, InvocableHandlerMethod>();
6667

68+
private final InvocableHandlerMethod defaultHandler;
69+
6770
private final Map<InvocableHandlerMethod, Expression> handlerSendTo =
6871
new HashMap<InvocableHandlerMethod, Expression>();
6972

@@ -82,7 +85,23 @@ public class DelegatingInvocableHandler {
8285
*/
8386
public DelegatingInvocableHandler(List<InvocableHandlerMethod> handlers, Object bean,
8487
BeanExpressionResolver beanExpressionResolver, BeanExpressionContext beanExpressionContext) {
88+
this(handlers, null, bean, beanExpressionResolver, beanExpressionContext);
89+
}
90+
91+
/**
92+
* Construct an instance with the supplied handlers for the bean.
93+
* @param handlers the handlers.
94+
* @param defaultHandler the default handler.
95+
* @param bean the bean.
96+
* @param beanExpressionResolver the resolver.
97+
* @param beanExpressionContext the context.
98+
* @since 2.0.3
99+
*/
100+
public DelegatingInvocableHandler(List<InvocableHandlerMethod> handlers,
101+
@Nullable InvocableHandlerMethod defaultHandler, Object bean, BeanExpressionResolver beanExpressionResolver,
102+
BeanExpressionContext beanExpressionContext) {
85103
this.handlers = new ArrayList<InvocableHandlerMethod>(handlers);
104+
this.defaultHandler = defaultHandler;
86105
this.bean = bean;
87106
this.resolver = beanExpressionResolver;
88107
this.beanExpressionContext = beanExpressionContext;
@@ -178,13 +197,19 @@ protected InvocableHandlerMethod findHandlerForPayload(Class<? extends Object> p
178197
for (InvocableHandlerMethod handler : this.handlers) {
179198
if (matchHandlerMethod(payloadClass, handler)) {
180199
if (result != null) {
181-
throw new AmqpException("Ambiguous methods for payload type: " + payloadClass + ": " +
182-
result.getMethod().getName() + " and " + handler.getMethod().getName());
200+
boolean resultIsDefault = result.equals(this.defaultHandler);
201+
if (!handler.equals(this.defaultHandler) && !resultIsDefault) {
202+
throw new AmqpException("Ambiguous methods for payload type: " + payloadClass + ": " +
203+
result.getMethod().getName() + " and " + handler.getMethod().getName());
204+
}
205+
if (!resultIsDefault) {
206+
continue; // otherwise replace the result with the actual match
207+
}
183208
}
184209
result = handler;
185210
}
186211
}
187-
return result;
212+
return result != null ? result : this.defaultHandler;
188213
}
189214

190215
protected boolean matchHandlerMethod(Class<? extends Object> payloadClass, InvocableHandlerMethod handler) {
@@ -237,4 +262,8 @@ public Method getMethodFor(Object payload) {
237262
return handlerForPayload == null ? null : handlerForPayload.getMethod();
238263
}
239264

265+
public boolean hasDefaultHandler() {
266+
return this.defaultHandler != null;
267+
}
268+
240269
}

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015-2017 the original author or authors.
2+
* Copyright 2015-2018 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.
@@ -50,6 +50,13 @@ public Object invoke(Message<?> message, Object... providedArgs) throws Exceptio
5050
if (this.invokerHandlerMethod != null) {
5151
return this.invokerHandlerMethod.invoke(message, providedArgs);
5252
}
53+
else if (this.delegatingHandler.hasDefaultHandler()) {
54+
// Needed to avoid returning raw Message which matches Object
55+
Object[] args = new Object[providedArgs.length + 1];
56+
args[0] = message.getPayload();
57+
System.arraycopy(providedArgs, 0, args, 1, providedArgs.length);
58+
return this.delegatingHandler.invoke(message, args);
59+
}
5360
else {
5461
return this.delegatingHandler.invoke(message, providedArgs);
5562
}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2017 the original author or authors.
2+
* Copyright 2014-2018 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.
@@ -318,6 +318,10 @@ public void commas() {
318318

319319
@Test
320320
public void multiListener() {
321+
Foo foo = new Foo();
322+
foo.field = "foo";
323+
assertEquals("FOO: foo handled by default handler",
324+
rabbitTemplate.convertSendAndReceive("multi.exch", "multi.rk", foo));
321325
Bar bar = new Bar();
322326
bar.field = "bar";
323327
rabbitTemplate.convertAndSend("multi.exch", "multi.rk", bar);
@@ -1340,6 +1344,14 @@ public String qux(@Header("amqp_receivedRoutingKey") String rk, @NonNull @Payloa
13401344
return "QUX: " + qux.field + ": " + rk;
13411345
}
13421346

1347+
@RabbitHandler(isDefault = true)
1348+
public String defaultHandler(@Payload Object payload) {
1349+
if (payload instanceof Foo) {
1350+
return "FOO: " + ((Foo) payload).field + " handled by default handler";
1351+
}
1352+
return payload.toString() + " handled by default handler";
1353+
}
1354+
13431355
}
13441356

13451357
@RabbitListener(id = "multi", bindings = @QueueBinding

0 commit comments

Comments
 (0)