Skip to content

Java Bean Validation ​

Spring 框架提供了对 Java Bean Validation API 的全面支持。

Bean Validation 概览 ​

Bean Validation 为 Java 应用程序提供了一种通过约束声明和元数据进行校验的通用方法。要使用它,你可以使用声明式校验约束来注解领域模型的属性,然后由运行时强制执行这些约束。系统提供了一些内置约束,你也可以定义自己的自定义约束。

考虑以下示例,它显示了一个具有两个属性的简单 PersonForm 模型:

java
public class PersonForm {
	private String name;
	private int age;
}
kotlin
class PersonForm(
		private val name: String,
		private val age: Int
)

Bean Validation 允许你声明约束,如下例所示:

java
public class PersonForm {

	@NotNull
	@Size(max=64)
	private String name;

	@Min(0)
	private int age;
}
kotlin
class PersonForm(
	@get:NotNull @get:Size(max=64)
	private val name: String,
	@get:Min(0)
	private val age: Int
)

然后,Bean Validation 校验器会根据声明的约束校验该类的实例。有关该 API 的通用信息,请参阅 Bean Validation。有关特定约束,请参阅 Hibernate Validator 文档。要了解如何将 Bean Validation 提供者设置为 Spring Bean,请继续阅读。

配置 Bean Validation 提供者 ​

Spring 提供了对 Bean Validation API 的完整支持,包括将 Bean Validation 提供者引导(bootstrapping)为 Spring Bean。这使你可以在应用程序中任何需要校验的地方注入 jakarta.validation.ValidatorFactory 或 jakarta.validation.Validator。

你可以使用 LocalValidatorFactoryBean 将默认的 Validator 配置为 Spring Bean,如下所示:

java
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class AppConfig {

	@Bean
	public LocalValidatorFactoryBean validator() {
		return new LocalValidatorFactoryBean();
	}
}
xml
<bean id="validator"
	class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

上述示例中的基本配置将使用其默认引导机制触发 Bean Validation 初始化。Bean Validation 提供者(如 Hibernate Validator)预计存在于类路径中并会被自动检测。

注入 Jakarta Validator ​

LocalValidatorFactoryBean 既实现了 jakarta.validation.ValidatorFactory 也实现了 jakarta.validation.Validator。如果你更喜欢直接使用 Bean Validation API,可以注入后者的引用来应用校验逻辑:

java
import jakarta.validation.Validator;

@Service
public class MyService {

	@Autowired
	private Validator validator;
}
kotlin
import jakarta.validation.Validator;

@Service
class MyService(@Autowired private val validator: Validator)

注入 Spring Validator ​

除了实现 jakarta.validation.Validator 之外,LocalValidatorFactoryBean 还适配了 org.springframework.validation.Validator。如果你的 Bean 需要 Spring Validation API,你可以注入后者的引用。

例如:

java
import org.springframework.validation.Validator;

@Service
public class MyService {

	@Autowired
	private Validator validator;
}
kotlin
import org.springframework.validation.Validator

@Service
class MyService(@Autowired private val validator: Validator)

当作为 org.springframework.validation.Validator 使用时,LocalValidatorFactoryBean 会调用底层的 jakarta.validation.Validator,然后将 ConstraintViolation 转换为 FieldError,并将它们注册到传递给 validate 方法的 Errors 对象中。

配置自定义约束 ​

每个 Bean Validation 约束由两部分组成:

  1. 一个 @Constraint 注解,用于声明约束及其可配置属性。
  2. jakarta.validation.ConstraintValidator 接口的一个实现,用于实现约束的行为。

要将声明与实现关联,每个 @Constraint 注解都会引用一个相应的 ConstraintValidator 实现类。在运行时,当在你的领域模型中遇到约束注解时,ConstraintValidatorFactory 会实例化所引用的实现。

默认情况下,LocalValidatorFactoryBean 配置了一个 SpringConstraintValidatorFactory,它使用 Spring 来创建 ConstraintValidator 实例。这使得你的自定义 ConstraintValidator 可以像任何其他 Spring Bean 一样受益于依赖注入。

以下示例显示了一个自定义 @Constraint 声明,以及一个使用 Spring 进行依赖注入的相关 ConstraintValidator 实现:

java
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}
kotlin
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = MyConstraintValidator::class)
annotation class MyConstraint
java
import jakarta.validation.ConstraintValidator;

public class MyConstraintValidator implements ConstraintValidator<MyConstraint, String> {

	@Autowired
	private Foo aDependency;

	// ...
}
kotlin
import jakarta.validation.ConstraintValidator

class MyConstraintValidator(private val aDependency: Foo) : ConstraintValidator<MyConstraint, String> {

	// ...
}

如上例所示,ConstraintValidator 实现可以像任何其他 Spring Bean 一样获取其 @Autowired 依赖项。

Spring 驱动的方法校验 (Method Validation) ​

你可以通过 MethodValidationPostProcessor Bean 定义将 Bean Validation 的方法校验功能集成到 Spring 上下文中:

java
@Configuration
public class ApplicationConfiguration {

	@Bean
	public static MethodValidationPostProcessor validationPostProcessor() {
		return new MethodValidationPostProcessor();
	}
}
kotlin
@Configuration
class ApplicationConfiguration {

	companion object {

		@Bean
		@JvmStatic
		fun validationPostProcessor() = MethodValidationPostProcessor()
	}
}
xml
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>

要符合 Spring 驱动的方法校验资格,目标类需要标有 Spring 的 @Validated 注解,该注解也可以可选地声明要使用的验证组。

提示

