Appearance
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:
前缀,否则注解会应用到构造函数参数而不是字段上。
性能优化建议
- 合理使用验证组:避免在不需要的场景下执行昂贵的验证
- 缓存验证器实例:对于复杂的自定义验证器,考虑使用单例模式
- 异步验证:对于需要数据库查询的验证,考虑使用异步方式
常见陷阱
- 忘记 @Valid 注解:没有 @Valid,验证不会执行
- Kotlin 注解位置错误:必须使用
@field:
前缀 - 循环依赖验证:在级联验证中要避免循环引用
总结 🎯
Spring MVC 的验证机制为我们提供了:
- ✅ 声明式验证:通过注解简化验证逻辑
- ✅ 标准化:基于 JSR-303/380 标准,具有良好的可移植性
- ✅ 灵活性:支持全局和局部验证器,满足不同需求
- ✅ 可扩展性:可以轻松创建自定义验证注解
- ✅ 国际化支持:验证消息可以国际化
通过合理使用 Spring MVC 的验证机制,我们可以构建更加健壮、安全的 Web 应用程序,让数据验证变得简单而优雅! 🚀