Skip to content

Commit 929cda6

Browse files
committed
Allow custom @validated annotations for handler method parameters
Issue: SPR-12406
1 parent 2602bcb commit 929cda6

File tree

8 files changed

+91
-56
lines changed

8 files changed

+91
-56
lines changed

spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolver.java

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,15 @@
3434
import org.springframework.validation.ObjectError;
3535
import org.springframework.validation.SmartValidator;
3636
import org.springframework.validation.Validator;
37+
import org.springframework.validation.annotation.Validated;
3738

3839
/**
3940
* A resolver to extract and convert the payload of a message using a
4041
* {@link MessageConverter}. It also validates the payload using a
4142
* {@link Validator} if the argument is annotated with a Validation annotation.
4243
*
43-
* <p>This {@link HandlerMethodArgumentResolver} should be ordered last as it supports all
44-
* types and does not require the {@link Payload} annotation.
44+
* <p>This {@link HandlerMethodArgumentResolver} should be ordered last as it
45+
* supports all types and does not require the {@link Payload} annotation.
4546
*
4647
* @author Rossen Stoyanchev
4748
* @author Brian Clozel
@@ -70,14 +71,14 @@ public boolean supportsParameter(MethodParameter parameter) {
7071

7172
@Override
7273
public Object resolveArgument(MethodParameter param, Message<?> message) throws Exception {
73-
Payload annot = param.getParameterAnnotation(Payload.class);
74-
if ((annot != null) && StringUtils.hasText(annot.value())) {
74+
Payload ann = param.getParameterAnnotation(Payload.class);
75+
if (ann != null && StringUtils.hasText(ann.value())) {
7576
throw new IllegalStateException("@Payload SpEL expressions not supported by this resolver");
7677
}
7778

7879
Object payload = message.getPayload();
7980
if (isEmptyPayload(payload)) {
80-
if (annot == null || annot.required()) {
81+
if (ann == null || ann.required()) {
8182
String paramName = getParameterName(param);
8283
BindingResult bindingResult = new BeanPropertyBindingResult(payload, paramName);
8384
bindingResult.addError(new ObjectError(paramName, "@Payload param is required"));
@@ -97,7 +98,7 @@ public Object resolveArgument(MethodParameter param, Message<?> message) throws
9798
payload = this.converter.fromMessage(message, targetClass);
9899
if (payload == null) {
99100
throw new MessageConversionException(message,
100-
"No converter found to convert to " + targetClass + ", message=" + message, null);
101+
"No converter found to convert to " + targetClass + ", message=" + message);
101102
}
102103
validate(message, param, payload);
103104
return payload;
@@ -106,7 +107,7 @@ public Object resolveArgument(MethodParameter param, Message<?> message) throws
106107

107108
private String getParameterName(MethodParameter param) {
108109
String paramName = param.getParameterName();
109-
return (paramName == null ? "Arg " + param.getParameterIndex() : paramName);
110+
return (paramName != null ? paramName : "Arg " + param.getParameterIndex());
110111
}
111112

112113
/**
@@ -132,26 +133,22 @@ protected void validate(Message<?> message, MethodParameter parameter, Object ta
132133
if (this.validator == null) {
133134
return;
134135
}
135-
136-
for (Annotation annot : parameter.getParameterAnnotations()) {
137-
if (annot.annotationType().getSimpleName().startsWith("Valid")) {
136+
for (Annotation ann : parameter.getParameterAnnotations()) {
137+
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
138+
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
139+
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
140+
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
138141
BeanPropertyBindingResult bindingResult =
139142
new BeanPropertyBindingResult(target, getParameterName(parameter));
140-
141-
Object hints = AnnotationUtils.getValue(annot);
142-
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
143-
144143
if (!ObjectUtils.isEmpty(validationHints) && this.validator instanceof SmartValidator) {
145144
((SmartValidator) this.validator).validate(target, bindingResult, validationHints);
146145
}
147146
else {
148147
this.validator.validate(target, bindingResult);
149148
}
150-
151149
if (bindingResult.hasErrors()) {
152150
throw new MethodArgumentNotValidException(message, parameter, bindingResult);
153151
}
154-
155152
break;
156153
}
157154
}

spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PayloadArgumentResolverTests.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
package org.springframework.messaging.handler.annotation.support;
1818

19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
1923
import java.lang.reflect.Method;
2024
import java.util.Locale;
2125

@@ -72,10 +76,8 @@ public class PayloadArgumentResolverTests {
7276

7377
@Before
7478
public void setup() throws Exception {
75-
7679
this.resolver = new PayloadArgumentResolver(new StringMessageConverter(), testValidator());
77-
78-
payloadMethod = PayloadArgumentResolverTests.class.getDeclaredMethod("handleMessage",
80+
this.payloadMethod = PayloadArgumentResolverTests.class.getDeclaredMethod("handleMessage",
7981
String.class, String.class, Locale.class, String.class, String.class, String.class, String.class);
8082

8183
this.paramAnnotated = getMethodParameter(this.payloadMethod, 0);
@@ -115,7 +117,6 @@ public void resolveRequiredEmptyNonAnnotatedParameter() throws Exception {
115117

116118
@Test
117119
public void resolveNotRequired() throws Exception {
118-
119120
Message<?> emptyByteArrayMessage = MessageBuilder.withPayload(new byte[0]).build();
120121
assertNull(this.resolver.resolveArgument(this.paramAnnotatedNotRequired, emptyByteArrayMessage));
121122

@@ -168,14 +169,12 @@ public void resolveFailValidationNoConversionNecessary() throws Exception {
168169

169170
@Test
170171
public void resolveNonAnnotatedParameter() throws Exception {
171-
172172
Message<?> notEmptyMessage = MessageBuilder.withPayload("ABC".getBytes()).build();
173173
assertEquals("ABC", this.resolver.resolveArgument(this.paramNotAnnotated, notEmptyMessage));
174174

175175
Message<?> emptyStringMessage = MessageBuilder.withPayload("").build();
176176
thrown.expect(MethodArgumentNotValidException.class);
177177
this.resolver.resolveArgument(this.paramValidated, emptyStringMessage);
178-
179178
}
180179

181180
@Test
@@ -188,8 +187,8 @@ public void resolveNonAnnotatedParameterFailValidation() throws Exception {
188187
assertEquals("invalidValue", this.resolver.resolveArgument(this.paramValidatedNotAnnotated, message));
189188
}
190189

191-
private Validator testValidator() {
192190

191+
private Validator testValidator() {
193192
return new Validator() {
194193
@Override
195194
public boolean supports(Class<?> clazz) {
@@ -216,9 +215,16 @@ private void handleMessage(
216215
@Payload(required=false) String paramNotRequired,
217216
@Payload(required=true) Locale nonConvertibleRequiredParam,
218217
@Payload("foo.bar") String paramWithSpelExpression,
219-
@Validated @Payload String validParam,
218+
@MyValid @Payload String validParam,
220219
@Validated String validParamNotAnnotated,
221220
String paramNotAnnotated) {
222221
}
223222

223+
224+
@Validated
225+
@Target({ElementType.PARAMETER})
226+
@Retention(RetentionPolicy.RUNTIME)
227+
public @interface MyValid {
228+
}
229+
224230
}

spring-web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2014 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.
@@ -59,6 +59,7 @@
5959
import org.springframework.validation.BindException;
6060
import org.springframework.validation.BindingResult;
6161
import org.springframework.validation.Errors;
62+
import org.springframework.validation.annotation.Validated;
6263
import org.springframework.web.HttpMediaTypeNotSupportedException;
6364
import org.springframework.web.bind.WebDataBinder;
6465
import org.springframework.web.bind.annotation.CookieValue;
@@ -82,11 +83,11 @@
8283
import org.springframework.web.multipart.MultipartRequest;
8384

8485
/**
85-
* Support class for invoking an annotated handler method. Operates on the introspection results of a {@link
86-
* HandlerMethodResolver} for a specific handler type.
86+
* Support class for invoking an annotated handler method. Operates on the introspection
87+
* results of a {@link HandlerMethodResolver} for a specific handler type.
8788
*
88-
* <p>Used by {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter} and {@link
89-
* org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter}.
89+
* <p>Used by {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter}
90+
* and {@link org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter}.
9091
*
9192
* @author Juergen Hoeller
9293
* @author Arjen Poutsma
@@ -295,10 +296,13 @@ else if (ModelAttribute.class.isInstance(paramAnn)) {
295296
else if (Value.class.isInstance(paramAnn)) {
296297
defaultValue = ((Value) paramAnn).value();
297298
}
298-
else if (paramAnn.annotationType().getSimpleName().startsWith("Valid")) {
299-
validate = true;
300-
Object value = AnnotationUtils.getValue(paramAnn);
301-
validationHints = (value instanceof Object[] ? (Object[]) value : new Object[] {value});
299+
else {
300+
Validated validatedAnn = AnnotationUtils.getAnnotation(paramAnn, Validated.class);
301+
if (validatedAnn != null || paramAnn.annotationType().getSimpleName().startsWith("Valid")) {
302+
validate = true;
303+
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(paramAnn));
304+
validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[]{hints});
305+
}
302306
}
303307
}
304308

spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.core.annotation.AnnotationUtils;
2828
import org.springframework.validation.BindException;
2929
import org.springframework.validation.Errors;
30+
import org.springframework.validation.annotation.Validated;
3031
import org.springframework.web.bind.WebDataBinder;
3132
import org.springframework.web.bind.annotation.ModelAttribute;
3233
import org.springframework.web.bind.support.WebDataBinderFactory;
@@ -157,9 +158,11 @@ protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest requ
157158
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
158159
Annotation[] annotations = parameter.getParameterAnnotations();
159160
for (Annotation ann : annotations) {
160-
if (ann.annotationType().getSimpleName().startsWith("Valid")) {
161-
Object hints = AnnotationUtils.getValue(ann);
162-
binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
161+
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
162+
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
163+
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
164+
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
165+
binder.validate(validationHints);
163166
break;
164167
}
165168
}

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.util.Assert;
3232
import org.springframework.validation.BindingResult;
3333
import org.springframework.validation.Errors;
34+
import org.springframework.validation.annotation.Validated;
3435
import org.springframework.web.bind.MethodArgumentNotValidException;
3536
import org.springframework.web.bind.WebDataBinder;
3637
import org.springframework.web.bind.annotation.RequestBody;
@@ -223,10 +224,12 @@ private Class<?> getCollectionParameterType(MethodParameter parameter) {
223224

224225
private void validate(WebDataBinder binder, MethodParameter parameter) throws MethodArgumentNotValidException {
225226
Annotation[] annotations = parameter.getParameterAnnotations();
226-
for (Annotation annot : annotations) {
227-
if (annot.annotationType().getSimpleName().startsWith("Valid")) {
228-
Object hints = AnnotationUtils.getValue(annot);
229-
binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
227+
for (Annotation ann : annotations) {
228+
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
229+
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
230+
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
231+
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
232+
binder.validate(validationHints);
230233
BindingResult bindingResult = binder.getBindingResult();
231234
if (bindingResult.hasErrors()) {
232235
if (isBindingErrorFatal(parameter)) {

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.http.server.ServletServerHttpRequest;
3434
import org.springframework.validation.BindingResult;
3535
import org.springframework.validation.Errors;
36+
import org.springframework.validation.annotation.Validated;
3637
import org.springframework.web.HttpMediaTypeNotAcceptableException;
3738
import org.springframework.web.HttpMediaTypeNotSupportedException;
3839
import org.springframework.web.accept.ContentNegotiationManager;
@@ -114,9 +115,11 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m
114115
private void validate(WebDataBinder binder, MethodParameter parameter) throws Exception {
115116
Annotation[] annotations = parameter.getParameterAnnotations();
116117
for (Annotation ann : annotations) {
117-
if (ann.annotationType().getSimpleName().startsWith("Valid")) {
118-
Object hints = AnnotationUtils.getValue(ann);
119-
binder.validate(hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
118+
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
119+
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
120+
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
121+
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
122+
binder.validate(validationHints);
120123
BindingResult bindingResult = binder.getBindingResult();
121124
if (bindingResult.hasErrors()) {
122125
if (isBindExceptionRequired(binder, parameter)) {

spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616

1717
package org.springframework.web.servlet.config;
1818

19+
import java.lang.annotation.ElementType;
1920
import java.lang.annotation.Retention;
2021
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
2123
import java.lang.reflect.Method;
2224
import java.util.Arrays;
2325
import java.util.Collection;
@@ -150,6 +152,7 @@ public class MvcNamespaceTests {
150152

151153
private HandlerMethod handlerMethod;
152154

155+
153156
@Before
154157
public void setUp() throws Exception {
155158
TestMockServletContext servletContext = new TestMockServletContext();
@@ -187,7 +190,7 @@ public void testDefaultConfig() throws Exception {
187190

188191
List<HttpMessageConverter<?>> converters = adapter.getMessageConverters();
189192
assertTrue(converters.size() > 0);
190-
for(HttpMessageConverter<?> converter : converters) {
193+
for (HttpMessageConverter<?> converter : converters) {
191194
if (converter instanceof AbstractJackson2HttpMessageConverter) {
192195
ObjectMapper objectMapper = ((AbstractJackson2HttpMessageConverter)converter).getObjectMapper();
193196
assertFalse(objectMapper.getDeserializationConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION));
@@ -262,9 +265,6 @@ public void testCustomValidator32() throws Exception {
262265
doTestCustomValidator("mvc-config-custom-validator-32.xml");
263266
}
264267

265-
/**
266-
* @throws Exception
267-
*/
268268
private void doTestCustomValidator(String xml) throws Exception {
269269
loadBeanDefinitions(xml, 13);
270270

@@ -808,12 +808,11 @@ public void testPathMatchingHandlerMappings() throws Exception {
808808
assertEquals(TestPathHelper.class, viewController.getUrlPathHelper().getClass());
809809
assertEquals(TestPathMatcher.class, viewController.getPathMatcher().getClass());
810810

811-
for(SimpleUrlHandlerMapping handlerMapping : appContext.getBeansOfType(SimpleUrlHandlerMapping.class).values()) {
811+
for (SimpleUrlHandlerMapping handlerMapping : appContext.getBeansOfType(SimpleUrlHandlerMapping.class).values()) {
812812
assertNotNull(handlerMapping);
813813
assertEquals(TestPathHelper.class, handlerMapping.getUrlPathHelper().getClass());
814814
assertEquals(TestPathMatcher.class, handlerMapping.getPathMatcher().getClass());
815815
}
816-
817816
}
818817

819818

@@ -827,13 +826,25 @@ private void loadBeanDefinitions(String fileName, int expectedBeanCount) {
827826
}
828827

829828

829+
@DateTimeFormat(iso=ISO.DATE)
830+
@Target({ElementType.PARAMETER})
831+
@Retention(RetentionPolicy.RUNTIME)
832+
public @interface IsoDate {
833+
}
834+
835+
@Validated(MyGroup.class)
836+
@Target({ElementType.PARAMETER})
837+
@Retention(RetentionPolicy.RUNTIME)
838+
public @interface MyValid {
839+
}
840+
830841
@Controller
831842
public static class TestController {
832843

833844
private boolean recordedValidationError;
834845

835846
@RequestMapping
836-
public void testBind(@RequestParam @DateTimeFormat(iso=ISO.DATE) Date date, @Validated(MyGroup.class) TestBean bean, BindingResult result) {
847+
public void testBind(@RequestParam @IsoDate Date date, @MyValid TestBean bean, BindingResult result) {
837848
this.recordedValidationError = (result.getErrorCount() == 1);
838849
}
839850
}
@@ -885,9 +896,11 @@ public RequestDispatcher getNamedDispatcher(String path) {
885896
}
886897
}
887898

888-
public static class TestCallableProcessingInterceptor extends CallableProcessingInterceptorAdapter { }
899+
public static class TestCallableProcessingInterceptor extends CallableProcessingInterceptorAdapter {
900+
}
889901

890-
public static class TestDeferredResultProcessingInterceptor extends DeferredResultProcessingInterceptorAdapter { }
902+
public static class TestDeferredResultProcessingInterceptor extends DeferredResultProcessingInterceptorAdapter {
903+
}
891904

892905
public static class TestPathMatcher implements PathMatcher {
893906

@@ -927,9 +940,11 @@ public String combine(String pattern1, String pattern2) {
927940
}
928941
}
929942

930-
public static class TestPathHelper extends UrlPathHelper { }
943+
public static class TestPathHelper extends UrlPathHelper {
944+
}
931945

932946
public static class TestCacheManager implements CacheManager {
947+
933948
@Override
934949
public Cache getCache(String name) {
935950
return new ConcurrentMapCache(name);

0 commit comments

Comments
 (0)