Skip to content

Spring Bean Validation 完全指南 🚀

NOTE

Bean Validation 是 Java 企业级应用中数据验证的标准化解决方案,它通过声明式注解的方式让数据验证变得简单而优雅。

🎯 什么是 Bean Validation?

想象一下,你正在开发一个用户注册系统。传统的做法是什么?

kotlin
// 繁琐的手动验证 😰
fun registerUser(user: User): String {
    if (user.name.isNullOrBlank()) {
        return "用户名不能为空"
    }
    if (user.name.length > 64) {
        return "用户名长度不能超过64个字符"
    }
    if (user.age < 0) {
        return "年龄不能为负数"
    }
    if (user.email.isNullOrBlank() || !isValidEmail(user.email)) {
        return "请输入有效的邮箱地址"
    }
    // 更多验证逻辑...
    return "注册成功"
}
kotlin
// 优雅的声明式验证 ✨
data class User(
    @field:NotNull(message = "用户名不能为空")
    @field:Size(max = 64, message = "用户名长度不能超过64个字符")
    val name: String,
    
    @field:Min(value = 0, message = "年龄不能为负数")
    val age: Int,
    
    @field:NotNull(message = "邮箱不能为空")
    @field:Email(message = "请输入有效的邮箱地址")
    val email: String
)

@Service
class UserService(@Autowired private val validator: Validator) {
    
    fun registerUser(user: User): String {
        val violations = validator.validate(user) 
        if (violations.isNotEmpty()) {
            return violations.first().message
        }
        return "注册成功"
    }
}

TIP

看到区别了吗?Bean Validation 让我们把验证逻辑从业务代码中分离出来,通过注解直接声明在数据模型上,代码更清晰、更易维护!

🏗️ Bean Validation 的核心原理

Bean Validation 的设计哲学可以用一句话概括:"让数据自己描述自己的约束"

IMPORTANT

Bean Validation 的核心优势:

  • 声明式:通过注解声明验证规则
  • 标准化:基于 Jakarta Validation API
  • 可扩展:支持自定义验证器
  • 国际化:支持多语言错误消息

📝 常用验证注解详解

基础约束注解

kotlin
data class PersonForm(
    // 非空验证
    @field:NotNull(message = "姓名不能为空")
    @field:NotBlank(message = "姓名不能为空白字符") // [!code highlight]
    val name: String,
    
    // 长度验证
    @field:Size(min = 2, max = 50, message = "姓名长度必须在2-50个字符之间")
    val fullName: String,
    
    // 数值范围验证
    @field:Min(value = 0, message = "年龄不能为负数")
    @field:Max(value = 150, message = "年龄不能超过150岁") // [!code highlight]
    val age: Int,
    
    // 邮箱验证
    @field:Email(message = "请输入有效的邮箱地址")
    val email: String,
    
    // 正则表达式验证
    @field:Pattern(
        regexp = "^1[3-9]\\d{9}$", 
        message = "请输入有效的手机号码"
    ) 
    val phone: String,
    
    // 日期验证
    @field:Past(message = "出生日期必须是过去的时间")
    val birthDate: LocalDate,
    
    // 布尔值验证
    @field:AssertTrue(message = "必须同意用户协议")
    val agreeToTerms: Boolean
)

WARNING

在 Kotlin 中使用 Bean Validation 注解时,需要添加 @field: 前缀,因为 Kotlin 的属性会生成字段、getter 和 setter,我们需要明确指定注解应用到字段上。

嵌套对象验证

kotlin
data class Address(
    @field:NotBlank(message = "省份不能为空")
    val province: String,
    
    @field:NotBlank(message = "城市不能为空")
    val city: String,
    
    @field:NotBlank(message = "详细地址不能为空")
    val detail: String
)

data class User(
    @field:NotBlank(message = "用户名不能为空")
    val username: String,
    
    // 嵌套验证 - 重要!
    @field:Valid // [!code highlight]
    @field:NotNull(message = "地址信息不能为空")
    val address: Address,
    
    // 集合验证
    @field:Valid // [!code highlight]
    @field:Size(min = 1, message = "至少需要一个联系地址")
    val addresses: List<Address>
)

