Skip to content

Commit c577fbb

Browse files
bdshadowgaryrussell
authored andcommitted
GH-1295: Validation for @RabbitHandler
Resolves #1295 GH-1295: add custom HandlerMethodArgumentResolver Signed-off-by: Dmitrii Bocharov <[email protected]> GH-1295: simplify configuration of argument Validator. Add argument validation support for @RabbitHandler methods Signed-off-by: Dmitrii Bocharov <[email protected]> Avoid double payload validation in the listener that has no default handler, when a validator is provided Signed-off-by: Dmitrii Bocharov <[email protected]>
1 parent d63b3ca commit c577fbb

File tree

7 files changed

+484
-13
lines changed

7 files changed

+484
-13
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ ext {
4646
commonsPoolVersion = '2.9.0'
4747
googleJsr305Version = '3.0.2'
4848
hamcrestVersion = '2.2'
49+
hibernateValidationVersion = '6.2.0.Final'
4950
jacksonBomVersion = '2.11.3'
5051
jaywayJsonPathVersion = '2.4.0'
5152
junit4Version = '4.13.1'
@@ -362,6 +363,7 @@ project('spring-rabbit') {
362363

363364
testApi project(':spring-rabbit-junit')
364365
testImplementation("com.willowtreeapps.assertk:assertk-jvm:$assertkVersion")
366+
testImplementation "org.hibernate.validator:hibernate-validator:$hibernateValidationVersion"
365367
testRuntimeOnly 'org.springframework:spring-web'
366368
testRuntimeOnly "org.apache.httpcomponents:httpclient:$commonsHttpClientVersion"
367369
testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core'

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2020 the original author or authors.
2+
* Copyright 2014-2021 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.
@@ -85,12 +85,14 @@
8585
import org.springframework.lang.Nullable;
8686
import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory;
8787
import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory;
88+
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
8889
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
8990
import org.springframework.util.Assert;
9091
import org.springframework.util.ClassUtils;
9192
import org.springframework.util.CollectionUtils;
9293
import org.springframework.util.ReflectionUtils;
9394
import org.springframework.util.StringUtils;
95+
import org.springframework.validation.Validator;
9496

9597
/**
9698
* Bean post-processor that registers methods annotated with {@link RabbitListener}
@@ -959,11 +961,19 @@ private MessageHandlerMethodFactory getFactory() {
959961

960962
private MessageHandlerMethodFactory createDefaultMessageHandlerMethodFactory() {
961963
DefaultMessageHandlerMethodFactory defaultFactory = new DefaultMessageHandlerMethodFactory();
964+
Validator validator = RabbitListenerAnnotationBeanPostProcessor.this.registrar.getValidator();
965+
if (validator != null) {
966+
defaultFactory.setValidator(validator);
967+
}
962968
defaultFactory.setBeanFactory(RabbitListenerAnnotationBeanPostProcessor.this.beanFactory);
963969
DefaultConversionService conversionService = new DefaultConversionService();
964970
conversionService.addConverter(
965971
new BytesToStringConverter(RabbitListenerAnnotationBeanPostProcessor.this.charset));
966972
defaultFactory.setConversionService(conversionService);
973+
974+
List<HandlerMethodArgumentResolver> customArgumentsResolver =
975+
new ArrayList<>(RabbitListenerAnnotationBeanPostProcessor.this.registrar.getCustomMethodArgumentResolvers());
976+
defaultFactory.setCustomArgumentResolvers(customArgumentsResolver);
967977
defaultFactory.afterPropertiesSet();
968978
return defaultFactory;
969979
}

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015-2019 the original author or authors.
2+
* Copyright 2015-2021 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.
@@ -25,6 +25,7 @@
2525
import org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter;
2626
import org.springframework.lang.Nullable;
2727
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
28+
import org.springframework.validation.Validator;
2829

2930
/**
3031
* @author Gary Russell
@@ -37,6 +38,8 @@ public class MultiMethodRabbitListenerEndpoint extends MethodRabbitListenerEndpo
3738

3839
private final Method defaultMethod;
3940

41+
private Validator validator;
42+
4043
/**
4144
* Construct an instance for the provided methods and bean.
4245
* @param methods the methods.
@@ -59,6 +62,15 @@ public MultiMethodRabbitListenerEndpoint(List<Method> methods, @Nullable Method
5962
setBean(bean);
6063
}
6164

65+
/**
66+
* Set a payload validator.
67+
* @param validator the validator.
68+
* @since 2.3.7
69+
*/
70+
public void setValidator(Validator validator) {
71+
this.validator = validator;
72+
}
73+
6274
@Override
6375
protected HandlerAdapter configureListenerAdapter(MessagingMessageListenerAdapter messageListener) {
6476
List<InvocableHandlerMethod> invocableHandlerMethods = new ArrayList<InvocableHandlerMethod>();
@@ -71,8 +83,9 @@ protected HandlerAdapter configureListenerAdapter(MessagingMessageListenerAdapte
7183
defaultHandler = handler;
7284
}
7385
}
74-
return new HandlerAdapter(new DelegatingInvocableHandler(invocableHandlerMethods, defaultHandler,
75-
getBean(), getResolver(), getBeanExpressionContext()));
86+
DelegatingInvocableHandler delegatingHandler = new DelegatingInvocableHandler(invocableHandlerMethods,
87+
defaultHandler, getBean(), getResolver(), getBeanExpressionContext(), this.validator);
88+
return new HandlerAdapter(delegatingHandler);
7689
}
7790

7891
}

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

