Create spring-boot-validation module

This commit is contained in:
Stéphane Nicoll
2025-03-19 14:43:09 +01:00
committed by Phillip Webb
parent aa11d6d0fb
commit fc3ae4f975
39 changed files with 100 additions and 62 deletions

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2025 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.boot.validation.autoconfigure;
import jakarta.validation.Configuration;
import jakarta.validation.Validation;
import org.springframework.boot.autoconfigure.preinitialize.BackgroundPreinitializer;
/**
* {@link BackgroundPreinitializer} for jakarta.validation.
*
* @author Phillip Webb
*/
final class JakartaValidationBackgroundPreinitializer implements BackgroundPreinitializer {
@Override
public void preinitialize() throws Exception {
Configuration<?> configuration = Validation.byDefaultProvider().configure();
configuration.buildValidatorFactory().getValidator();
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2012-2025 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.boot.validation.autoconfigure;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
/**
* Enable the {@code Primary} flag on the auto-configured validator if necessary.
* <p>
* As {@link LocalValidatorFactoryBean} exposes 3 validator related contracts and we're
* only checking for the absence {@link jakarta.validation.Validator}, we should flag the
* auto-configured validator as primary only if no Spring's {@link Validator} is flagged
* as primary.
*
* @author Stephane Nicoll
* @author Matej Nedic
* @author Andy Wilkinson
*/
class PrimaryDefaultValidatorPostProcessor implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
/**
* The bean name of the auto-configured Validator.
*/
private static final String VALIDATOR_BEAN_NAME = "defaultValidator";
private ConfigurableListableBeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof ConfigurableListableBeanFactory listableBeanFactory) {
this.beanFactory = listableBeanFactory;
}
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
BeanDefinition definition = getAutoConfiguredValidator(registry);
if (definition != null) {
definition.setPrimary(!hasPrimarySpringValidator());
}
}
private BeanDefinition getAutoConfiguredValidator(BeanDefinitionRegistry registry) {
if (registry.containsBeanDefinition(VALIDATOR_BEAN_NAME)) {
BeanDefinition definition = registry.getBeanDefinition(VALIDATOR_BEAN_NAME);
if (definition.getRole() == BeanDefinition.ROLE_INFRASTRUCTURE
&& isTypeMatch(VALIDATOR_BEAN_NAME, LocalValidatorFactoryBean.class)) {
return definition;
}
}
return null;
}
private boolean isTypeMatch(String name, Class<?> type) {
return this.beanFactory != null && this.beanFactory.isTypeMatch(name, type);
}
private boolean hasPrimarySpringValidator() {
String[] validatorBeans = this.beanFactory.getBeanNamesForType(Validator.class, false, false);
for (String validatorBean : validatorBeans) {
BeanDefinition definition = this.beanFactory.getBeanDefinition(validatorBean);
if (definition.isPrimary()) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2012-2025 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.boot.validation.autoconfigure;
import jakarta.validation.Validator;
import jakarta.validation.executable.ExecutableValidator;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnResource;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.validation.MessageInterpolatorFactory;
import org.springframework.boot.validation.beanvalidation.FilteredMethodValidationPostProcessor;
import org.springframework.boot.validation.beanvalidation.MethodValidationExcludeFilter;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Role;
import org.springframework.core.env.Environment;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
/**
* {@link EnableAutoConfiguration Auto-configuration} to configure the validation
* infrastructure.
*
* @author Stephane Nicoll
* @author Madhura Bhave
* @author Yanming Zhou
* @since 4.0.0
*/
@AutoConfiguration
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/jakarta.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean(Validator.class)
public static LocalValidatorFactoryBean defaultValidator(ApplicationContext applicationContext,
ObjectProvider<ValidationConfigurationCustomizer> customizers) {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
factoryBean.setConfigurationInitializer((configuration) -> customizers.orderedStream()
.forEach((customizer) -> customizer.customize(configuration)));
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(applicationContext);
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
return factoryBean;
}
@Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
ObjectProvider<Validator> validator, ObjectProvider<MethodValidationExcludeFilter> excludeFilters) {
FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor(
excludeFilters.orderedStream());
boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
processor.setProxyTargetClass(proxyTargetClass);
boolean adaptConstraintViolations = environment
.getProperty("spring.validation.method.adapt-constraint-violations", Boolean.class, false);
processor.setAdaptConstraintViolations(adaptConstraintViolations);
processor.setValidatorProvider(validator);
return processor;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2012-2025 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.boot.validation.autoconfigure;
import jakarta.validation.Configuration;
/**
* Callback interface that can be used to customize {@link Configuration}.
*
* @author Dang Zhicairang
* @since 4.0.0
*/
@FunctionalInterface
public interface ValidationConfigurationCustomizer {
/**
* Customize the given {@code configuration}.
* @param configuration the configuration to customize
*/
void customize(Configuration<?> configuration);
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2012-2025 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.boot.validation.autoconfigure;
import jakarta.validation.ValidationException;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.validation.MessageInterpolatorFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.MessageSource;
import org.springframework.validation.Errors;
import org.springframework.validation.SmartValidator;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
/**
* {@link Validator} implementation that delegates calls to another {@link Validator}.
* This {@link Validator} implements Spring's {@link SmartValidator} interface but does
* not implement the JSR-303 {@code jakarta.validator.Validator} interface.
*
* @author Stephane Nicoll
* @author Phillip Webb
* @author Zisis Pavloudis
* @since 4.0.0
*/
public class ValidatorAdapter implements SmartValidator, ApplicationContextAware, InitializingBean, DisposableBean {
private final SmartValidator target;
private final boolean existingBean;
ValidatorAdapter(SmartValidator target, boolean existingBean) {
this.target = target;
this.existingBean = existingBean;
}
public final Validator getTarget() {
return this.target;
}
@Override
public boolean supports(Class<?> type) {
return this.target.supports(type);
}
@Override
public void validate(Object target, Errors errors) {
this.target.validate(target, errors);
}
@Override
public void validate(Object target, Errors errors, Object... validationHints) {
this.target.validate(target, errors, validationHints);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (!this.existingBean && this.target instanceof ApplicationContextAware contextAwareTarget) {
contextAwareTarget.setApplicationContext(applicationContext);
}
}
@Override
public void afterPropertiesSet() throws Exception {
if (!this.existingBean && this.target instanceof InitializingBean initializingBean) {
initializingBean.afterPropertiesSet();
}
}
@Override
public void destroy() throws Exception {
if (!this.existingBean && this.target instanceof DisposableBean disposableBean) {
disposableBean.destroy();
}
}
/**
* Return a {@link Validator} that only implements the {@link Validator} interface,
* wrapping it if necessary.
* <p>
* If the specified {@link Validator} is not {@code null}, it is wrapped. If not, a
* {@link jakarta.validation.Validator} is retrieved from the context and wrapped.
* Otherwise, a new default validator is created.
* @param applicationContext the application context
* @param validator an existing validator to use or {@code null}
* @return the validator to use
*/
public static Validator get(ApplicationContext applicationContext, Validator validator) {
if (validator != null) {
return wrap(validator, false);
}
return getExistingOrCreate(applicationContext);
}
private static Validator getExistingOrCreate(ApplicationContext applicationContext) {
Validator existing = getExisting(applicationContext);
if (existing != null) {
return wrap(existing, true);
}
return create(applicationContext);
}
private static Validator getExisting(ApplicationContext applicationContext) {
try {
jakarta.validation.Validator validatorBean = applicationContext.getBean(jakarta.validation.Validator.class);
if (validatorBean instanceof Validator validator) {
return validator;
}
return new SpringValidatorAdapter(validatorBean);
}
catch (NoSuchBeanDefinitionException ex) {
return null;
}
}
private static Validator create(MessageSource messageSource) {
OptionalValidatorFactoryBean validator = new OptionalValidatorFactoryBean();
try {
MessageInterpolatorFactory factory = new MessageInterpolatorFactory(messageSource);
validator.setMessageInterpolator(factory.getObject());
}
catch (ValidationException ex) {
// Ignore
}
return wrap(validator, false);
}
private static Validator wrap(Validator validator, boolean existingBean) {
if (validator instanceof jakarta.validation.Validator jakartaValidator) {
if (jakartaValidator instanceof SpringValidatorAdapter adapter) {
return new ValidatorAdapter(adapter, existingBean);
}
return new ValidatorAdapter(new SpringValidatorAdapter(jakartaValidator), existingBean);
}
return validator;
}
@Override
@SuppressWarnings("unchecked")
public <T> T unwrap(Class<T> type) {
if (type.isInstance(this.target)) {
return (T) this.target;
}
return this.target.unwrap(type);
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2019 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.
*/
/**
* Auto-configuration for (JSR-303) Validation.
*/
package org.springframework.boot.validation.autoconfigure;

View File

@@ -0,0 +1,12 @@
{
"groups": [],
"properties": [
{
"name": "spring.validation.method.adapt-constraint-violations",
"type": "java.lang.Boolean",
"description": "Whether to adapt ConstraintViolations to MethodValidationResult.",
"defaultValue": false
}
],
"hints": []
}

View File

@@ -0,0 +1,3 @@
# Background Preinitializers
org.springframework.boot.autoconfigure.preinitialize.BackgroundPreinitializer=\
org.springframework.boot.validation.autoconfigure.JakartaValidationBackgroundPreinitializer

View File

@@ -0,0 +1 @@
org.springframework.boot.validation.autoconfigure.ValidationAutoConfiguration

View File

@@ -0,0 +1,486 @@
/*
* Copyright 2012-2025 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.boot.validation.autoconfigure;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validator;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.validation.autoconfigure.ValidationAutoConfigurationTests.CustomValidatorConfiguration.TestBeanPostProcessor;
import org.springframework.boot.validation.beanvalidation.MethodValidationExcludeFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.CustomValidatorBean;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
import org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean;
import org.springframework.validation.method.MethodValidationException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ValidationAutoConfiguration}.
*
* @author Stephane Nicoll
* @author Phillip Webb
* @author Yanming Zhou
*/
class ValidationAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ValidationAutoConfiguration.class));
@Test
void validationAutoConfigurationShouldConfigureDefaultValidator() {
this.contextRunner.run((context) -> {
assertThat(context.getBeanNamesForType(Validator.class)).containsExactly("defaultValidator");
assertThat(context.getBeanNamesForType(org.springframework.validation.Validator.class))
.containsExactly("defaultValidator");
assertThat(context.getBean(Validator.class)).isInstanceOf(LocalValidatorFactoryBean.class)
.isEqualTo(context.getBean(org.springframework.validation.Validator.class));
assertThat(isPrimaryBean(context, "defaultValidator")).isTrue();
});
}
@Test
void validationAutoConfigurationWhenUserProvidesValidatorShouldBackOff() {
this.contextRunner.withUserConfiguration(UserDefinedValidatorConfig.class).run((context) -> {
assertThat(context.getBeanNamesForType(Validator.class)).containsExactly("customValidator");
assertThat(context.getBeanNamesForType(org.springframework.validation.Validator.class))
.containsExactly("customValidator");
assertThat(context.getBean(Validator.class)).isInstanceOf(OptionalValidatorFactoryBean.class)
.isEqualTo(context.getBean(org.springframework.validation.Validator.class));
assertThat(isPrimaryBean(context, "customValidator")).isFalse();
});
}
@Test
void validationAutoConfigurationWhenUserProvidesDefaultValidatorShouldNotEnablePrimary() {
this.contextRunner.withUserConfiguration(UserDefinedDefaultValidatorConfig.class).run((context) -> {
assertThat(context.getBeanNamesForType(Validator.class)).containsExactly("defaultValidator");
assertThat(context.getBeanNamesForType(org.springframework.validation.Validator.class))
.containsExactly("defaultValidator");
assertThat(isPrimaryBean(context, "defaultValidator")).isFalse();
});
}
@Test
void validationAutoConfigurationWhenUserProvidesJsrValidatorShouldBackOff() {
this.contextRunner.withUserConfiguration(UserDefinedJsrValidatorConfig.class).run((context) -> {
assertThat(context.getBeanNamesForType(Validator.class)).containsExactly("customValidator");
assertThat(context.getBeanNamesForType(org.springframework.validation.Validator.class)).isEmpty();
assertThat(isPrimaryBean(context, "customValidator")).isFalse();
});
}
@Test
void validationAutoConfigurationWhenUserProvidesSpringValidatorShouldCreateJsrValidator() {
this.contextRunner.withUserConfiguration(UserDefinedSpringValidatorConfig.class).run((context) -> {
assertThat(context.getBeanNamesForType(Validator.class)).containsExactly("defaultValidator");
assertThat(context.getBeanNamesForType(org.springframework.validation.Validator.class))
.containsExactly("customValidator", "anotherCustomValidator", "defaultValidator");
assertThat(context.getBean(Validator.class)).isInstanceOf(LocalValidatorFactoryBean.class)
.isEqualTo(context.getBean(org.springframework.validation.Validator.class));
assertThat(isPrimaryBean(context, "defaultValidator")).isTrue();
});
}
@Test
void validationAutoConfigurationWhenUserProvidesPrimarySpringValidatorShouldRemovePrimaryFlag() {
this.contextRunner.withUserConfiguration(UserDefinedPrimarySpringValidatorConfig.class).run((context) -> {
assertThat(context.getBeanNamesForType(Validator.class)).containsExactly("defaultValidator");
assertThat(context.getBeanNamesForType(org.springframework.validation.Validator.class))
.containsExactly("customValidator", "anotherCustomValidator", "defaultValidator");
assertThat(context.getBean(Validator.class)).isInstanceOf(LocalValidatorFactoryBean.class);
assertThat(context.getBean(org.springframework.validation.Validator.class))
.isEqualTo(context.getBean("anotherCustomValidator"));
assertThat(isPrimaryBean(context, "defaultValidator")).isFalse();
});
}
@Test
void whenUserProvidesSpringValidatorInParentContextThenAutoConfiguredValidatorIsPrimary() {
new ApplicationContextRunner().withUserConfiguration(UserDefinedSpringValidatorConfig.class).run((parent) -> {
this.contextRunner.withParent(parent).run((context) -> {
assertThat(context.getBeanNamesForType(Validator.class)).containsExactly("defaultValidator");
assertThat(context.getBeanNamesForType(org.springframework.validation.Validator.class))
.containsExactly("defaultValidator");
assertThat(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context.getBeanFactory(),
org.springframework.validation.Validator.class))
.containsExactly("defaultValidator", "customValidator", "anotherCustomValidator");
assertThat(isPrimaryBean(context, "defaultValidator")).isTrue();
});
});
}
@Test
void whenUserProvidesPrimarySpringValidatorInParentContextThenAutoConfiguredValidatorIsPrimary() {
new ApplicationContextRunner().withUserConfiguration(UserDefinedPrimarySpringValidatorConfig.class)
.run((parent) -> {
this.contextRunner.withParent(parent).run((context) -> {
assertThat(context.getBeanNamesForType(Validator.class)).containsExactly("defaultValidator");
assertThat(context.getBeanNamesForType(org.springframework.validation.Validator.class))
.containsExactly("defaultValidator");
assertThat(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context.getBeanFactory(),
org.springframework.validation.Validator.class))
.containsExactly("defaultValidator", "customValidator", "anotherCustomValidator");
assertThat(isPrimaryBean(context, "defaultValidator")).isTrue();
});
});
}
@Test
void validationIsEnabled() {
this.contextRunner.withUserConfiguration(SampleService.class).run((context) -> {
assertThat(context.getBeansOfType(Validator.class)).hasSize(1);
SampleService service = context.getBean(SampleService.class);
service.doSomething("Valid");
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> service.doSomething("KO"));
});
}
@Test
void classCanBeExcludedFromValidation() {
this.contextRunner.withUserConfiguration(ExcludedServiceConfiguration.class).run((context) -> {
assertThat(context.getBeansOfType(Validator.class)).hasSize(1);
ExcludedService service = context.getBean(ExcludedService.class);
service.doSomething("Valid");
assertThatNoException().isThrownBy(() -> service.doSomething("KO"));
});
}
@Test
void validationUsesCglibProxy() {
this.contextRunner.withUserConfiguration(DefaultAnotherSampleService.class).run((context) -> {
assertThat(context.getBeansOfType(Validator.class)).hasSize(1);
DefaultAnotherSampleService service = context.getBean(DefaultAnotherSampleService.class);
service.doSomething(42);
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> service.doSomething(2));
});
}
@Test
void validationCanBeConfiguredToUseJdkProxy() {
this.contextRunner.withUserConfiguration(AnotherSampleServiceConfiguration.class)
.withPropertyValues("spring.aop.proxy-target-class=false")
.run((context) -> {
assertThat(context.getBeansOfType(Validator.class)).hasSize(1);
assertThat(context.getBeansOfType(DefaultAnotherSampleService.class)).isEmpty();
AnotherSampleService service = context.getBean(AnotherSampleService.class);
service.doSomething(42);
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(() -> service.doSomething(2));
});
}
@Test
void validationCanBeConfiguredToAdaptConstraintViolations() {
this.contextRunner.withUserConfiguration(AnotherSampleServiceConfiguration.class)
.withPropertyValues("spring.validation.method.adapt-constraint-violations=true")
.run((context) -> {
assertThat(context.getBeansOfType(Validator.class)).hasSize(1);
AnotherSampleService service = context.getBean(AnotherSampleService.class);
service.doSomething(42);
assertThatExceptionOfType(MethodValidationException.class).isThrownBy(() -> service.doSomething(2));
});
}
@Test
void validationUseDefaultAdaptToConstraintViolationsValue() {
this.contextRunner.withUserConfiguration(AnotherSampleServiceConfiguration.class).run((context) -> {
MethodValidationPostProcessor postProcessor = context.getBean(MethodValidationPostProcessor.class);
assertThat(postProcessor).hasFieldOrPropertyWithValue("adaptConstraintViolations", false);
});
}
@Test
@SuppressWarnings("unchecked")
void userDefinedMethodValidationPostProcessorTakesPrecedence() {
this.contextRunner.withUserConfiguration(SampleConfiguration.class).run((context) -> {
assertThat(context.getBeansOfType(Validator.class)).hasSize(1);
Object userMethodValidationPostProcessor = context.getBean("testMethodValidationPostProcessor");
assertThat(context.getBean(MethodValidationPostProcessor.class))
.isSameAs(userMethodValidationPostProcessor);
assertThat(context.getBeansOfType(MethodValidationPostProcessor.class)).hasSize(1);
Object validator = ReflectionTestUtils.getField(userMethodValidationPostProcessor, "validator");
assertThat(validator).isInstanceOf(Supplier.class);
assertThat(context.getBean(Validator.class)).isNotSameAs(((Supplier<Validator>) validator).get());
});
}
@Test
void methodValidationPostProcessorValidatorDependencyDoesNotTriggerEarlyInitialization() {
this.contextRunner.withUserConfiguration(CustomValidatorConfiguration.class)
.run((context) -> assertThat(context.getBean(TestBeanPostProcessor.class).postProcessed)
.contains("someService"));
}
@Test
void validationIsEnabledInChildContext() {
this.contextRunner.run((parent) -> new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ValidationAutoConfiguration.class))
.withUserConfiguration(SampleService.class)
.withParent(parent)
.run((context) -> {
assertThat(context.getBeansOfType(Validator.class)).isEmpty();
assertThat(parent.getBeansOfType(Validator.class)).hasSize(1);
SampleService service = context.getBean(SampleService.class);
service.doSomething("Valid");
assertThatExceptionOfType(ConstraintViolationException.class)
.isThrownBy(() -> service.doSomething("KO"));
}));
}
@Test
void configurationCustomizerBeansAreCalledInOrder() {
this.contextRunner.withUserConfiguration(ConfigurationCustomizersConfiguration.class).run((context) -> {
ValidationConfigurationCustomizer customizerOne = context.getBean("customizerOne",
ValidationConfigurationCustomizer.class);
ValidationConfigurationCustomizer customizerTwo = context.getBean("customizerTwo",
ValidationConfigurationCustomizer.class);
InOrder inOrder = Mockito.inOrder(customizerOne, customizerTwo);
then(customizerTwo).should(inOrder).customize(any(jakarta.validation.Configuration.class));
then(customizerOne).should(inOrder).customize(any(jakarta.validation.Configuration.class));
});
}
private boolean isPrimaryBean(AssertableApplicationContext context, String beanName) {
return ((BeanDefinitionRegistry) context.getSourceApplicationContext()).getBeanDefinition(beanName).isPrimary();
}
@Configuration(proxyBeanMethods = false)
static class Config {
}
@Configuration(proxyBeanMethods = false)
static class UserDefinedValidatorConfig {
@Bean
OptionalValidatorFactoryBean customValidator() {
return new OptionalValidatorFactoryBean();
}
}
@Configuration(proxyBeanMethods = false)
static class UserDefinedDefaultValidatorConfig {
@Bean
OptionalValidatorFactoryBean defaultValidator() {
return new OptionalValidatorFactoryBean();
}
}
@Configuration(proxyBeanMethods = false)
static class UserDefinedJsrValidatorConfig {
@Bean
Validator customValidator() {
return mock(Validator.class);
}
}
@Configuration(proxyBeanMethods = false)
static class UserDefinedSpringValidatorConfig {
@Bean
org.springframework.validation.Validator customValidator() {
return mock(org.springframework.validation.Validator.class);
}
@Bean
org.springframework.validation.Validator anotherCustomValidator() {
return mock(org.springframework.validation.Validator.class);
}
}
@Configuration(proxyBeanMethods = false)
static class UserDefinedPrimarySpringValidatorConfig {
@Bean
org.springframework.validation.Validator customValidator() {
return mock(org.springframework.validation.Validator.class);
}
@Bean
@Primary
org.springframework.validation.Validator anotherCustomValidator() {
return mock(org.springframework.validation.Validator.class);
}
}
@Validated
static class SampleService {
void doSomething(@Size(min = 3, max = 10) String name) {
}
}
@Configuration(proxyBeanMethods = false)
static final class ExcludedServiceConfiguration {
@Bean
ExcludedService excludedService() {
return new ExcludedService();
}
@Bean
MethodValidationExcludeFilter exclusionFilter() {
return (type) -> type.equals(ExcludedService.class);
}
}
@Validated
static final class ExcludedService {
void doSomething(@Size(min = 3, max = 10) String name) {
}
}
interface AnotherSampleService {
void doSomething(@Min(42) Integer counter);
}
@Validated
static class DefaultAnotherSampleService implements AnotherSampleService {
@Override
public void doSomething(Integer counter) {
}
}
@Configuration(proxyBeanMethods = false)
static class AnotherSampleServiceConfiguration {
@Bean
AnotherSampleService anotherSampleService() {
return new DefaultAnotherSampleService();
}
}
@Configuration(proxyBeanMethods = false)
static class SampleConfiguration {
@Bean
static MethodValidationPostProcessor testMethodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
@org.springframework.context.annotation.Configuration(proxyBeanMethods = false)
static class CustomValidatorConfiguration {
CustomValidatorConfiguration(SomeService someService) {
}
@Bean
Validator customValidator() {
return new CustomValidatorBean();
}
@Bean
static TestBeanPostProcessor testBeanPostProcessor() {
return new TestBeanPostProcessor();
}
@Configuration(proxyBeanMethods = false)
static class SomeServiceConfiguration {
@Bean
SomeService someService() {
return new SomeService();
}
}
static class SomeService {
}
static class TestBeanPostProcessor implements BeanPostProcessor {
private final Set<String> postProcessed = new HashSet<>();
@Override
public Object postProcessAfterInitialization(Object bean, String name) {
this.postProcessed.add(name);
return bean;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String name) {
return bean;
}
}
}
@Configuration(proxyBeanMethods = false)
static class ConfigurationCustomizersConfiguration {
@Bean
@Order(1)
ValidationConfigurationCustomizer customizerOne() {
return mock(ValidationConfigurationCustomizer.class);
}
@Bean
@Order(0)
ValidationConfigurationCustomizer customizerTwo() {
return mock(ValidationConfigurationCustomizer.class);
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2012-2025 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.boot.validation.autoconfigure;
import jakarta.validation.Validator;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test for {@link ValidationAutoConfiguration} when Hibernate validator is present but no
* EL implementation is available.
*
* @author Stephane Nicoll
*/
@ClassPathExclusions({ "tomcat-embed-el-*.jar", "el-api-*.jar" })
class ValidationAutoConfigurationWithHibernateValidatorMissingElImplTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ValidationAutoConfiguration.class));
@Test
void missingElDependencyIsTolerated() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(Validator.class);
assertThat(context).hasSingleBean(MethodValidationPostProcessor.class);
});
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2012-2025 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.boot.validation.autoconfigure;
import jakarta.validation.Validator;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test for {@link ValidationAutoConfiguration} when no JSR-303 provider is available.
*
* @author Stephane Nicoll
*/
@ClassPathExclusions("hibernate-validator-*.jar")
class ValidationAutoConfigurationWithoutValidatorTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ValidationAutoConfiguration.class));
@Test
void validationIsDisabled() {
this.contextRunner.run((context) -> {
assertThat(context).doesNotHaveBean(Validator.class);
assertThat(context).doesNotHaveBean(MethodValidationPostProcessor.class);
});
}
}