⚙️ Spring 中的 Bean Validation 配置

基础配置

kotlin
@Configuration
class ValidationConfig {
    
    // 配置验证器工厂
    @Bean
    fun validator(): LocalValidatorFactoryBean {
        return LocalValidatorFactoryBean()
    }
    
    // 启用方法级验证
    @Bean
    fun methodValidationPostProcessor(): MethodValidationPostProcessor {
        val processor = MethodValidationPostProcessor()
        processor.setAdaptConstraintViolations(true) 
        return processor
    }
}

在 Service 中使用验证器

kotlin
@Service
class UserService(
    @Autowired private val validator: Validator
) {
    
    fun createUser(userForm: UserForm): Result<User> {
        // 手动验证
        val violations = validator.validate(userForm)
        
        if (violations.isNotEmpty()) {
            val errorMessages = violations.map { it.message }
            return Result.failure(ValidationException(errorMessages))
        }
        
        // 业务逻辑处理
        val user = User(
            username = userForm.username,
            email = userForm.email,
            age = userForm.age
        )
        
        return Result.success(user)
    }
}

🎯 方法级验证

方法级验证让我们可以在方法参数和返回值上直接应用验证约束:

kotlin
@Service
@Validated
class StudentService {
    
    // 参数验证
    fun enrollStudent(
        @Valid student: Student, 
        @Max(value = 10, message = "课程数量不能超过10门") courseCount: Int
    ): String {
        // 业务逻辑
        return "学生 ${student.name} 成功注册了 $courseCount 门课程"
    }
    
    // 返回值验证
    @Valid
    fun getStudentById(
        @Min(value = 1, message = "学生ID必须大于0") id: Long
    ): Student {
        // 查询逻辑
        return Student(name = "张三", age = 20, email = "[email protected]")
    }
}

CAUTION

方法级验证依赖于 AOP 代理,因此:

  • 只对通过 Spring 容器调用的方法生效
  • 类内部方法调用不会触发验证
  • 需要注意代理的限制

验证异常处理

kotlin
@ControllerAdvice
class ValidationExceptionHandler {
    
    // 处理方法验证异常
    @ExceptionHandler(MethodValidationException::class)
    fun handleMethodValidation(ex: MethodValidationException): ResponseEntity<ErrorResponse> {
        val errors = ex.allValidationResults.flatMap { result ->
            result.resolvableErrors.map { error ->
                FieldError(
                    field = result.methodParameter.parameterName ?: "unknown",
                    message = error.defaultMessage ?: "验证失败"
                )
            }
        }
        
        return ResponseEntity.badRequest().body(
            ErrorResponse(
                code = "VALIDATION_ERROR",
                message = "参数验证失败",
                errors = errors
            )
        )
    }
    
    // 处理约束违反异常
    @ExceptionHandler(ConstraintViolationException::class)
    fun handleConstraintViolation(ex: ConstraintViolationException): ResponseEntity<ErrorResponse> {
        val errors = ex.constraintViolations.map { violation ->
            FieldError(
                field = violation.propertyPath.toString(),
                message = violation.message
            )
        }
        
        return ResponseEntity.badRequest().body(
            ErrorResponse(
                code = "CONSTRAINT_VIOLATION",
                message = "数据约束违反",
                errors = errors
            )
        )
    }
}

data class ErrorResponse(
    val code: String,
    val message: String,
    val errors: List<FieldError>
)

data class FieldError(
    val field: String,
    val message: String
)

🛠️ 自定义验证器

当内置的验证注解无法满足业务需求时,我们可以创建自定义验证器:

创建自定义注解

kotlin
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY_GETTER)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [PhoneNumberValidator::class]) 
@MustBeDocumented
annotation class PhoneNumber(
    val message: String = "无效的手机号码格式",
    val groups: Array<KClass<*>> = [],
    val payload: Array<KClass<out Payload>> = []
)

实现验证器

