diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java b/spring-web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java index a96e2ff951..51fe506d13 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java @@ -22,6 +22,7 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; import org.springframework.validation.DataBinder; +import org.springframework.validation.SmartValidator; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.context.request.NativeWebRequest; @@ -148,7 +149,8 @@ public class DefaultDataBinderFactory implements WebDataBinderFactory { public static void initBinder(DataBinder binder, MethodParameter parameter) { for (Annotation annotation : parameter.getParameterAnnotations()) { if (annotation.annotationType().getName().equals("jakarta.validation.Valid")) { - binder.setExcludedValidators(validator -> validator instanceof jakarta.validation.Validator); + binder.setExcludedValidators(v -> v instanceof jakarta.validation.Validator || + v instanceof SmartValidator sv && sv.unwrap(jakarta.validation.Validator.class) != null); } } } diff --git a/spring-web/src/test/java/org/springframework/web/bind/support/DefaultDataBinderFactoryTests.java b/spring-web/src/test/java/org/springframework/web/bind/support/DefaultDataBinderFactoryTests.java new file mode 100644 index 0000000000..1572c81cd4 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/bind/support/DefaultDataBinderFactoryTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2023 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 + * + * https://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.web.bind.support; + +import jakarta.validation.Valid; +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.validation.Errors; +import org.springframework.validation.SmartValidator; +import org.springframework.validation.Validator; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.testfixture.method.ResolvableMethod; +import org.springframework.web.testfixture.servlet.MockHttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link DefaultDataBinderFactory}. + */ +public class DefaultDataBinderFactoryTests { + + @Test + void jakartaValidatorExcludedWhenMethodValidationApplicable() throws Exception { + DefaultDataBinderFactory binderFactory = new DefaultDataBinderFactory(null); + binderFactory.setMethodValidationApplicable(true); + + MethodParameter parameter = ResolvableMethod.on(DefaultDataBinderFactoryTests.class) + .named("handle").build().annotPresent(Valid.class).arg(); + + WebDataBinder dataBinder = binderFactory.createBinder( + new ServletWebRequest(new MockHttpServletRequest()), new Foo(), "foo", + ResolvableType.forMethodParameter(parameter)); + + Validator springValidator = mock(Validator.class); + when(springValidator.supports(Foo.class)).thenReturn(true); + dataBinder.addValidators(springValidator); + + LocalValidatorFactoryBean beanValidator = new LocalValidatorFactoryBean(); + beanValidator.afterPropertiesSet(); + dataBinder.addValidators(beanValidator); + + WrappedBeanValidator wrappedBeanValidator = new WrappedBeanValidator(beanValidator); + dataBinder.addValidators(wrappedBeanValidator); + + assertThat(dataBinder.getValidatorsToApply()).containsExactly(springValidator); + } + + + @SuppressWarnings("unused") + private void handle(@Valid Foo foo) { + } + + + private static class WrappedBeanValidator implements SmartValidator { + + private final jakarta.validation.Validator validator; + + private WrappedBeanValidator(jakarta.validation.Validator validator) { + this.validator = validator; + } + + @Override + public boolean supports(Class clazz) { + return true; + } + + @Override + public void validate(Object target, Errors errors, Object... validationHints) { + } + + @Override + public void validate(Object target, Errors errors) { + } + + @Override + public T unwrap(Class type) { + return (T) this.validator; + } + } + + + private static class Foo {} + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java b/spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java index 48c7b4845d..767ea0a7b2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java @@ -31,6 +31,7 @@ import org.springframework.lang.Nullable; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.DataBinder; +import org.springframework.validation.SmartValidator; import org.springframework.validation.support.BindingAwareConcurrentModel; import org.springframework.web.bind.support.BindParamNameResolver; import org.springframework.web.bind.support.WebBindingInitializer; @@ -224,7 +225,8 @@ public class BindingContext { if (ReactiveAdapterRegistry.getSharedInstance().getAdapter(parameter.getParameterType()) == null) { for (Annotation annotation : parameter.getParameterAnnotations()) { if (annotation.annotationType().getName().equals("jakarta.validation.Valid")) { - binder.setExcludedValidators(validator -> validator instanceof jakarta.validation.Validator); + binder.setExcludedValidators(v -> v instanceof jakarta.validation.Validator || + v instanceof SmartValidator sv && sv.unwrap(jakarta.validation.Validator.class) != null); } } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/BindingContextTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/BindingContextTests.java new file mode 100644 index 0000000000..078adc96a7 --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/BindingContextTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2023 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 + * + * https://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.web.reactive; + +import jakarta.validation.Valid; +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.validation.Errors; +import org.springframework.validation.SmartValidator; +import org.springframework.validation.Validator; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; +import org.springframework.web.testfixture.method.ResolvableMethod; +import org.springframework.web.testfixture.server.MockServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.when; + +/** + * Unit tests for {@link BindingContext}. + */ +public class BindingContextTests { + + @Test + void jakartaValidatorExcludedWhenMethodValidationApplicable() { + BindingContext bindingContext = new BindingContext(null); + bindingContext.setMethodValidationApplicable(true); + + MethodParameter parameter = ResolvableMethod.on(BindingContextTests.class) + .named("handle").build().annotPresent(Valid.class).arg(); + + WebDataBinder dataBinder = bindingContext.createDataBinder( + MockServerWebExchange.from(MockServerHttpRequest.get("")), new Foo(), "foo", + ResolvableType.forMethodParameter(parameter)); + + Validator springValidator = mock(Validator.class); + when(springValidator.supports(Foo.class)).thenReturn(true); + dataBinder.addValidators(springValidator); + + LocalValidatorFactoryBean beanValidator = new LocalValidatorFactoryBean(); + beanValidator.afterPropertiesSet(); + dataBinder.addValidators(beanValidator); + + WrappedBeanValidator wrappedBeanValidator = new WrappedBeanValidator(beanValidator); + dataBinder.addValidators(wrappedBeanValidator); + + assertThat(dataBinder.getValidatorsToApply()).containsExactly(springValidator); + } + + + @SuppressWarnings("unused") + private void handle(@Valid Foo foo) { + } + + + private static class WrappedBeanValidator implements SmartValidator { + + private final jakarta.validation.Validator validator; + + private WrappedBeanValidator(jakarta.validation.Validator validator) { + this.validator = validator; + } + + @Override + public boolean supports(Class clazz) { + return true; + } + + @Override + public void validate(Object target, Errors errors, Object... validationHints) { + } + + @Override + public void validate(Object target, Errors errors) { + } + + @Override + public T unwrap(Class type) { + return (T) this.validator; + } + } + + + private static class Foo {} + +}