View File

@@ -0,0 +1,222 @@
/*
* Copyright 2012-2025 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.boot.validation.autoconfigure;
import java.util.HashMap;
import jakarta.validation.Validator;
import jakarta.validation.constraints.Min;
import org.hibernate.validator.HibernateValidator;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.validation.Errors;
import org.springframework.validation.MapBindingResult;
import org.springframework.validation.SmartValidator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatRuntimeException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
/**
* Tests for {@link ValidatorAdapter}.
*
* @author Stephane Nicoll
* @author Madhura Bhave
*/
class ValidatorAdapterTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();
@Test
void wrapLocalValidatorFactoryBean() {
this.contextRunner.withUserConfiguration(LocalValidatorFactoryBeanConfig.class).run((context) -> {
ValidatorAdapter wrapper = context.getBean(ValidatorAdapter.class);
assertThat(wrapper.supports(SampleData.class)).isTrue();
MapBindingResult errors = new MapBindingResult(new HashMap<>(), "test");
wrapper.validate(new SampleData(40), errors);
assertThat(errors.getErrorCount()).isOne();
});
}
@Test
void wrapperInvokesCallbackOnNonManagedBean() {
this.contextRunner.withUserConfiguration(NonManagedBeanConfig.class).run((context) -> {
LocalValidatorFactoryBean validator = context.getBean(NonManagedBeanConfig.class).validator;
then(validator).should().setApplicationContext(any(ApplicationContext.class));
then(validator).should().afterPropertiesSet();
then(validator).should(never()).destroy();
context.close();
then(validator).should().destroy();
});
}
@Test
void wrapperDoesNotInvokeCallbackOnManagedBean() {
this.contextRunner.withUserConfiguration(ManagedBeanConfig.class).run((context) -> {
LocalValidatorFactoryBean validator = context.getBean(ManagedBeanConfig.class).validator;
then(validator).should(never()).setApplicationContext(any(ApplicationContext.class));
then(validator).should(never()).afterPropertiesSet();
then(validator).should(never()).destroy();
context.close();
then(validator).should(never()).destroy();
});
}
@Test
void wrapperWhenValidationProviderNotPresentShouldNotThrowException() {
ClassPathResource hibernateValidator = new ClassPathResource(
"META-INF/services/jakarta.validation.spi.ValidationProvider");
this.contextRunner
.withClassLoader(new FilteredClassLoader(FilteredClassLoader.ClassPathResourceFilter.of(hibernateValidator),
FilteredClassLoader.PackageFilter.of("org.hibernate.validator")))
.run((context) -> ValidatorAdapter.get(context, null));
}
@Test
void unwrapToJakartaValidatorShouldReturnJakartaValidator() {
this.contextRunner.withUserConfiguration(LocalValidatorFactoryBeanConfig.class).run((context) -> {
ValidatorAdapter wrapper = context.getBean(ValidatorAdapter.class);
assertThat(wrapper.unwrap(Validator.class)).isInstanceOf(Validator.class);
});
}
@Test
void whenJakartaValidatorIsWrappedMultipleTimesUnwrapToJakartaValidatorShouldReturnJakartaValidator() {
this.contextRunner.withUserConfiguration(DoubleWrappedConfig.class).run((context) -> {
ValidatorAdapter wrapper = context.getBean(ValidatorAdapter.class);
assertThat(wrapper.unwrap(Validator.class)).isInstanceOf(Validator.class);
});
}
@Test
void unwrapToUnsupportedTypeShouldThrow() {
this.contextRunner.withUserConfiguration(LocalValidatorFactoryBeanConfig.class).run((context) -> {
ValidatorAdapter wrapper = context.getBean(ValidatorAdapter.class);
assertThatRuntimeException().isThrownBy(() -> wrapper.unwrap(HibernateValidator.class));
});
}
@Configuration(proxyBeanMethods = false)
static class LocalValidatorFactoryBeanConfig {
@Bean
LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
@Bean
ValidatorAdapter wrapper(LocalValidatorFactoryBean validator) {
return new ValidatorAdapter(validator, true);
}
}
@Configuration(proxyBeanMethods = false)
static class DoubleWrappedConfig {
@Bean
LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
@Bean
ValidatorAdapter wrapper(LocalValidatorFactoryBean validator) {
return new ValidatorAdapter(new Wrapper(validator), true);
}
static class Wrapper implements SmartValidator {
private final SmartValidator delegate;
Wrapper(SmartValidator delegate) {
this.delegate = delegate;
}
@Override
public boolean supports(Class<?> type) {
return this.delegate.supports(type);
}
@Override
public void validate(Object target, Errors errors) {
this.delegate.validate(target, errors);
}
@Override
public void validate(Object target, Errors errors, Object... validationHints) {
this.delegate.validate(target, errors, validationHints);
}
@Override
@SuppressWarnings("unchecked")
public <T> T unwrap(Class<T> type) {
if (type.isInstance(this.delegate)) {
return (T) this.delegate;
}
return this.delegate.unwrap(type);
}
}
}
@Configuration(proxyBeanMethods = false)
static class NonManagedBeanConfig {
private final LocalValidatorFactoryBean validator = mock(LocalValidatorFactoryBean.class);
@Bean
ValidatorAdapter wrapper() {
return new ValidatorAdapter(this.validator, false);
}
}
@Configuration(proxyBeanMethods = false)
static class ManagedBeanConfig {
private final LocalValidatorFactoryBean validator = mock(LocalValidatorFactoryBean.class);
@Bean
ValidatorAdapter wrapper() {
return new ValidatorAdapter(this.validator, true);
}
}
static class SampleData {
@Min(42)
private final int counter;
SampleData(int counter) {
this.counter = counter;
}
}
}