Skip to content

Commit bec7210

Browse files
committed
Improve check to skip bean validation in DataBinder
DataBinder should skip any jakarta.validation.Validator yielding to method validation when that is expected. This change improves the check to skip by unwrapping the validator from in a SmartValidator before checking if it is for bean validation. Closes gh-31711
1 parent 0a94dce commit bec7210

File tree

4 files changed

+214
-2
lines changed

4 files changed

+214
-2
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.core.ResolvableType;
2323
import org.springframework.lang.Nullable;
2424
import org.springframework.validation.DataBinder;
25+
import org.springframework.validation.SmartValidator;
2526
import org.springframework.web.bind.WebDataBinder;
2627
import org.springframework.web.context.request.NativeWebRequest;
2728

@@ -148,7 +149,8 @@ private static class MethodValidationInitializer {
148149
public static void initBinder(DataBinder binder, MethodParameter parameter) {
149150
for (Annotation annotation : parameter.getParameterAnnotations()) {
150151
if (annotation.annotationType().getName().equals("jakarta.validation.Valid")) {
151-
binder.setExcludedValidators(validator -> validator instanceof jakarta.validation.Validator);
152+
binder.setExcludedValidators(v -> v instanceof jakarta.validation.Validator ||
153+
v instanceof SmartValidator sv && sv.unwrap(jakarta.validation.Validator.class) != null);
152154
}
153155
}
154156
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.bind.support;
18+
19+
import jakarta.validation.Valid;
20+
import org.junit.jupiter.api.Test;
21+
22+
import org.springframework.core.MethodParameter;
23+
import org.springframework.core.ResolvableType;
24+
import org.springframework.validation.Errors;
25+
import org.springframework.validation.SmartValidator;
26+
import org.springframework.validation.Validator;
27+
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
28+
import org.springframework.web.bind.WebDataBinder;
29+
import org.springframework.web.context.request.ServletWebRequest;
30+
import org.springframework.web.testfixture.method.ResolvableMethod;
31+
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.mockito.Mockito.mock;
35+
import static org.mockito.Mockito.when;
36+
37+
/**
38+
* Unit tests for {@link DefaultDataBinderFactory}.
39+
*/
40+
public class DefaultDataBinderFactoryTests {
41+
42+
@Test
43+
void jakartaValidatorExcludedWhenMethodValidationApplicable() throws Exception {
44+
DefaultDataBinderFactory binderFactory = new DefaultDataBinderFactory(null);
45+
binderFactory.setMethodValidationApplicable(true);
46+
47+
MethodParameter parameter = ResolvableMethod.on(DefaultDataBinderFactoryTests.class)
48+
.named("handle").build().annotPresent(Valid.class).arg();
49+
50+
WebDataBinder dataBinder = binderFactory.createBinder(
51+
new ServletWebRequest(new MockHttpServletRequest()), new Foo(), "foo",
52+
ResolvableType.forMethodParameter(parameter));
53+
54+
Validator springValidator = mock(Validator.class);
55+
when(springValidator.supports(Foo.class)).thenReturn(true);
56+
dataBinder.addValidators(springValidator);
57+
58+
LocalValidatorFactoryBean beanValidator = new LocalValidatorFactoryBean();
59+
beanValidator.afterPropertiesSet();
60+
dataBinder.addValidators(beanValidator);
61+
62+
WrappedBeanValidator wrappedBeanValidator = new WrappedBeanValidator(beanValidator);
63+
dataBinder.addValidators(wrappedBeanValidator);
64+
65+
assertThat(dataBinder.getValidatorsToApply()).containsExactly(springValidator);
66+
}
67+
68+
69+
@SuppressWarnings("unused")
70+
private void handle(@Valid Foo foo) {
71+
}
72+
73+
74+
private static class WrappedBeanValidator implements SmartValidator {
75+
76+
private final jakarta.validation.Validator validator;
77+
78+
private WrappedBeanValidator(jakarta.validation.Validator validator) {
79+
this.validator = validator;
80+
}
81+
82+
@Override
83+
public boolean supports(Class<?> clazz) {
84+
return true;
85+
}
86+
87+
@Override
88+
public void validate(Object target, Errors errors, Object... validationHints) {
89+
}
90+
91+
@Override
92+
public void validate(Object target, Errors errors) {
93+
}
94+
95+
@Override
96+
public <T> T unwrap(Class<T> type) {
97+
return (T) this.validator;
98+
}
99+
}
100+
101+
102+
private static class Foo {}
103+
104+
}

spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.ui.Model;
3232
import org.springframework.validation.BindingResult;
3333
import org.springframework.validation.DataBinder;
34+
import org.springframework.validation.SmartValidator;
3435
import org.springframework.validation.support.BindingAwareConcurrentModel;
3536
import org.springframework.web.bind.support.BindParamNameResolver;
3637
import org.springframework.web.bind.support.WebBindingInitializer;
@@ -224,7 +225,8 @@ public static void initBinder(DataBinder binder, MethodParameter parameter) {
224225
if (ReactiveAdapterRegistry.getSharedInstance().getAdapter(parameter.getParameterType()) == null) {
225226
for (Annotation annotation : parameter.getParameterAnnotations()) {
226227
if (annotation.annotationType().getName().equals("jakarta.validation.Valid")) {
227-
binder.setExcludedValidators(validator -> validator instanceof jakarta.validation.Validator);
228+
binder.setExcludedValidators(v -> v instanceof jakarta.validation.Validator ||
229+
v instanceof SmartValidator sv && sv.unwrap(jakarta.validation.Validator.class) != null);
228230
}
229231
}
230232
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.reactive;
18+
19+
import jakarta.validation.Valid;
20+
import org.junit.jupiter.api.Test;
21+
22+
import org.springframework.core.MethodParameter;
23+
import org.springframework.core.ResolvableType;
24+
import org.springframework.validation.Errors;
25+
import org.springframework.validation.SmartValidator;
26+
import org.springframework.validation.Validator;
27+
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
28+
import org.springframework.web.bind.WebDataBinder;
29+
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
30+
import org.springframework.web.testfixture.method.ResolvableMethod;
31+
import org.springframework.web.testfixture.server.MockServerWebExchange;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.mockito.BDDMockito.mock;
35+
import static org.mockito.BDDMockito.when;
36+
37+
/**
38+
* Unit tests for {@link BindingContext}.
39+
*/
40+
public class BindingContextTests {
41+
42+
@Test
43+
void jakartaValidatorExcludedWhenMethodValidationApplicable() {
44+
BindingContext bindingContext = new BindingContext(null);
45+
bindingContext.setMethodValidationApplicable(true);
46+
47+
MethodParameter parameter = ResolvableMethod.on(BindingContextTests.class)
48+
.named("handle").build().annotPresent(Valid.class).arg();
49+
50+
WebDataBinder dataBinder = bindingContext.createDataBinder(
51+
MockServerWebExchange.from(MockServerHttpRequest.get("")), new Foo(), "foo",
52+
ResolvableType.forMethodParameter(parameter));
53+
54+
Validator springValidator = mock(Validator.class);
55+
when(springValidator.supports(Foo.class)).thenReturn(true);
56+
dataBinder.addValidators(springValidator);
57+
58+
LocalValidatorFactoryBean beanValidator = new LocalValidatorFactoryBean();
59+
beanValidator.afterPropertiesSet();
60+
dataBinder.addValidators(beanValidator);
61+
62+
WrappedBeanValidator wrappedBeanValidator = new WrappedBeanValidator(beanValidator);
63+
dataBinder.addValidators(wrappedBeanValidator);
64+
65+
assertThat(dataBinder.getValidatorsToApply()).containsExactly(springValidator);
66+
}
67+
68+
69+
@SuppressWarnings("unused")
70+
private void handle(@Valid Foo foo) {
71+
}
72+
73+
74+
private static class WrappedBeanValidator implements SmartValidator {
75+
76+
private final jakarta.validation.Validator validator;
77+
78+
private WrappedBeanValidator(jakarta.validation.Validator validator) {
79+
this.validator = validator;
80+
}
81+
82+
@Override
83+
public boolean supports(Class<?> clazz) {
84+
return true;
85+
}
86+
87+
@Override
88+
public void validate(Object target, Errors errors, Object... validationHints) {
89+
}
90+
91+
@Override
92+
public void validate(Object target, Errors errors) {
93+
}
94+
95+
@Override
96+
public <T> T unwrap(Class<T> type) {
97+
return (T) this.validator;
98+
}
99+
}
100+
101+
102+
private static class Foo {}
103+
104+
}

0 commit comments

Comments
 (0)