Skip to content

Commit bd054a4

Browse files
committed
Add method validation to Spring MVC
See gh-29825
1 parent cb04c3b commit bd054a4

File tree

15 files changed

+1000
-27
lines changed

15 files changed

+1000
-27
lines changed

spring-context/src/main/java/org/springframework/validation/DataBinder.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.HashMap;
2525
import java.util.List;
2626
import java.util.Map;
27+
import java.util.function.Predicate;
2728

2829
import org.apache.commons.logging.Log;
2930
import org.apache.commons.logging.LogFactory;
@@ -164,6 +165,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
164165

165166
private final List<Validator> validators = new ArrayList<>();
166167

168+
@Nullable
169+
private Predicate<Validator> excludedValidators;
170+
167171

168172
/**
169173
* Create a new DataBinder instance, with default object name.
@@ -580,6 +584,14 @@ private void assertValidators(Validator... validators) {
580584
}
581585
}
582586

587+
/**
588+
* Configure a predicate to exclude validators.
589+
* @since 6.1
590+
*/
591+
public void setExcludedValidators(Predicate<Validator> predicate) {
592+
this.excludedValidators = predicate;
593+
}
594+
583595
/**
584596
* Add Validators to apply after each binding step.
585597
* @see #setValidator(Validator)
@@ -616,6 +628,18 @@ public List<Validator> getValidators() {
616628
return Collections.unmodifiableList(this.validators);
617629
}
618630

631+
/**
632+
* Return the Validators to apply after data binding. This includes the
633+
* configured {@link #getValidators() validators} filtered by the
634+
* {@link #setExcludedValidators(Predicate) exclude predicate}.
635+
* @since 6.1
636+
*/
637+
public List<Validator> getValidatorsToApply() {
638+
return (this.excludedValidators != null ?
639+
this.validators.stream().filter(validator -> !this.excludedValidators.test(validator)).toList() :
640+
Collections.unmodifiableList(this.validators));
641+
}
642+
619643

