Skip to content

Spring MVC 数据验证:让你的 API 更加安全可靠 🛡️

为什么需要数据验证?

想象一下,你开了一家餐厅,顾客可以通过手机 App 点餐。如果没有任何检查机制,顾客可能会:

  • 订购数量为 -5 份的汉堡 🍔
  • 输入电话号码为 "我是外星人" 👽
  • 提交空白的收货地址 📍

这些无效数据会让你的系统崩溃,订单无法处理。数据验证就像餐厅的服务员,在订单进入厨房前先检查一遍,确保所有信息都是合理的。

IMPORTANT

Spring MVC 的数据验证机制帮助开发者在数据进入业务逻辑之前,自动检查和过滤无效数据,提高系统的健壮性和安全性。

Spring MVC 验证机制概览

Spring MVC 提供了两种层次的验证机制:

两种验证级别详解

1. 方法参数级别验证 📝

这是最常见的验证方式,适用于 @RequestBody@ModelAttribute@RequestPart 参数。

kotlin
@RestController
@RequestMapping("/api/users")
class UserController {

    @PostMapping("/register")
    fun registerUser(@Valid @RequestBody user: UserRegistrationDto): ResponseEntity<String> { 
        // 如果验证失败,会抛出 MethodArgumentNotValidException
        // 验证成功才会执行到这里
        return ResponseEntity.ok("用户注册成功")
    }
}

data class UserRegistrationDto(
    @field:NotBlank(message = "用户名不能为空")
    @field:Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间")
    val username: String,

    @field:Email(message = "邮箱格式不正确")
    @field:NotBlank(message = "邮箱不能为空")
    val email: String,

    @field:NotBlank(message = "密码不能为空")
    @field:Size(min = 6, message = "密码长度至少6位")
    val password: String,

    @field:Min(value = 18, message = "年龄必须大于等于18岁")
    @field:Max(value = 100, message = "年龄不能超过100岁")
    val age: Int
)
kotlin
@RestController
@RequestMapping("/api/products")
class ProductController {

    @PostMapping
    fun createProduct(@Valid @RequestBody product: ProductDto): ResponseEntity<String> { 
        return ResponseEntity.ok("商品创建成功")
    }
}

data class ProductDto(
    @field:NotBlank(message = "商品名称不能为空")
    val name: String,

    @field:DecimalMin(value = "0.01", message = "价格必须大于0")
    val price: BigDecimal,

    @field:Min(value = 0, message = "库存不能为负数")
    val stock: Int
)

TIP

在 Kotlin 中使用 Bean Validation 注解时,需要加上 @field: 前缀,这样注解才会应用到属性字段上。

2. 方法级别验证 🎯

当你需要对方法参数直接添加约束注解时,Spring 会启用方法级别验证:

kotlin
@RestController
@RequestMapping("/api/orders")
class OrderController {

    @GetMapping("/{orderId}")
    fun getOrder(
        @PathVariable @Min(value = 1, message = "订单ID必须大于0") orderId: Long, 
        @RequestParam @NotBlank(message = "用户ID不能为空") userId: String
    ): ResponseEntity<OrderDto> {
        // 如果验证失败,会抛出 HandlerMethodValidationException
        return ResponseEntity.ok(OrderDto(orderId, userId, "已完成"))
    }

    @PostMapping("/search")
    fun searchOrders(
        @RequestParam @Size(min = 1, max = 10, message = "关键词长度必须在1-10个字符之间") keyword: String, 
        @RequestParam @Min(value = 1, message = "页码必须大于0") page: Int = 1, 
        @RequestParam @Min(value = 1, message = "每页大小必须大于0") @Max(value = 100, message = "每页最多100条") size: Int = 10
    ): ResponseEntity<List<OrderDto>> {
        // 验证逻辑会自动执行
        return ResponseEntity.ok(emptyList())
    }
}

验证异常处理 🚨

Spring MVC 会根据验证级别抛出不同的异常:

验证级别异常类型触发条件
方法参数级别MethodArgumentNotValidException@Valid/@Validated 注解的参数验证失败
方法级别HandlerMethodValidationException方法参数上的约束注解验证失败

统一异常处理

kotlin
@ControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidationException(ex: MethodArgumentNotValidException): ResponseEntity<ErrorResponse> {
        val errors = ex.bindingResult.fieldErrors.map { error ->
            FieldError(error.field, error.defaultMessage ?: "验证失败")
        }
        
        return ResponseEntity.badRequest().body(
            ErrorResponse(
                code = "VALIDATION_FAILED",
                message = "请求参数验证失败",
                errors = errors
            )
        )
    }

    @ExceptionHandler(HandlerMethodValidationException::class)
    fun handleMethodValidationException(ex: HandlerMethodValidationException): ResponseEntity<ErrorResponse> {
        val errors = mutableListOf<FieldError>()
        
        // 使用访问者模式处理不同类型的参数验证错误
        ex.visitResults(object : HandlerMethodValidationException.Visitor {
            override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) {
                result.resolvableErrors.forEach { error ->
                    errors.add(FieldError(
                        field = requestParam?.name ?: "unknown",
                        message = error.defaultMessage ?: "参数验证失败"
                    ))
                }
            }
            
            override fun pathVariable(pathVariable: PathVariable?, result: ParameterValidationResult) {
                result.resolvableErrors.forEach { error ->
                    errors.add(FieldError(
                        field = pathVariable?.name ?: "unknown", 
                        message = error.defaultMessage ?: "路径参数验证失败"
                    ))
                }
            }
            
            override fun other(result: ParameterValidationResult) {
                result.resolvableErrors.forEach { error ->
                    errors.add(FieldError(
                        field = "parameter",
                        message = error.defaultMessage ?: "参数验证失败"
                    ))
                }
            }
        })
        
        return ResponseEntity.badRequest().body(
            ErrorResponse(
                code = "METHOD_VALIDATION_FAILED",
                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
)

高级验证技巧 🎨

1. 自定义验证器

kotlin
// 自定义注解
@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>> = []
)

