Allow custom @Validated annotations for handler method parameters

Issue: SPR-12406
This commit is contained in:
Juergen Hoeller
2014-11-24 23:34:24 +01:00
parent 2602bcbb7c
commit 929cda6790
8 changed files with 91 additions and 56 deletions

View File

@@ -34,14 +34,15 @@ import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.SmartValidator;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
/**
* A resolver to extract and convert the payload of a message using a
* {@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.
* <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
@@ -70,14 +71,14 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public Object resolveArgument(MethodParameter param, Message<?> message) throws Exception {
Payload annot = param.getParameterAnnotation(Payload.class);
if ((annot != null) && StringUtils.hasText(annot.value())) {
Payload ann = param.getParameterAnnotation(Payload.class);
if (ann != null && StringUtils.hasText(ann.value())) {
throw new IllegalStateException("@Payload SpEL expressions not supported by this resolver");
}
Object payload = message.getPayload();
if (isEmptyPayload(payload)) {
if (annot == null || annot.required()) {
if (ann == null || ann.required()) {
String paramName = getParameterName(param);
BindingResult bindingResult = new BeanPropertyBindingResult(payload, paramName);
bindingResult.addError(new ObjectError(paramName, "@Payload param is required"));
@@ -97,7 +98,7 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver {
payload = this.converter.fromMessage(message, targetClass);
if (payload == null) {
throw new MessageConversionException(message,
"No converter found to convert to " + targetClass + ", message=" + message, null);
"No converter found to convert to " + targetClass + ", message=" + message);
}
validate(message, param, payload);
return payload;
@@ -106,7 +107,7 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver {
private String getParameterName(MethodParameter param) {
String paramName = param.getParameterName();
return (paramName == null ? "Arg " + param.getParameterIndex() : paramName);
return (paramName != null ? paramName : "Arg " + param.getParameterIndex());
}
/**
@@ -132,26 +133,22 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver {
if (this.validator == null) {
return;
}
for (Annotation annot : parameter.getParameterAnnotations()) {
if (annot.annotationType().getSimpleName().startsWith("Valid")) {
for (Annotation ann : parameter.getParameterAnnotations()) {
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
BeanPropertyBindingResult bindingResult =
new BeanPropertyBindingResult(target, getParameterName(parameter));
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 {
this.validator.validate(target, bindingResult);
}
if (bindingResult.hasErrors()) {
throw new MethodArgumentNotValidException(message, parameter, bindingResult);
}
break;
}
}

View File

@@ -16,6 +16,10 @@
package org.springframework.messaging.handler.annotation.support;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Locale;
@@ -72,10 +76,8 @@ public class PayloadArgumentResolverTests {
@Before
public void setup() throws Exception {
this.resolver = new PayloadArgumentResolver(new StringMessageConverter(), testValidator());
payloadMethod = PayloadArgumentResolverTests.class.getDeclaredMethod("handleMessage",
this.payloadMethod = PayloadArgumentResolverTests.class.getDeclaredMethod("handleMessage",
String.class, String.class, Locale.class, String.class, String.class, String.class, String.class);
this.paramAnnotated = getMethodParameter(this.payloadMethod, 0);
@@ -115,7 +117,6 @@ public class PayloadArgumentResolverTests {
@Test
public void resolveNotRequired() throws Exception {
Message<?> emptyByteArrayMessage = MessageBuilder.withPayload(new byte[0]).build();
assertNull(this.resolver.resolveArgument(this.paramAnnotatedNotRequired, emptyByteArrayMessage));
@@ -168,14 +169,12 @@ public class PayloadArgumentResolverTests {
@Test
public void resolveNonAnnotatedParameter() throws Exception {
Message<?> notEmptyMessage = MessageBuilder.withPayload("ABC".getBytes()).build();
assertEquals("ABC", this.resolver.resolveArgument(this.paramNotAnnotated, notEmptyMessage));
Message<?> emptyStringMessage = MessageBuilder.withPayload("").build();
thrown.expect(MethodArgumentNotValidException.class);
this.resolver.resolveArgument(this.paramValidated, emptyStringMessage);
}
@Test
@@ -188,8 +187,8 @@ public class PayloadArgumentResolverTests {
assertEquals("invalidValue", this.resolver.resolveArgument(this.paramValidatedNotAnnotated, message));
}
private Validator testValidator() {
private Validator testValidator() {
return new Validator() {
@Override
public boolean supports(Class<?> clazz) {
@@ -216,9 +215,16 @@ public class PayloadArgumentResolverTests {
@Payload(required=false) String paramNotRequired,
@Payload(required=true) Locale nonConvertibleRequiredParam,
@Payload("foo.bar") String paramWithSpelExpression,
@Validated @Payload String validParam,
@MyValid @Payload String validParam,
@Validated String validParamNotAnnotated,
String paramNotAnnotated) {
}
@Validated
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyValid {
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 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.
@@ -59,6 +59,7 @@ import org.springframework.util.ReflectionUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.CookieValue;
@@ -82,11 +83,11 @@ import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartRequest;
/**
* Support class for invoking an annotated handler method. Operates on the introspection results of a {@link
* HandlerMethodResolver} for a specific handler type.
* Support class for invoking an annotated handler method. Operates on the introspection
* results of a {@link HandlerMethodResolver} for a specific handler type.
*
* <p>Used by {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter} and {@link
* org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter}.
* <p>Used by {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter}
* and {@link org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter}.
*
* @author Juergen Hoeller
* @author Arjen Poutsma
@@ -295,10 +296,13 @@ public class HandlerMethodInvoker {
else if (Value.class.isInstance(paramAnn)) {
defaultValue = ((Value) paramAnn).value();
}
else if (paramAnn.annotationType().getSimpleName().startsWith("Valid")) {
validate = true;
Object value = AnnotationUtils.getValue(paramAnn);
validationHints = (value instanceof Object[] ? (Object[]) value : new Object[] {value});
else {
Validated validatedAnn = AnnotationUtils.getAnnotation(paramAnn, Validated.class);
if (validatedAnn != null || paramAnn.annotationType().getSimpleName().startsWith("Valid")) {
validate = true;
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(paramAnn));
validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[]{hints});
}
}
}

View File

@@ -27,6 +27,7 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.Errors;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.support.WebDataBinderFactory;
@@ -157,9 +158,11 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
if (ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = AnnotationUtils.getValue(ann);
binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
binder.validate(validationHints);
break;
}
}

View File

@@ -31,6 +31,7 @@ import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.RequestBody;
@@ -223,10 +224,12 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
private void validate(WebDataBinder binder, MethodParameter parameter) throws MethodArgumentNotValidException {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation annot : annotations) {
if (annot.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = AnnotationUtils.getValue(annot);
binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
for (Annotation ann : annotations) {
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
binder.validate(validationHints);
BindingResult bindingResult = binder.getBindingResult();
if (bindingResult.hasErrors()) {
if (isBindingErrorFatal(parameter)) {

View File

@@ -33,6 +33,7 @@ import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.accept.ContentNegotiationManager;
@@ -114,9 +115,11 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
private void validate(WebDataBinder binder, MethodParameter parameter) throws Exception {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
if (ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = AnnotationUtils.getValue(ann);
binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
binder.validate(validationHints);
BindingResult bindingResult = binder.getBindingResult();
if (bindingResult.hasErrors()) {
if (isBindExceptionRequired(binder, parameter)) {

View File

@@ -16,8 +16,10 @@
package org.springframework.web.servlet.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
@@ -150,6 +152,7 @@ public class MvcNamespaceTests {
private HandlerMethod handlerMethod;
@Before
public void setUp() throws Exception {
TestMockServletContext servletContext = new TestMockServletContext();
@@ -187,7 +190,7 @@ public class MvcNamespaceTests {
List<HttpMessageConverter<?>> converters = adapter.getMessageConverters();
assertTrue(converters.size() > 0);
for(HttpMessageConverter<?> converter : converters) {
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof AbstractJackson2HttpMessageConverter) {
ObjectMapper objectMapper = ((AbstractJackson2HttpMessageConverter)converter).getObjectMapper();
assertFalse(objectMapper.getDeserializationConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION));
@@ -262,9 +265,6 @@ public class MvcNamespaceTests {
doTestCustomValidator("mvc-config-custom-validator-32.xml");
}
/**
* @throws Exception
*/
private void doTestCustomValidator(String xml) throws Exception {
loadBeanDefinitions(xml, 13);
@@ -808,12 +808,11 @@ public class MvcNamespaceTests {
assertEquals(TestPathHelper.class, viewController.getUrlPathHelper().getClass());
assertEquals(TestPathMatcher.class, viewController.getPathMatcher().getClass());
for(SimpleUrlHandlerMapping handlerMapping : appContext.getBeansOfType(SimpleUrlHandlerMapping.class).values()) {
for (SimpleUrlHandlerMapping handlerMapping : appContext.getBeansOfType(SimpleUrlHandlerMapping.class).values()) {
assertNotNull(handlerMapping);
assertEquals(TestPathHelper.class, handlerMapping.getUrlPathHelper().getClass());
assertEquals(TestPathMatcher.class, handlerMapping.getPathMatcher().getClass());
}
}
@@ -827,13 +826,25 @@ public class MvcNamespaceTests {
}
@DateTimeFormat(iso=ISO.DATE)
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface IsoDate {
}
@Validated(MyGroup.class)
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyValid {
}
@Controller
public static class TestController {
private boolean recordedValidationError;
@RequestMapping
public void testBind(@RequestParam @DateTimeFormat(iso=ISO.DATE) Date date, @Validated(MyGroup.class) TestBean bean, BindingResult result) {
public void testBind(@RequestParam @IsoDate Date date, @MyValid TestBean bean, BindingResult result) {
this.recordedValidationError = (result.getErrorCount() == 1);
}
}
@@ -885,9 +896,11 @@ public class MvcNamespaceTests {
}
}
public static class TestCallableProcessingInterceptor extends CallableProcessingInterceptorAdapter { }
public static class TestCallableProcessingInterceptor extends CallableProcessingInterceptorAdapter {
}
public static class TestDeferredResultProcessingInterceptor extends DeferredResultProcessingInterceptorAdapter { }
public static class TestDeferredResultProcessingInterceptor extends DeferredResultProcessingInterceptorAdapter {
}
public static class TestPathMatcher implements PathMatcher {
@@ -927,9 +940,11 @@ public class MvcNamespaceTests {
}
}
public static class TestPathHelper extends UrlPathHelper { }
public static class TestPathHelper extends UrlPathHelper {
}
public static class TestCacheManager implements CacheManager {
@Override
public Cache getCache(String name) {
return new ConcurrentMapCache(name);

View File

@@ -2388,7 +2388,12 @@ public class ServletAnnotationControllerTests {
@Controller
private static class MyTypedCommandProvidingFormController
extends MyCommandProvidingFormController<Integer, TestBean, ITestBean> {
}
@Validated(MyGroup.class)
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyValid {
}
@Controller
@@ -2409,7 +2414,7 @@ public class ServletAnnotationControllerTests {
@Override
@RequestMapping("/myPath.do")
public String myHandle(@ModelAttribute("myCommand") @Validated(MyGroup.class) TestBean tb, BindingResult errors, ModelMap model) {
public String myHandle(@ModelAttribute("myCommand") @MyValid TestBean tb, BindingResult errors, ModelMap model) {
if (!errors.hasFieldErrors("sex")) {
throw new IllegalStateException("requiredFields not applied");
}
@@ -2712,11 +2717,10 @@ public class ServletAnnotationControllerTests {
}
}
@Controller
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Controller
public @interface MyControllerAnnotation {
}
@MyControllerAnnotation