620644
//---------------------------------------------------------------------
621645
// Implementation of PropertyEditorRegistry/TypeConverter interface
@@ -906,7 +930,7 @@ public void validate() {
906930
Assert.state(target != null, "No target to validate");
907931
BindingResult bindingResult = getBindingResult();
908932
// Call each validator with the same binding result
909-
for (Validator validator : getValidators()) {
933+
for (Validator validator : getValidatorsToApply()) {
910934
validator.validate(target, bindingResult);
911935
}
912936
}
@@ -924,7 +948,7 @@ public void validate(Object... validationHints) {
924948
Assert.state(target != null, "No target to validate");
925949
BindingResult bindingResult = getBindingResult();
926950
// Call each validator with the same binding result
927-
for (Validator validator : getValidators()) {
951+
for (Validator validator : getValidatorsToApply()) {
928952
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator smartValidator) {
929953
smartValidator.validate(target, bindingResult, validationHints);
930954
}

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

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -16,7 +16,11 @@
1616

1717
package org.springframework.web.bind.support;
1818

19+
import java.lang.annotation.Annotation;
20+
21+
import org.springframework.core.MethodParameter;
1922
import org.springframework.lang.Nullable;
23+
import org.springframework.validation.DataBinder;
2024
import org.springframework.web.bind.WebDataBinder;
2125
import org.springframework.web.context.request.NativeWebRequest;
2226

@@ -32,6 +36,8 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory {
3236
@Nullable
3337
private final WebBindingInitializer initializer;
3438

39+
private boolean methodValidationApplicable;
40+
3541

3642
/**
3743
* Create a new {@code DefaultDataBinderFactory} instance.
@@ -43,6 +49,17 @@ public DefaultDataBinderFactory(@Nullable WebBindingInitializer initializer) {
4349
}
4450

4551

52+
/**
53+
* Configure flag to signal whether validation will be applied to handler
54+
* method arguments, which is the case if Bean Validation is enabled in
55+
* Spring MVC, and method parameters have {@code @Constraint} annotations.
56+
* @since 6.1
57+
*/
58+
public void setMethodValidationApplicable(boolean methodValidationApplicable) {
59+
this.methodValidationApplicable = methodValidationApplicable;
60+
}
61+
62+
4663
/**
4764
* Create a new {@link WebDataBinder} for the given target object and
4865
* initialize it through a {@link WebBindingInitializer}.
@@ -87,4 +104,36 @@ protected void initBinder(WebDataBinder dataBinder, NativeWebRequest webRequest)
87104

88105
}
89106

107+
/**
108+
* {@inheritDoc}.
109+
* <p>By default, if the parameter has {@code @Valid}, Bean Validation is
110+
* excluded, deferring to method validation.
111+
*/
112+
@Override
113+
public WebDataBinder createBinder(
114+
NativeWebRequest webRequest, @Nullable Object target, String objectName,
115+
MethodParameter parameter) throws Exception {
116+
117+
WebDataBinder dataBinder = createBinder(webRequest, target, objectName);
118+
if (this.methodValidationApplicable) {
119+
MethodValidationInitializer.updateBinder(dataBinder, parameter);
120+
}
121+
return dataBinder;
122+
}
123+
124+
125+
/**
126+
* Excludes Bean Validation if the method parameter has {@code @Valid}.
127+
*/
128+
private static class MethodValidationInitializer {
129+
130+
public static void updateBinder(DataBinder binder, MethodParameter parameter) {
131+
for (Annotation annotation : parameter.getParameterAnnotations()) {
132+
if (annotation.annotationType().getName().equals("jakarta.validation.Valid")) {
133+
binder.setExcludedValidators(validator -> validator instanceof jakarta.validation.Validator);
134+
}
135+
}
136+
}
137+
}
138+
90139
}

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.web.bind.support;
1818

19+
import org.springframework.core.MethodParameter;
1920
import org.springframework.lang.Nullable;
2021
import org.springframework.web.bind.WebDataBinder;
2122
import org.springframework.web.context.request.NativeWebRequest;
@@ -24,6 +25,7 @@
2425
* A factory for creating a {@link WebDataBinder} instance for a named target object.
2526
*
2627
* @author Arjen Poutsma
28+
* @author Rossen Stoyanchev
2729
* @since 3.1
2830
*/
2931
public interface WebDataBinderFactory {
@@ -40,4 +42,18 @@ public interface WebDataBinderFactory {
4042
WebDataBinder createBinder(NativeWebRequest webRequest, @Nullable Object target, String objectName)
4143
throws Exception;
4244

45+
/**
46+
* Variant of {@link #createBinder(NativeWebRequest, Object, String)} with a
47+
* {@link MethodParameter} for which the {@code DataBinder} is created. This
48+
* may provide more insight to initialize the {@link WebDataBinder}.
49+
* @since 6.1
50+
*/
51+
default WebDataBinder createBinder(
52+
NativeWebRequest webRequest, @Nullable Object target, String objectName,
53+
MethodParameter parameter) throws Exception {
54+
55+
return createBinder(webRequest, target, objectName);
56+
}
57+
58+
4359
}

spring-web/src/main/java/org/springframework/web/method/HandlerMethod.java

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -22,6 +22,7 @@
2222
import java.util.Arrays;
2323
import java.util.List;
2424
import java.util.StringJoiner;
25+
import java.util.function.Predicate;
2526
import java.util.stream.Collectors;
2627
import java.util.stream.IntStream;
2728

@@ -35,6 +36,10 @@
3536
import org.springframework.core.MethodParameter;
3637
import org.springframework.core.ResolvableType;
3738
import org.springframework.core.annotation.AnnotatedElementUtils;
39+
import org.springframework.core.annotation.AnnotationUtils;
40+
import org.springframework.core.annotation.MergedAnnotation;
41+
import org.springframework.core.annotation.MergedAnnotationPredicates;
42+
import org.springframework.core.annotation.MergedAnnotations;
3843
import org.springframework.core.annotation.SynthesizingMethodParameter;
3944
import org.springframework.http.HttpStatusCode;
4045
import org.springframework.lang.NonNull;
@@ -44,6 +49,7 @@
4449
import org.springframework.util.ObjectUtils;
4550
import org.springframework.util.ReflectionUtils;
4651
import org.springframework.util.StringUtils;
52+
import org.springframework.validation.annotation.Validated;
4753
import org.springframework.web.bind.annotation.ResponseStatus;
4854

4955
/**
@@ -84,6 +90,10 @@ public class HandlerMethod {
8490

8591
private final MethodParameter[] parameters;
8692

93+
private final boolean validateArguments;
94+
95+
private final boolean validateReturnValue;
96+
8797
@Nullable
8898
private HttpStatusCode responseStatus;
8999

@@ -122,6 +132,8 @@ protected HandlerMethod(Object bean, Method method, @Nullable MessageSource mess
122132
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
123133
ReflectionUtils.makeAccessible(this.bridgedMethod);
124134
this.parameters = initMethodParameters();
135+
this.validateArguments = MethodValidationInitializer.checkArguments(this.beanType, this.parameters);
136+
this.validateReturnValue = MethodValidationInitializer.checkReturnValue(this.beanType, this.bridgedMethod);
125137
evaluateResponseStatus();
126138
this.description = initDescription(this.beanType, this.method);
127139
}
@@ -141,6 +153,8 @@ public HandlerMethod(Object bean, String methodName, Class<?>... parameterTypes)
141153
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(this.method);
142154
ReflectionUtils.makeAccessible(this.bridgedMethod);
143155
this.parameters = initMethodParameters();
156+
this.validateArguments = MethodValidationInitializer.checkArguments(this.beanType, this.parameters);
157+
this.validateReturnValue = MethodValidationInitializer.checkReturnValue(this.beanType, this.bridgedMethod);
144158
evaluateResponseStatus();
145159
this.description = initDescription(this.beanType, this.method);
146160
}
@@ -177,6 +191,8 @@ public HandlerMethod(
177191
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
178192
ReflectionUtils.makeAccessible(this.bridgedMethod);
179193
this.parameters = initMethodParameters();
194+
this.validateArguments = MethodValidationInitializer.checkArguments(this.beanType, this.parameters);
195+
this.validateReturnValue = MethodValidationInitializer.checkReturnValue(this.beanType, this.bridgedMethod);
180196
evaluateResponseStatus();
181197
this.description = initDescription(this.beanType, this.method);
182198
}
@@ -193,6 +209,8 @@ protected HandlerMethod(HandlerMethod handlerMethod) {
193209
this.method = handlerMethod.method;
194210
this.bridgedMethod = handlerMethod.bridgedMethod;
195211
this.parameters = handlerMethod.parameters;
212+
this.validateArguments = handlerMethod.validateArguments;
213+
this.validateReturnValue = handlerMethod.validateReturnValue;
196214
this.responseStatus = handlerMethod.responseStatus;
197215
this.responseStatusReason = handlerMethod.responseStatusReason;
198216
this.description = handlerMethod.description;
@@ -212,6 +230,8 @@ private HandlerMethod(HandlerMethod handlerMethod, Object handler) {
212230
this.method = handlerMethod.method;
213231
this.bridgedMethod = handlerMethod.bridgedMethod;
214232
this.parameters = handlerMethod.parameters;
233+
this.validateArguments = handlerMethod.validateArguments;
234+
this.validateReturnValue = handlerMethod.validateReturnValue;
215235
this.responseStatus = handlerMethod.responseStatus;
216236
this.responseStatusReason = handlerMethod.responseStatusReason;
217237
this.resolvedFromHandlerMethod = handlerMethod;
@@ -290,6 +310,33 @@ public MethodParameter[] getMethodParameters() {
290310
return this.parameters;
291311
}
292312

313+
/**
314+
* Whether the method arguments are a candidate for method validation, which
315+
* is the case when there are parameter {@code jakarta.validation.Constraint}
316+
* annotations.
317+
* <p>The presence of {@code jakarta.validation.Valid} by itself does not
318+
* trigger method validation since such parameters are already validated at
319+
* the level of argument resolvers.
320+
* <p><strong>Note:</strong> if the class is annotated with {@link Validated},
321+
* this method returns false, deferring to method validation via AOP proxy.
322+
* @since 6.1
323+
*/
324+
public boolean shouldValidateArguments() {
325+
return this.validateArguments;
326+
}
327+
328+
/**
329+
* Whether the method return value is a candidate for method validation, which
330+
* is the case when there are method {@code jakarta.validation.Constraint}
331+
* or {@code jakarta.validation.Valid} annotations.
332+
* <p><strong>Note:</strong> if the class is annotated with {@link Validated},
333+
* this method returns false, deferring to method validation via AOP proxy.
334+
* @since 6.1
335+
*/
336+
public boolean shouldValidateReturnValue() {
337+
return this.validateReturnValue;
338+
}
339+
293340
/**
294341
* Return the specified response status, if any.
295342
* @since 4.3.8
@@ -603,4 +650,38 @@ public ReturnValueMethodParameter clone() {
603650
}
604651
}
605652

653+
654+
/**
655+
* Checks for the presence of {@code @Constraint} and {@code @Valid}
656+
* annotations on the method and method parameters.
657+
*/
658+
private static class MethodValidationInitializer {
659+
660+
private static final Predicate<MergedAnnotation<? extends Annotation>> INPUT_PREDICATE =
661+
MergedAnnotationPredicates.typeIn("jakarta.validation.Constraint");
662+
663+
private static final Predicate<MergedAnnotation<? extends Annotation>> OUTPUT_PREDICATE =
664+
MergedAnnotationPredicates.typeIn("jakarta.validation.Valid", "jakarta.validation.Constraint");
665+
666+
public static boolean checkArguments(Class<?> beanType, MethodParameter[] parameters) {
667+
if (AnnotationUtils.findAnnotation(beanType, Validated.class) == null) {
668+
for (MethodParameter parameter : parameters) {
669+
MergedAnnotations merged = MergedAnnotations.from(parameter.getParameterAnnotations());
670+
if (merged.stream().anyMatch(INPUT_PREDICATE)) {
671+
return true;
672+
}
673+
}
674+
}
675+
return false;
676+
}
677+
678+
public static boolean checkReturnValue(Class<?> beanType, Method method) {
679+
if (AnnotationUtils.findAnnotation(beanType, Validated.class) == null) {
680+
MergedAnnotations merged = MergedAnnotations.from(method, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY);
681+
return merged.stream().anyMatch(OUTPUT_PREDICATE);
682+
}
683+
return false;
684+
}
685+
}
686+
606687
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAn
164164
if (bindingResult == null) {
165165
// Bean property binding and validation;
166166
// skipped in case of binding failure on construction.
167-
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
167+
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name, parameter);
168168
if (binder.getTarget() != null) {
169169
if (!mavContainer.isBindingDisabled(name)) {
170170
bindRequestParameters(binder, webRequest);
@@ -251,7 +251,7 @@ protected Object constructAttribute(Constructor<?> ctor, String attributeName, M
251251
String[] paramNames = BeanUtils.getParameterNames(ctor);
252252
Class<?>[] paramTypes = ctor.getParameterTypes();
253253
Object[] args = new Object[paramTypes.length];
254-
WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName);
254+
WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName, parameter);
255255
String fieldDefaultPrefix = binder.getFieldDefaultPrefix();
256256
String fieldMarkerPrefix = binder.getFieldMarkerPrefix();
257257
boolean bindingFailure = false;

0 commit comments

Comments
 (0)