kotlin
@Component
class PhoneNumberValidator(
    @Autowired private val phoneService: PhoneService
) : ConstraintValidator<PhoneNumber, String> {
    
    private val phonePattern = "^1[3-9]\\d{9}$".toRegex()
    
    override fun isValid(value: String?, context: ConstraintValidatorContext): Boolean {
        if (value.isNullOrBlank()) {
            return true // 空值由 @NotNull 处理
        }
        
        // 基础格式验证
        if (!phonePattern.matches(value)) {
            return false
        }
        
        // 业务逻辑验证(可以注入其他服务)
        return phoneService.isValidPhoneNumber(value) 
    }
}

使用自定义验证器

kotlin
data class UserRegistrationForm(
    @field:NotBlank(message = "用户名不能为空")
    val username: String,
    
    @field:PhoneNumber(message = "请输入有效的手机号码") // [!code highlight]
    val phone: String,
    
    @field:Email(message = "请输入有效的邮箱地址")
    val email: String
)

🌍 国际化支持

Bean Validation 支持国际化错误消息:

配置消息源

kotlin
@Configuration
class MessageConfig {
    
    @Bean
    fun messageSource(): MessageSource {
        val messageSource = ResourceBundleMessageSource()
        messageSource.setBasenames("messages/validation") 
        messageSource.setDefaultEncoding("UTF-8")
        return messageSource
    }
}

创建消息文件

properties
# 通用验证消息
NotNull.user.username=用户名不能为空
Size.user.username=用户名长度必须在{2}到{1}个字符之间
Email.user.email=请输入有效的邮箱地址

# 自定义验证消息
PhoneNumber.user.phone=请输入有效的中国大陆手机号码

# 字段名称本地化
user.username=用户名
user.email=邮箱地址
user.phone=手机号码
properties
# General validation messages
NotNull.user.username=Username cannot be null
Size.user.username=Username must be between {2} and {1} characters
Email.user.email=Please enter a valid email address

# Custom validation messages
PhoneNumber.user.phone=Please enter a valid Chinese mainland phone number

# Field name localization
user.username=Username
user.email=Email
user.phone=Phone Number

🎨 在 Spring MVC 中的应用

Controller 中的验证

kotlin
@RestController
@RequestMapping("/api/users")
class UserController(
    private val userService: UserService
) {
    
    @PostMapping
    fun createUser(
        @Valid @RequestBody userForm: UserRegistrationForm, 
        bindingResult: BindingResult
    ): ResponseEntity<*> {
        
        // 检查验证结果
        if (bindingResult.hasErrors()) {
            val errors = bindingResult.fieldErrors.map { error ->
                FieldErrorDto(
                    field = error.field,
                    message = error.defaultMessage ?: "验证失败"
                )
            }
            
            return ResponseEntity.badRequest().body(
                ValidationErrorResponse(
                    message = "数据验证失败",
                    errors = errors
                )
            )
        }
        
        // 业务逻辑处理
        val result = userService.createUser(userForm)
        return ResponseEntity.ok(result)
    }
    
    // 路径参数验证
    @GetMapping("/{id}")
    fun getUserById(
        @PathVariable @Min(value = 1, message = "用户ID必须大于0") id: Long
    ): ResponseEntity<User> {
        val user = userService.findById(id)
        return ResponseEntity.ok(user)
    }
}

全局异常处理

kotlin
@ControllerAdvice
class GlobalExceptionHandler {
    
    // 处理请求体验证异常
    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidationException(
        ex: MethodArgumentNotValidException
    ): ResponseEntity<ValidationErrorResponse> {
        
        val errors = ex.bindingResult.fieldErrors.map { error ->
            FieldErrorDto(
                field = error.field,
                message = error.defaultMessage ?: "验证失败",
                rejectedValue = error.rejectedValue?.toString()
            )
        }
        
        return ResponseEntity.badRequest().body(
            ValidationErrorResponse(
                message = "请求数据验证失败",
                errors = errors
            )
        )
    }
    
    // 处理路径参数验证异常
    @ExceptionHandler(ConstraintViolationException::class)
    fun handleConstraintViolation(
        ex: ConstraintViolationException
    ): ResponseEntity<ValidationErrorResponse> {
        
        val errors = ex.constraintViolations.map { violation ->
            FieldErrorDto(
                field = violation.propertyPath.toString(),
                message = violation.message,
                rejectedValue = violation.invalidValue?.toString()
            )
        }
        
        return ResponseEntity.badRequest().body(
            ValidationErrorResponse(
                message = "参数约束违反",
                errors = errors
            )
        )
    }
}