Lines changed: 55 additions & 1 deletion
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-2021 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.
@@ -17,14 +17,18 @@
1717
package org.springframework.amqp.rabbit.listener;
1818

1919
import java.util.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.Collections;
2022
import java.util.List;
2123

2224
import org.springframework.beans.factory.BeanFactory;
2325
import org.springframework.beans.factory.BeanFactoryAware;
2426
import org.springframework.beans.factory.InitializingBean;
2527
import org.springframework.lang.Nullable;
2628
import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory;
29+
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
2730
import org.springframework.util.Assert;
31+
import org.springframework.validation.Validator;
2832

2933
/**
3034
* Helper bean for registering {@link RabbitListenerEndpoint} with
@@ -41,6 +45,8 @@ public class RabbitListenerEndpointRegistrar implements BeanFactoryAware, Initia
4145
private final List<AmqpListenerEndpointDescriptor> endpointDescriptors =
4246
new ArrayList<AmqpListenerEndpointDescriptor>();
4347

48+
private List<HandlerMethodArgumentResolver> customMethodArgumentResolvers = new ArrayList<>();
49+
4450
@Nullable
4551
private RabbitListenerEndpointRegistry endpointRegistry;
4652

@@ -54,6 +60,8 @@ public class RabbitListenerEndpointRegistrar implements BeanFactoryAware, Initia
5460

5561
private boolean startImmediately;
5662

63+
private Validator validator;
64+
5765
/**
5866
* Set the {@link RabbitListenerEndpointRegistry} instance to use.
5967
* @param endpointRegistry the {@link RabbitListenerEndpointRegistry} instance to use.
@@ -71,6 +79,27 @@ public RabbitListenerEndpointRegistry getEndpointRegistry() {
7179
return this.endpointRegistry;
7280
}
7381

82+
/**
83+
* Return the list of {@link HandlerMethodArgumentResolver}.
84+
* @return the list of {@link HandlerMethodArgumentResolver}.
85+
* @since 2.3.7
86+
*/
87+
public List<HandlerMethodArgumentResolver> getCustomMethodArgumentResolvers() {
88+
return Collections.unmodifiableList(this.customMethodArgumentResolvers);
89+
}
90+
91+
92+
/**
93+
* Add custom methods arguments resolvers to
94+
* {@link org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor}
95+
* Default empty list.
96+
* @param methodArgumentResolvers the methodArgumentResolvers to assign.
97+
* @since 2.3.7
98+
*/
99+
public void setCustomMethodArgumentResolvers(HandlerMethodArgumentResolver... methodArgumentResolvers) {
100+
this.customMethodArgumentResolvers = Arrays.asList(methodArgumentResolvers);
101+
}
102+
74103
/**
75104
* Set the {@link MessageHandlerMethodFactory} to use to configure the message
76105
* listener responsible to serve an endpoint detected by this processor.
@@ -84,6 +113,8 @@ public RabbitListenerEndpointRegistry getEndpointRegistry() {
84113
* @param rabbitHandlerMethodFactory the {@link MessageHandlerMethodFactory} instance.
85114
*/
86115
public void setMessageHandlerMethodFactory(MessageHandlerMethodFactory rabbitHandlerMethodFactory) {
116+
Assert.isNull(this.validator,
117+
"A validator cannot be provided with a custom message handler factory");
87118
this.messageHandlerMethodFactory = rabbitHandlerMethodFactory;
88119
}
89120

