Java Bean Validation
Spring 框架提供了对 Java Bean Validation API 的全面支持。
Bean Validation 概览
Bean Validation 为 Java 应用程序提供了一种通过约束声明和元数据进行校验的通用方法。要使用它,你可以使用声明式校验约束来注解领域模型的属性,然后由运行时强制执行这些约束。系统提供了一些内置约束,你也可以定义自己的自定义约束。
考虑以下示例,它显示了一个具有两个属性的简单 PersonForm 模型:
public class PersonForm {
private String name;
private int age;
}class PersonForm(
private val name: String,
private val age: Int
)Bean Validation 允许你声明约束,如下例所示:
public class PersonForm {
@NotNull
@Size(max=64)
private String name;
@Min(0)
private int age;
}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,如下所示:
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@Configuration
public class AppConfig {
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}<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,可以注入后者的引用来应用校验逻辑:
import jakarta.validation.Validator;
@Service
public class MyService {
@Autowired
private Validator validator;
}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,你可以注入后者的引用。
例如:
import org.springframework.validation.Validator;
@Service
public class MyService {
@Autowired
private Validator validator;
}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 约束由两部分组成:
- 一个
@Constraint注解,用于声明约束及其可配置属性。 jakarta.validation.ConstraintValidator接口的一个实现,用于实现约束的行为。
要将声明与实现关联,每个 @Constraint 注解都会引用一个相应的 ConstraintValidator 实现类。在运行时,当在你的领域模型中遇到约束注解时,ConstraintValidatorFactory 会实例化所引用的实现。
默认情况下,LocalValidatorFactoryBean 配置了一个 SpringConstraintValidatorFactory,它使用 Spring 来创建 ConstraintValidator 实例。这使得你的自定义 ConstraintValidator 可以像任何其他 Spring Bean 一样受益于依赖注入。
以下示例显示了一个自定义 @Constraint 声明,以及一个使用 Spring 进行依赖注入的相关 ConstraintValidator 实现:
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}@Target(AnnotationTarget.FUNCTION, AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = MyConstraintValidator::class)
annotation class MyConstraintimport jakarta.validation.ConstraintValidator;
public class MyConstraintValidator implements ConstraintValidator<MyConstraint, String> {
@Autowired
private Foo aDependency;
// ...
}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 上下文中:
@Configuration
public class ApplicationConfiguration {
@Bean
public static MethodValidationPostProcessor validationPostProcessor() {
return new MethodValidationPostProcessor();
}
}@Configuration
class ApplicationConfiguration {
companion object {
@Bean
@JvmStatic
fun validationPostProcessor() = MethodValidationPostProcessor()
}
}<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>要符合 Spring 驱动的方法校验资格,目标类需要标有 Spring 的 @Validated 注解,该注解也可以可选地声明要使用的验证组。
提示
方法校验依赖于目标类周围的 AOP 代理,可以是用于接口方法的 JDK 动态代理,也可以是 CGLIB 代理。使用代理存在某些局限性。此外请记住,始终在代理类上使用方法和访问器(accessor);直接访问字段将不起作用。
Spring MVC 和 WebFlux 对相同的方法校验有内置支持,但不需要 AOP。
方法校验异常
默认情况下,会抛出 jakarta.validation.ConstraintViolationException。作为另一种选择,你可以抛出 MethodValidationException。要启用此功能,请设置以下标志:
@Configuration
public class ApplicationConfiguration {
@Bean
public static MethodValidationPostProcessor validationPostProcessor() {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
processor.setAdaptConstraintViolations(true);
return processor;
}
}@Configuration
class ApplicationConfiguration {
companion object {
@Bean
@JvmStatic
fun validationPostProcessor() = MethodValidationPostProcessor().apply {
setAdaptConstraintViolations(true)
}
}
}MethodValidationException 包含一个 ParameterValidationResult 列表,该列表按方法参数过滤错误,每个结果都暴露了 MethodParameter、参数值以及从 ConstraintViolation 适配而来的 MessageSourceResolvable 错误列表。
自定义校验错误
适配后的 MessageSourceResolvable 错误可以通过配置的 MessageSource 转换为错误消息。
例如,给定以下类声明:
record Person(@Size(min = 1, max = 10) String name) {
}
@Validated
public class MyService {
void addStudent(@Valid Person person, @Max(2) int degrees) {
// ...
}
}@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"
你可以通过属性文件自定义消息:
Size.person.name=请提供一个长度在 {2} 到 {1} 之间的 {0}
person.name=用户名degrees 参数上的约束违反会适配为:
- 错误代码:
"Max.myService#addStudent.degrees","Max.degrees","Max.int","Max"
自定义消息:
Max.degrees=提供的 {0} 不能超过 {1}配置 DataBinder
你可以使用 Validator 配置 DataBinder 实例。配置完成后,你可以通过调用 binder.validate() 来触发校验。任何校验错误都会自动添加到 Binder 的 BindingResult 中。
Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());
// 绑定到目标对象
binder.bind(propertyValues);
// 校验目标对象
binder.validate();
// 获取包含任何校验错误的 BindingResult
BindingResult results = binder.getBindingResult();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 层校验)。
- 是 Spring 对
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 代码。