data class ValidationErrorResponse(
    val message: String,
    val errors: List<FieldErrorDto>,
    val timestamp: LocalDateTime = LocalDateTime.now()
)

data class FieldErrorDto(
    val field: String,
    val message: String,
    val rejectedValue: String? = null
)

🚀 最佳实践与性能优化

验证分组

对于复杂的业务场景,我们可以使用验证分组来控制在不同情况下应用哪些验证规则:

kotlin
// 定义验证分组
interface CreateGroup
interface UpdateGroup

data class User(
    // 创建时不需要验证ID,更新时需要
    @field:NotNull(groups = [UpdateGroup::class], message = "更新时ID不能为空")
    @field:Min(value = 1, groups = [UpdateGroup::class], message = "ID必须大于0")
    val id: Long?,
    
    // 创建和更新时都需要验证
    @field:NotBlank(groups = [CreateGroup::class, UpdateGroup::class], message = "用户名不能为空")
    @field:Size(min = 3, max = 20, groups = [CreateGroup::class, UpdateGroup::class], message = "用户名长度必须在3-20个字符之间")
    val username: String,
    
    // 只在创建时验证密码
    @field:NotBlank(groups = [CreateGroup::class], message = "密码不能为空")
    @field:Size(min = 6, groups = [CreateGroup::class], message = "密码长度至少6个字符")
    val password: String?
)
kotlin
@RestController
class UserController(private val userService: UserService) {
    
    @PostMapping
    fun createUser(
        @Validated(CreateGroup::class) @RequestBody user: User
    ): ResponseEntity<User> {
        return ResponseEntity.ok(userService.create(user))
    }
    
    @PutMapping("/{id}")
    fun updateUser(
        @PathVariable id: Long,
        @Validated(UpdateGroup::class) @RequestBody user: User
    ): ResponseEntity<User> {
        return ResponseEntity.ok(userService.update(id, user))
    }
}

性能优化建议

性能优化要点

  1. 合理使用验证分组:避免不必要的验证
  2. 缓存验证器实例:Spring 会自动处理
  3. 避免过度复杂的正则表达式:影响性能
  4. 批量验证优化:对于大量数据的验证场景
kotlin
@Service
class BatchValidationService(
    private val validator: Validator
) {
    
    fun validateBatch(users: List<User>): BatchValidationResult {
        val validUsers = mutableListOf<User>()
        val invalidUsers = mutableListOf<Pair<User, Set<ConstraintViolation<User>>>>()
        
        users.forEach { user ->
            val violations = validator.validate(user)
            if (violations.isEmpty()) {
                validUsers.add(user)
            } else {
                invalidUsers.add(user to violations)
            }
        }
        
        return BatchValidationResult(validUsers, invalidUsers)
    }
}

data class BatchValidationResult(
    val validUsers: List<User>,
    val invalidUsers: List<Pair<User, Set<ConstraintViolation<User>>>>
)

🎯 总结

Bean Validation 是 Spring 生态系统中不可或缺的数据验证解决方案。它的核心价值在于:

核心优势

声明式验证:通过注解简化验证逻辑
标准化API:基于 Jakarta Validation 标准
高度可扩展:支持自定义验证器和约束
完美集成:与 Spring MVC/WebFlux 无缝集成
国际化支持:多语言错误消息
性能优秀:运行时验证,无额外开销

IMPORTANT

记住:好的验证不仅仅是技术实现,更是用户体验的重要组成部分。清晰、友好的错误提示能让用户更容易理解和修正输入错误。

通过合理使用 Bean Validation,我们可以构建出既健壮又用户友好的应用程序。验证逻辑的分离不仅让代码更清晰,也让维护变得更简单。在实际项目中,建议根据业务复杂度选择合适的验证策略,既要保证数据的完整性,也要考虑开发效率和用户体验的平衡。 🎉