@@ -127,6 +158,26 @@ public void setBeanFactory(BeanFactory beanFactory) {
127158
this.beanFactory = beanFactory;
128159
}
129160

161+
/**
162+
* Get the validator, if supplied.
163+
* @return the validator.
164+
* @since 2.3.7
165+
*/
166+
@Nullable
167+
public Validator getValidator() {
168+
return this.validator;
169+
}
170+
171+
/**
172+
* Set the validator to use if the default message handler factory is used.
173+
* @param validator the validator.
174+
* @since 2.3.7
175+
*/
176+
public void setValidator(Validator validator) {
177+
Assert.isNull(this.messageHandlerMethodFactory,
178+
"A validator cannot be provided with a custom message handler factory");
179+
this.validator = validator;
180+
}
130181

131182
@Override
132183
public void afterPropertiesSet() {
@@ -137,6 +188,9 @@ protected void registerAllEndpoints() {
137188
Assert.state(this.endpointRegistry != null, "No registry available");
138189
synchronized (this.endpointDescriptors) {
139190
for (AmqpListenerEndpointDescriptor descriptor : this.endpointDescriptors) {
191+
if (descriptor.endpoint instanceof MultiMethodRabbitListenerEndpoint && this.validator != null) {
192+
((MultiMethodRabbitListenerEndpoint) descriptor.endpoint).setValidator(this.validator);
193+
}
140194
this.endpointRegistry.registerListenerContainer(// NOSONAR never null
141195
descriptor.endpoint, resolveContainerFactory(descriptor));
142196
}

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

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015-2020 the original author or authors.
2+
* Copyright 2015-2021 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.
@@ -38,10 +38,14 @@
3838
import org.springframework.expression.spel.standard.SpelExpressionParser;
3939
import org.springframework.lang.Nullable;
4040
import org.springframework.messaging.Message;
41+
import org.springframework.messaging.MessageHeaders;
42+
import org.springframework.messaging.converter.MessageConverter;
4143
import org.springframework.messaging.handler.annotation.Header;
4244
import org.springframework.messaging.handler.annotation.SendTo;
45+
import org.springframework.messaging.handler.annotation.support.PayloadMethodArgumentResolver;
4346
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
4447
import org.springframework.util.Assert;
48+
import org.springframework.validation.Validator;
4549

4650

4751
/**
@@ -66,6 +70,9 @@ public class DelegatingInvocableHandler {
6670

6771
private final ConcurrentMap<Class<?>, InvocableHandlerMethod> cachedHandlers = new ConcurrentHashMap<>();
6872

73+
private final ConcurrentMap<InvocableHandlerMethod, MethodParameter> payloadMethodParameters =
74+
new ConcurrentHashMap<>();
75+
6976
private final InvocableHandlerMethod defaultHandler;
7077

7178
private final Map<InvocableHandlerMethod, Expression> handlerSendTo = new HashMap<>();
@@ -76,6 +83,8 @@ public class DelegatingInvocableHandler {
7683

7784
private final BeanExpressionContext beanExpressionContext;
7885

86+
private final PayloadValidator validator;
87+
7988
/**
8089
* Construct an instance with the supplied handlers for the bean.
8190
* @param handlers the handlers.
@@ -86,7 +95,7 @@ public class DelegatingInvocableHandler {
8695
public DelegatingInvocableHandler(List<InvocableHandlerMethod> handlers, Object bean,
8796
BeanExpressionResolver beanExpressionResolver, BeanExpressionContext beanExpressionContext) {
8897

89-
this(handlers, null, bean, beanExpressionResolver, beanExpressionContext);
98+
this(handlers, null, bean, beanExpressionResolver, beanExpressionContext, null);
9099
}
91100

92101
/**
@@ -101,14 +110,33 @@ public DelegatingInvocableHandler(List<InvocableHandlerMethod> handlers, Object
101110
public DelegatingInvocableHandler(List<InvocableHandlerMethod> handlers,
102111
@Nullable InvocableHandlerMethod defaultHandler, Object bean, BeanExpressionResolver beanExpressionResolver,
103112
BeanExpressionContext beanExpressionContext) {
113+
this(handlers, defaultHandler, bean, beanExpressionResolver, beanExpressionContext, null);
114+
}
115+
116+
/**
117+
* Construct an instance with the supplied handlers for the bean.
118+
* @param handlers the handlers.
119+
* @param defaultHandler the default handler.
120+
* @param bean the bean.
121+
* @param beanExpressionResolver the resolver.
122+
* @param beanExpressionContext the context.
123+
* @param validator the validator.
124+
* @since 2.0.3
125+
*/
126+
public DelegatingInvocableHandler(List<InvocableHandlerMethod> handlers,
127+
@Nullable InvocableHandlerMethod defaultHandler, Object bean, BeanExpressionResolver beanExpressionResolver,
128+
BeanExpressionContext beanExpressionContext, @Nullable Validator validator) {
104129

105130
this.handlers = new ArrayList<>(handlers);
106131
this.defaultHandler = defaultHandler;
107132
this.bean = bean;
108133
this.resolver = beanExpressionResolver;
109134
this.beanExpressionContext = beanExpressionContext;
135+
this.validator = validator == null ? null : new PayloadValidator(validator);
110136
}
111137

138+
139+
112140
/**
113141
* @return the bean
114142
*/
@@ -127,6 +155,12 @@ public Object getBean() {
127155
public InvocationResult invoke(Message<?> message, Object... providedArgs) throws Exception { // NOSONAR
128156
Class<? extends Object> payloadClass = message.getPayload().getClass();
129157
InvocableHandlerMethod handler = getHandlerForPayload(payloadClass);
158+
if (this.validator != null && this.defaultHandler != null) {
159+
MethodParameter parameter = this.payloadMethodParameters.get(handler);
160+
if (parameter != null && this.validator.supportsParameter(parameter)) {
161+
this.validator.validate(message, parameter, message.getPayload());
162+
}
163+
}
130164
Object result = handler.invoke(message, providedArgs);
131165
if (message.getHeaders().get(AmqpHeaders.REPLY_TO) == null) {
132166
Expression replyTo = this.handlerSendTo.get(handler);
@@ -227,6 +261,9 @@ protected boolean matchHandlerMethod(Class<? extends Object> payloadClass, Invoc
227261
if ((methodParameter.getParameterAnnotations().length == 0
228262
|| !methodParameter.hasParameterAnnotation(Header.class))
229263
&& methodParameter.getParameterType().isAssignableFrom(payloadClass)) {
264+
if (this.validator != null) {
265+
this.payloadMethodParameters.put(handler, methodParameter);
266+
}
230267
return true;
231268
}
232269
}
@@ -239,6 +276,9 @@ protected boolean matchHandlerMethod(Class<? extends Object> payloadClass, Invoc
239276
if (foundCandidate) {
240277
throw new AmqpException("Ambiguous payload parameter for " + method.toGenericString());
241278
}
279+
if (this.validator != null) {
280+
this.payloadMethodParameters.put(handler, methodParameter);
281+
}
242282
foundCandidate = true;
243283
}
244284
}
@@ -285,4 +325,31 @@ public InvocationResult getInvocationResultFor(Object result, Object inboundPayl
285325
return null;
286326
}
287327

328+
private static final class PayloadValidator extends PayloadMethodArgumentResolver {
329+
330+
PayloadValidator(Validator validator) {
331+
super(new MessageConverter() { // Required but never used
332+
333+
@Override
334+
@Nullable
335+
public Message<?> toMessage(Object payload, @Nullable
336+
MessageHeaders headers) {
337+
return null;
338+
}
339+
340+
@Override
341+
@Nullable
342+
public Object fromMessage(Message<?> message, Class<?> targetClass) {
343+
return null;
344+
}
345+
346+
}, validator);
347+
}
348+
349+
@Override
350+
public void validate(Message<?> message, MethodParameter parameter, Object target) { // NOSONAR - public
351+
super.validate(message, parameter, target);
352+
}
353+
354+
}
288355
}

0 commit comments

Comments
 (0)