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:
Brian Clozel
2014-01-02 15:33:07 +01:00
parent 1c83e8653a
commit 2c8f670d5f
9 changed files with 335 additions and 29 deletions

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}

View File

@@ -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) {
}
};
}