Appearance
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
最佳实践建议 💡
验证设计原则
- 早期验证:在数据进入业务逻辑前就进行验证
- 明确错误:提供清晰、用户友好的错误消息
- 分层验证:API 层验证格式,业务层验证逻辑
- 性能考虑:避免过度复杂的验证逻辑
常见陷阱
- 不要在 Kotlin 中忘记使用
@field:
前缀 - 避免同时使用
@Valid
和约束注解在同一参数上 - 注意
@Validated
和@Valid
的区别
安全提醒
客户端验证只是用户体验的改善,服务端验证才是安全的保障。永远不要相信来自客户端的数据!
总结 🎯
Spring MVC 的数据验证机制就像一道安全门,确保只有合格的数据才能进入你的应用程序。通过合理使用 @Valid
、@Validated
和各种约束注解,你可以:
- ✅ 自动验证请求参数的格式和内容
- ✅ 提供统一的错误处理机制
- ✅ 支持复杂的嵌套对象验证
- ✅ 实现国际化的错误消息
- ✅ 提高应用程序的健壮性和安全性
记住,好的验证机制不仅能防止错误数据进入系统,还能为用户提供友好的反馈,让你的 API 更加专业和可靠! 🚀