方法校验依赖于目标类周围的 AOP 代理,可以是用于接口方法的 JDK 动态代理,也可以是 CGLIB 代理。使用代理存在某些局限性。此外请记住,始终在代理类上使用方法和访问器(accessor);直接访问字段将不起作用。

Spring MVC 和 WebFlux 对相同的方法校验有内置支持,但不需要 AOP。

方法校验异常 ​

默认情况下,会抛出 jakarta.validation.ConstraintViolationException。作为另一种选择,你可以抛出 MethodValidationException。要启用此功能,请设置以下标志:

java
@Configuration
public class ApplicationConfiguration {

	@Bean
	public static MethodValidationPostProcessor validationPostProcessor() {
		MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
		processor.setAdaptConstraintViolations(true);
		return processor;
	}
}
kotlin
@Configuration
class ApplicationConfiguration {

	companion object {

		@Bean
		@JvmStatic
		fun validationPostProcessor() = MethodValidationPostProcessor().apply {
			setAdaptConstraintViolations(true)
		}
	}
}

MethodValidationException 包含一个 ParameterValidationResult 列表,该列表按方法参数过滤错误,每个结果都暴露了 MethodParameter、参数值以及从 ConstraintViolation 适配而来的 MessageSourceResolvable 错误列表。

自定义校验错误 ​

适配后的 MessageSourceResolvable 错误可以通过配置的 MessageSource 转换为错误消息。

例如,给定以下类声明:

java
record Person(@Size(min = 1, max = 10) String name) {
}

@Validated
public class MyService {

	void addStudent(@Valid Person person, @Max(2) int degrees) {
		// ...
	}
}
kotlin
@JvmRecord
internal data class Person(@Size(min = 1, max = 10) val name: String)

@Validated
class MyService {

	fun addStudent(person: @Valid Person?, degrees: @Max(2) Int) {
		// ...
	}
}

Person.name() 上的 ConstraintViolation 会被适配为具有以下属性的 FieldError:

  • 错误代码:"Size.person.name", "Size.name", "Size.java.lang.String", "Size"
  • 消息参数:"name", 10, 1
  • 默认消息:"size must be between 1 and 10"

你可以通过属性文件自定义消息:

properties
Size.person.name=请提供一个长度在 {2} 到 {1} 之间的 {0}
person.name=用户名

degrees 参数上的约束违反会适配为:

  • 错误代码:"Max.myService#addStudent.degrees", "Max.degrees", "Max.int", "Max"

自定义消息:

properties
Max.degrees=提供的 {0} 不能超过 {1}

配置 DataBinder ​

你可以使用 Validator 配置 DataBinder 实例。配置完成后,你可以通过调用 binder.validate() 来触发校验。任何校验错误都会自动添加到 Binder 的 BindingResult 中。

java
Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());

// 绑定到目标对象
binder.bind(propertyValues);

// 校验目标对象
binder.validate();

// 获取包含任何校验错误的 BindingResult
BindingResult results = binder.getBindingResult();
kotlin
val target = Foo()
val binder = DataBinder(target)
binder.validator = FooValidator()

// 绑定到目标对象
binder.bind(propertyValues)

// 校验目标对象
binder.validate()

// 获取包含任何校验错误的 BindingResult
val results = binder.bindingResult

补充教学 ​

1. Spring Validator vs Bean Validation:我该选哪个? ​

  • Bean Validation (JSR-380):现代开发的首选。它是标准的、声明式的(基于注解),易于阅读且与 Web 框架无缝集成。
  • Spring Validator 接口:当你需要执行复杂的、涉及多个对象或需要调用外部服务的逻辑校验时,由代码编写的 Spring Validator 可能更灵活。
  • 结论:大部分简单的格式和存在性校验使用注解;复杂的业务逻辑规则使用 Spring Validator 或自定义约束。

2. @Valid vs @Validated:傻傻分不清楚? ​

  • @Valid (Jakarta标准):
    • 源自 Bean Validation 标准。
    • 主要用于 级联校验(校验成员属性中的对象)。
    • 在方法参数上使用时,触发模型绑定后的校验(如 Controller 中)。
  • @Validated (Spring特有):
    • 是 Spring 对 @Valid 的加强。
    • 支持 校验组 (Validation Groups):允许你定义在不同场景下应用不同的校验规则(如“新增”时需要密码,“修改”时不需要)。
    • 用于 类级别 以开启方法参数校验(Service 层校验)。

3. 自定义校验器的 Spring 注入原理 ​

很多人好奇为什么 ConstraintValidator 里面可以直接 @Autowired。 这是因为 Spring 在初始化 LocalValidatorFactoryBean 时,会自动配置一个适配器。当校验引擎需要一个新的验证器实例时,它会向 Spring 的 ApplicationContext 申请,而不是简单的 new 出来。这使得校验逻辑可以轻松访问数据库、配置项或其他 Service。

4. 方法级别校验的深坑:AOP 代理 ​

在 Service 层使用 @Validated 校验方法参数时,请务必记住:

  • 自调用问题:如果类内部的方法 A 调用同类的方法 B,由于绕过了代理对象,方法 B 上的校验规则将 失效。
  • 异常处理:Service 层触发的校验失败通常抛出 ConstraintViolationException 或 MethodValidationException(取决于配置),如果你希望在 Controller 层给用户友好的返回,需要使用 @ControllerAdvice 全局捕获这些异常。

5. Spring Boot 下的快速起步 ​

在 Spring Boot 中,你只需要引入 spring-boot-starter-validation,它会自动为你配置好 LocalValidatorFactoryBean,你完全不需要手动编写 AppConfig 里面的那些 @Bean 代码。

Based on Spring Framework.