Skip to content

Spring MVC 数据验证:让你的 API 更加健壮 🛡️

概述

在 Web 开发中,数据验证是确保应用程序安全性和数据完整性的关键环节。Spring MVC 提供了强大的验证机制,让我们能够轻松地对用户输入进行校验,避免脏数据进入业务逻辑层。

IMPORTANT

Spring MVC 的验证机制基于 JSR-303/JSR-380 Bean Validation 标准,提供了声明式的验证方式,让代码更加简洁和可维护。

为什么需要数据验证? 🤔

想象一下,如果没有数据验证会发生什么:

kotlin
@PostMapping("/users")
fun createUser(@RequestBody user: User): ResponseEntity<User> {
    // 😱 这些问题都可能发生:
    // 1. email 可能是空字符串或格式错误
    // 2. age 可能是负数或超过合理范围
    // 3. username 可能包含特殊字符
    // 4. 需要在每个方法中重复写验证逻辑
    
    if (user.email.isBlank()) { 
        throw IllegalArgumentException("邮箱不能为空") 
    } 
    
    if (!user.email.contains("@")) { 
        throw IllegalArgumentException("邮箱格式错误") 
    } 
    
    if (user.age < 0 || user.age > 150) { 
        throw IllegalArgumentException("年龄不合法") 
    } 
    
    // 更多验证逻辑... 代码变得冗长且重复
    return ResponseEntity.ok(userService.save(user))
}
kotlin
@PostMapping("/users")
fun createUser(@Valid @RequestBody user: User): ResponseEntity<User> { 
    // ✅ 验证自动完成,代码简洁清晰
    return ResponseEntity.ok(userService.save(user)) 
} 

Spring MVC 验证的核心原理 🔍

Spring MVC 的验证机制基于以下核心组件:

全局验证器配置 🌍

Spring MVC 默认会自动注册 LocalValidatorFactoryBean 作为全局验证器,但我们也可以自定义:

kotlin
@Configuration
class WebConfiguration : WebMvcConfigurer {
    
    override fun getValidator(): Validator {
        val validator = OptionalValidatorFactoryBean()
        
        // 可以自定义验证器的行为
        validator.setValidationMessageSource(messageSource()) 
        
        return validator
    }
    
    @Bean
    fun messageSource(): MessageSource {
        val messageSource = ReloadableResourceBundleMessageSource()
        messageSource.setBasename("classpath:validation-messages")
        messageSource.setDefaultEncoding("UTF-8")
        return messageSource
    }
}
kotlin
// 大多数情况下,使用默认配置即可
// Spring Boot 会自动配置 LocalValidatorFactoryBean
@SpringBootApplication
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

TIP

在大多数情况下,Spring Boot 的默认配置已经足够使用。只有在需要特殊定制时才需要自定义全局验证器。

实战示例:构建用户注册 API 👨‍💻

让我们通过一个完整的用户注册示例来看看验证是如何工作的:

1. 定义数据模型

kotlin
data class User(
    @field:NotBlank(message = "用户名不能为空") // [!code highlight]
    @field:Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间") // [!code highlight]
    val username: String,
    
    @field:NotBlank(message = "邮箱不能为空") // [!code highlight]
    @field:Email(message = "邮箱格式不正确") // [!code highlight]
    val email: String,
    
    @field:NotNull(message = "年龄不能为空") // [!code highlight]
    @field:Min(value = 18, message = "年龄不能小于18岁") // [!code highlight]
    @field:Max(value = 100, message = "年龄不能大于100岁") // [!code highlight]
    val age: Int,
    
    @field:NotBlank(message = "密码不能为空") // [!code highlight]
    @field:Pattern( // [!code highlight]
        regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d@$!%*?&]{8,}$", 
        message = "密码必须包含大小写字母和数字,长度至少8位"
    ) 
    val password: String
)

2. 创建 Controller

kotlin
@RestController
@RequestMapping("/api/users")
class UserController(private val userService: UserService) {
    
    @PostMapping("/register")
    fun registerUser(
        @Valid @RequestBody user: User
    ): ResponseEntity<UserResponse> {
        val savedUser = userService.createUser(user)
        return ResponseEntity.ok(UserResponse.from(savedUser))
    }
    
    @PutMapping("/{id}")
    fun updateUser(
        @PathVariable id: Long,
        @Valid @RequestBody user: User
    ): ResponseEntity<UserResponse> {
        val updatedUser = userService.updateUser(id, user)
        return ResponseEntity.ok(UserResponse.from(updatedUser))
    }
}

3. 全局异常处理

kotlin
@RestControllerAdvice
class GlobalExceptionHandler {
    
    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidationExceptions(
        ex: MethodArgumentNotValidException
    ): ResponseEntity<ValidationErrorResponse> {
        
        val errors = ex.bindingResult.fieldErrors.map { error ->
            FieldError(
                field = error.field,
                message = error.defaultMessage ?: "验证失败"
            )
        }
        
        val response = ValidationErrorResponse(
            message = "数据验证失败",
            errors = errors
        )
        
        return ResponseEntity.badRequest().body(response)
    }
}

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

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

