Support Validation in @MessageMapping annotated methods
Payload parameters in @MessageMapping annotated methods can now also be validated when annotated with a Validation annotation (@Valid, @Validated...). A default Validator is registered by the MessageBroker Configurer, but it is possible to provide a list of custom validators as well. Issue: SPR-11185
This commit is contained in:
@@ -41,7 +41,10 @@ import org.springframework.messaging.Message;
|
||||
* a message and optionally convert it using a
|
||||
* {@link org.springframework.messaging.converter.MessageConverter}.
|
||||
* The presence of the annotation is not required since it is assumed by default
|
||||
* for method arguments that are not annotated.</li>
|
||||
* for method arguments that are not annotated. Payload method arguments annotated
|
||||
* with Validation annotations (like
|
||||
* {@link org.springframework.validation.annotation.Validated}) will be subject to
|
||||
* JSR-303 validation.</li>
|
||||
* <li>{@link Header}-annotated method arguments to extract a specific
|
||||
* header value along with type conversion with a
|
||||
* {@link org.springframework.core.convert.converter.Converter} if necessary.</li>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2002-2014 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.messaging.handler.annotation.support;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.ObjectError;
|
||||
|
||||
/**
|
||||
* Exception to be thrown when validation on an method parameter annotated with {@code @Valid} fails.
|
||||
* @author Brian Clozel
|
||||
* @since 4.0.1
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class MethodArgumentNotValidException extends MessagingException {
|
||||
|
||||
private final MethodParameter parameter;
|
||||
|
||||
private final BindingResult bindingResult;
|
||||
|
||||
|
||||
public MethodArgumentNotValidException(Message<?> message, MethodParameter parameter, BindingResult bindingResult) {
|
||||
super(message);
|
||||
this.parameter = parameter;
|
||||
this.bindingResult = bindingResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
StringBuilder sb = new StringBuilder("Validation failed for parameter at index ")
|
||||
.append(this.parameter.getParameterIndex()).append(" in method: ")
|
||||
.append(this.parameter.getMethod().toGenericString())
|
||||
.append(", with ").append(this.bindingResult.getErrorCount()).append(" error(s): ");
|
||||
for (ObjectError error : this.bindingResult.getAllErrors()) {
|
||||
sb.append("[").append(error).append("] ");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
* Copyright 2002-2014 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,32 +17,45 @@
|
||||
package org.springframework.messaging.handler.annotation.support;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.handler.annotation.Payload;
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
import org.springframework.messaging.converter.MessageConverter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.BeanPropertyBindingResult;
|
||||
import org.springframework.validation.SmartValidator;
|
||||
import org.springframework.validation.Validator;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
/**
|
||||
* A resolver to extract and convert the payload of a message using a
|
||||
* {@link MessageConverter}.
|
||||
* {@link MessageConverter}. It also validates the payload using a
|
||||
* {@link Validator} if the argument is annotated with a Validation annotation.
|
||||
*
|
||||
* <p>This {@link HandlerMethodArgumentResolver} should be ordered last as it supports all
|
||||
* types and does not require the {@link Payload} annotation.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 4.0
|
||||
*/
|
||||
public class PayloadArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
private final MessageConverter converter;
|
||||
|
||||
private final Validator validator;
|
||||
|
||||
public PayloadArgumentResolver(MessageConverter messageConverter) {
|
||||
|
||||
public PayloadArgumentResolver(MessageConverter messageConverter, Validator validator) {
|
||||
Assert.notNull(messageConverter, "converter must not be null");
|
||||
Assert.notNull(validator, "validator must not be null");
|
||||
this.converter = messageConverter;
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -72,7 +85,10 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
throw new IllegalStateException("@Payload SpEL expressions not supported by this resolver.");
|
||||
}
|
||||
|
||||
return this.converter.fromMessage(message, targetClass);
|
||||
Object target = this.converter.fromMessage(message, targetClass);
|
||||
validate(message, parameter, target);
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
protected boolean isEmptyPayload(Message<?> message) {
|
||||
@@ -88,4 +104,28 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
}
|
||||
}
|
||||
|
||||
protected void validate(Message<?> message, MethodParameter parameter, Object target) {
|
||||
Annotation[] annotations = parameter.getParameterAnnotations();
|
||||
for (Annotation annot : annotations) {
|
||||
if (annot.annotationType().getSimpleName().startsWith("Valid")) {
|
||||
BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(target, parameter.getParameterName());
|
||||
Object hints = AnnotationUtils.getValue(annot);
|
||||
Object[] validationHints = hints instanceof Object[] ? (Object[]) hints : new Object[] {hints};
|
||||
|
||||
if (!ObjectUtils.isEmpty(validationHints) && this.validator instanceof SmartValidator) {
|
||||
((SmartValidator) this.validator).validate(target, bindingResult, validationHints);
|
||||
}
|
||||
else if (this.validator != null) {
|
||||
this.validator.validate(target, bindingResult);
|
||||
}
|
||||
|
||||
if (bindingResult.hasErrors()) {
|
||||
throw new MethodArgumentNotValidException(message, parameter, bindingResult);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
* Copyright 2002-2014 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,13 +17,7 @@
|
||||
package org.springframework.messaging.simp.annotation.support;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
@@ -60,6 +54,7 @@ import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.validation.Validator;
|
||||
|
||||
/**
|
||||
* A handler for messages delegating to
|
||||
@@ -87,6 +82,8 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan
|
||||
|
||||
private PathMatcher pathMatcher = new AntPathMatcher();
|
||||
|
||||
private Validator validator;
|
||||
|
||||
private final Object lifecycleMonitor = new Object();
|
||||
|
||||
private volatile boolean running = false;
|
||||
@@ -171,6 +168,22 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan
|
||||
return this.pathMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* The configured Validator instance
|
||||
*/
|
||||
public Validator getValidator() {
|
||||
return validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Validator instance used for validating @Payload arguments
|
||||
* @see org.springframework.validation.annotation.Validated
|
||||
* @see PayloadArgumentResolver
|
||||
*/
|
||||
public void setValidator(Validator validator) {
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoStartup() {
|
||||
return true;
|
||||
@@ -230,7 +243,7 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan
|
||||
resolvers.add(new MessageMethodArgumentResolver());
|
||||
|
||||
resolvers.addAll(getCustomArgumentResolvers());
|
||||
resolvers.add(new PayloadArgumentResolver(this.messageConverter));
|
||||
resolvers.add(new PayloadArgumentResolver(this.messageConverter, this.validator));
|
||||
|
||||
return resolvers;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
* Copyright 2002-2014 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.
|
||||
@@ -19,6 +19,11 @@ package org.springframework.messaging.simp.config;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanInitializationException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.converter.ByteArrayMessageConverter;
|
||||
@@ -41,6 +46,8 @@ import org.springframework.messaging.support.ExecutorSubscribableChannel;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.Validator;
|
||||
|
||||
/**
|
||||
* Provides essential configuration for handling messages with simple messaging
|
||||
@@ -62,13 +69,15 @@ import org.springframework.util.MimeTypeUtils;
|
||||
* to and from the client inbound/outbound channels (e.g. STOMP over WebSocket).
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 4.0
|
||||
*/
|
||||
public abstract class AbstractMessageBrokerConfiguration {
|
||||
public abstract class AbstractMessageBrokerConfiguration implements ApplicationContextAware {
|
||||
|
||||
private static final boolean jackson2Present= ClassUtils.isPresent(
|
||||
"com.fasterxml.jackson.databind.ObjectMapper", AbstractMessageBrokerConfiguration.class.getClassLoader());
|
||||
|
||||
private static final String MVC_VALIDATOR_NAME = "mvcValidator";
|
||||
|
||||
private ChannelRegistration clientInboundChannelRegistration;
|
||||
|
||||
@@ -76,6 +85,8 @@ public abstract class AbstractMessageBrokerConfiguration {
|
||||
|
||||
private MessageBrokerRegistry brokerRegistry;
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
|
||||
/**
|
||||
* Protected constructor.
|
||||
@@ -204,6 +215,7 @@ public abstract class AbstractMessageBrokerConfiguration {
|
||||
|
||||
handler.setDestinationPrefixes(getBrokerRegistry().getApplicationDestinationPrefixes());
|
||||
handler.setMessageConverter(brokerMessageConverter());
|
||||
handler.setValidator(simpValidator());
|
||||
return handler;
|
||||
}
|
||||
|
||||
@@ -268,6 +280,61 @@ public abstract class AbstractMessageBrokerConfiguration {
|
||||
return new DefaultUserSessionRegistry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to provide a custom {@link Validator}.
|
||||
* @since 4.0.1
|
||||
*/
|
||||
public Validator getValidator() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
public ApplicationContext getApplicationContext() {
|
||||
return applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link org.springframework.validation.Validator}s instance for validating
|
||||
* {@code @Payload} method arguments.
|
||||
* In order, this method tries to get a Validator instance:
|
||||
* <ul>
|
||||
* <li>delegating to getValidator() first</li>
|
||||
* <li>if none returned, getting an existing instance with its well-known name "mvcValidator", created by an MVC configuration</li>
|
||||
* <li>if none returned, checking the classpath for the presence of a JSR-303 implementation before creating a
|
||||
* {@code LocalValidatorFactoryBean}</li>
|
||||
* <li>returning a no-op Validator instance</li>
|
||||
* </ul>
|
||||
*/
|
||||
protected Validator simpValidator() {
|
||||
Validator validator = getValidator();
|
||||
if (validator == null) {
|
||||
if(this.applicationContext.containsBean(MVC_VALIDATOR_NAME)) {
|
||||
validator = this.applicationContext.getBean(MVC_VALIDATOR_NAME, Validator.class);
|
||||
}
|
||||
else if (ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) {
|
||||
Class<?> clazz;
|
||||
try {
|
||||
String className = "org.springframework.validation.beanvalidation.LocalValidatorFactoryBean";
|
||||
clazz = ClassUtils.forName(className, AbstractMessageBrokerConfiguration.class.getClassLoader());
|
||||
}
|
||||
catch (ClassNotFoundException e) {
|
||||
throw new BeanInitializationException("Could not find default validator", e);
|
||||
}
|
||||
catch (LinkageError e) {
|
||||
throw new BeanInitializationException("Could not find default validator", e);
|
||||
}
|
||||
validator = (Validator) BeanUtils.instantiate(clazz);
|
||||
}
|
||||
else {
|
||||
validator = noopValidator;
|
||||
}
|
||||
}
|
||||
return validator;
|
||||
}
|
||||
|
||||
private static final AbstractBrokerMessageHandler noopBroker = new AbstractBrokerMessageHandler(null) {
|
||||
|
||||
@@ -285,4 +352,15 @@ public abstract class AbstractMessageBrokerConfiguration {
|
||||
|
||||
};
|
||||
|
||||
private static final Validator noopValidator = new Validator() {
|
||||
@Override
|
||||
public boolean supports(Class<?> clazz) {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public void validate(Object target, Errors errors) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user