// 验证器实现
class PhoneNumberValidator : ConstraintValidator<PhoneNumber, String> {
    override fun isValid(value: String?, context: ConstraintValidatorContext): Boolean {
        if (value.isNullOrBlank()) return true // 空值由 @NotBlank 处理
        return value.matches(Regex("^1[3-9]\\d{9}$"))
    }
}

// 使用示例
data class UserProfileDto(
    @field:NotBlank(message = "姓名不能为空")
    val name: String,
    
    @field:PhoneNumber // [!code highlight]
    val phone: String
)

2. 分组验证

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

data class UserDto(
    val id: Long?,
    
    @field:NotBlank(message = "用户名不能为空", groups = [CreateGroup::class, UpdateGroup::class])
    val username: String,
    
    @field:NotBlank(message = "密码不能为空", groups = [CreateGroup::class]) // [!code highlight]
    val password: String?
)

@RestController
class UserController {
    
    @PostMapping("/users")
    fun createUser(@Validated(CreateGroup::class) @RequestBody user: UserDto): ResponseEntity<String> { 
        // 只验证 CreateGroup 组的约束
        return ResponseEntity.ok("用户创建成功")
    }
    
    @PutMapping("/users/{id}")
    fun updateUser(
        @PathVariable id: Long,
        @Validated(UpdateGroup::class) @RequestBody user: UserDto
    ): ResponseEntity<String> {
        // 只验证 UpdateGroup 组的约束(不包括密码)
        return ResponseEntity.ok("用户更新成功")
    }
}

3. 嵌套对象验证

kotlin
data class OrderDto(
    @field:NotBlank(message = "订单号不能为空")
    val orderNumber: String,
    
    @field:Valid // [!code highlight]
    @field:NotNull(message = "收货地址不能为空")
    val shippingAddress: AddressDto,
    
    @field:Valid // [!code highlight]
    @field:NotEmpty(message = "订单项不能为空")
    val items: List<OrderItemDto>
)

data class AddressDto(
    @field:NotBlank(message = "收货人姓名不能为空")
    val receiverName: String,
    
    @field:NotBlank(message = "详细地址不能为空")
    val detailAddress: String,
    
    @field:PhoneNumber
    val phone: String
)

data class OrderItemDto(
    @field:NotBlank(message = "商品名称不能为空")
    val productName: String,
    
    @field:Min(value = 1, message = "数量必须大于0")
    val quantity: Int,
    
    @field:DecimalMin(value = "0.01", message = "价格必须大于0")
    val price: BigDecimal
)

验证配置与优化 ⚙️

全局验证器配置

kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
    
    override fun getValidator(): Validator {
        val factory = LocalValidatorFactoryBean()
        factory.setValidationMessageSource(messageSource()) 
        return factory
    }
    
    @Bean
    fun messageSource(): MessageSource {
        val messageSource = ReloadableResourceBundleMessageSource()
        messageSource.setBasename("classpath:validation-messages")
        messageSource.setDefaultEncoding("UTF-8")
        return messageSource
    }
}

国际化错误消息

validation-messages.properties 配置示例
properties
# validation-messages.properties
NotBlank.user.username=用户名不能为空
Size.user.username=用户名长度必须在{min}-{max}个字符之间
Email.user.email=请输入有效的邮箱地址
Min.user.age=年龄必须大于等于{value}岁

# validation-messages_en.properties  
NotBlank.user.username=Username cannot be blank
Size.user.username=Username must be between {min} and {max} characters
Email.user.email=Please enter a valid email address
Min.user.age=Age must be at least {value} years old

最佳实践建议 💡

验证设计原则

  1. 早期验证:在数据进入业务逻辑前就进行验证
  2. 明确错误:提供清晰、用户友好的错误消息
  3. 分层验证:API 层验证格式,业务层验证逻辑
  4. 性能考虑:避免过度复杂的验证逻辑

常见陷阱

  • 不要在 Kotlin 中忘记使用 @field: 前缀
  • 避免同时使用 @Valid 和约束注解在同一参数上
  • 注意 @Validated@Valid 的区别

安全提醒

客户端验证只是用户体验的改善,服务端验证才是安全的保障。永远不要相信来自客户端的数据!

总结 🎯

Spring MVC 的数据验证机制就像一道安全门,确保只有合格的数据才能进入你的应用程序。通过合理使用 @Valid@Validated 和各种约束注解,你可以:

  • ✅ 自动验证请求参数的格式和内容
  • ✅ 提供统一的错误处理机制
  • ✅ 支持复杂的嵌套对象验证
  • ✅ 实现国际化的错误消息
  • ✅ 提高应用程序的健壮性和安全性

记住,好的验证机制不仅能防止错误数据进入系统,还能为用户提供友好的反馈,让你的 API 更加专业和可靠! 🚀