局部验证器:针对特定需求 🎯

有时候我们需要为特定的 Controller 添加自定义验证逻辑:

kotlin
@Controller
class ProductController {
    
    @InitBinder
    fun initBinder(binder: WebDataBinder) {
        // 添加自定义验证器
        binder.addValidators(ProductValidator()) 
    }
    
    @PostMapping("/products")
    fun createProduct(@Valid @RequestBody product: Product): ResponseEntity<Product> {
        return ResponseEntity.ok(productService.save(product))
    }
}

// 自定义验证器
class ProductValidator : Validator {
    
    override fun supports(clazz: Class<*>): Boolean {
        return Product::class.java.isAssignableFrom(clazz)
    }
    
    override fun validate(target: Any, errors: Errors) {
        val product = target as Product
        
        // 自定义业务验证逻辑
        if (product.price <= 0) { 
            errors.rejectValue("price", "price.invalid", "商品价格必须大于0") 
        } 
        
        if (product.category == "electronics" && product.warranty == null) { 
            errors.rejectValue("warranty", "warranty.required", "电子产品必须提供保修信息") 
        } 
    }
}

验证组:不同场景不同规则 🏷️

使用验证组可以在不同场景下应用不同的验证规则:

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

data class User(
    @field:Null(groups = [CreateGroup::class], message = "创建用户时ID必须为空")
    @field:NotNull(groups = [UpdateGroup::class], message = "更新用户时ID不能为空")
    val id: Long?,
    
    @field:NotBlank(groups = [CreateGroup::class, UpdateGroup::class])
    val username: String,
    
    @field:NotBlank(groups = [CreateGroup::class])
    @field:Size(min = 8, groups = [CreateGroup::class])
    val password: String?
)

@RestController
class UserController {
    
    @PostMapping("/users")
    fun createUser(
        @Validated(CreateGroup::class) @RequestBody user: User
    ): ResponseEntity<User> {
        return ResponseEntity.ok(userService.create(user))
    }
    
    @PutMapping("/users/{id}")
    fun updateUser(
        @PathVariable id: Long,
        @Validated(UpdateGroup::class) @RequestBody user: User
    ): ResponseEntity<User> {
        return ResponseEntity.ok(userService.update(id, user))
    }
}

常用验证注解速查表 📋

注解作用示例
@NotNull值不能为 null@NotNull val id: Long
@NotBlank字符串不能为空或空白@NotBlank val name: String
@NotEmpty集合、数组不能为空@NotEmpty val tags: List<String>
@Size限制字符串或集合大小@Size(min=3, max=20) val username: String
@Min/@Max数值范围限制@Min(18) @Max(100) val age: Int
@Email邮箱格式验证@Email val email: String
@Pattern正则表达式验证@Pattern(regexp="^[0-9]+$") val phone: String
@Valid级联验证@Valid val address: Address

高级特性:自定义验证注解 🚀

创建自己的验证注解来处理复杂的业务规则:

自定义验证注解示例
kotlin
// 1. 定义注解
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [PhoneNumberValidator::class])
annotation class PhoneNumber(
    val message: String = "手机号格式不正确",
    val groups: Array<KClass<*>> = [],
    val payload: Array<KClass<out Payload>> = []
)

// 2. 实现验证器
class PhoneNumberValidator : ConstraintValidator<PhoneNumber, String> {
    
    private val phonePattern = "^1[3-9]\\d{9}$".toRegex()
    
    override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean {
        return value?.matches(phonePattern) ?: true // null 值由 @NotNull 处理
    }
}

// 3. 使用自定义注解
data class User(
    @field:NotBlank
    val name: String,
    
    @field:PhoneNumber // [!code highlight]
    val phone: String
)

最佳实践与注意事项 ⚡

WARNING

在 Kotlin 中使用验证注解时,必须使用 @field: 前缀,否则注解会应用到构造函数参数而不是字段上。

性能优化建议

  1. 合理使用验证组:避免在不需要的场景下执行昂贵的验证
  2. 缓存验证器实例:对于复杂的自定义验证器,考虑使用单例模式
  3. 异步验证:对于需要数据库查询的验证,考虑使用异步方式

常见陷阱

  1. 忘记 @Valid 注解:没有 @Valid,验证不会执行
  2. Kotlin 注解位置错误:必须使用 @field: 前缀
  3. 循环依赖验证:在级联验证中要避免循环引用

总结 🎯

Spring MVC 的验证机制为我们提供了:

  • 声明式验证:通过注解简化验证逻辑
  • 标准化:基于 JSR-303/380 标准,具有良好的可移植性
  • 灵活性:支持全局和局部验证器,满足不同需求
  • 可扩展性:可以轻松创建自定义验证注解
  • 国际化支持:验证消息可以国际化

通过合理使用 Spring MVC 的验证机制,我们可以构建更加健壮、安全的 Web 应用程序,让数据验证变得简单而优雅! 🚀