数据校验
为什么需要数据校验?
在Web应用中,用户输入的数据可能不符合预期,数据校验可以:
- 确保数据的完整性和有效性
- 提高应用的安全性
- 提供友好的错误提示
Bean Validation (JSR-380)
Spring MVC集成了Bean Validation规范,提供了一套标准的校验注解。
添加依赖
Maven:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>常用校验注解
1. @NotNull, @NotEmpty, @NotBlank
java
public class User {
@NotNull(message = "ID不能为空")
private Long id;
@NotEmpty(message = "用户名不能为空") // 不为null且长度>0
private String username;
@NotBlank(message = "密码不能为空") // 不为null且trim后长度>0
private String password;
}2. @Size - 字符串/集合长度
java
public class User {
@Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
private String username;
@Size(min = 1, message = "至少需要一个角色")
private List<String> roles;
}3. @Email - 邮箱格式
java
public class User {
@Email(message = "邮箱格式不正确")
private String email;
}4. @Pattern - 正则表达式
java
public class User {
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
private String username;
}5. @Min, @Max - 数值范围
java
public class Product {
@Min(value = 0, message = "价格不能为负数")
private BigDecimal price;
@Max(value = 100, message = "折扣不能超过100")
private Integer discount;
}6. @DecimalMin, @DecimalMax
java
public class Product {
@DecimalMin(value = "0.01", message = "价格最低为0.01")
@DecimalMax(value = "999999.99", message = "价格最高为999999.99")
private BigDecimal price;
}7. @Positive, @Negative
java
public class Order {
@Positive(message = "数量必须为正数")
private Integer quantity;
@PositiveOrZero(message = "折扣金额必须为非负数")
private BigDecimal discount;
}8. @Past, @Future - 日期校验
java
public class User {
@Past(message = "生日必须是过去的日期")
private LocalDate birthday;
@Future(message = "截止日期必须是未来的日期")
private LocalDate deadline;
}9. @Valid - 嵌套校验
java
public class Order {
@Valid
@NotNull(message = "订单项不能为空")
private List<OrderItem> items;
@Valid
@NotNull(message = "收货地址不能为空")
private Address shippingAddress;
}
public class OrderItem {
@NotNull(message = "商品不能为空")
private Product product;
@Positive(message = "数量必须为正数")
private Integer quantity;
}在Controller中使用
基本使用
java
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
User savedUser = userService.save(user);
return ResponseEntity.ok(savedUser);
}
}处理校验错误
java
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<?> createUser(
@Valid @RequestBody User user,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
Map<String, String> errors = new HashMap<>();
bindingResult.getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity.badRequest().body(errors);
}
User savedUser = userService.save(user);
return ResponseEntity.ok(savedUser);
}
}全局异常处理
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity.badRequest().body(errors);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, String>> handleConstraintViolation(
ConstraintViolationException ex) {
Map<String, String> errors = new HashMap<>();
ex.getConstraintViolations().forEach(violation -> {
String propertyPath = violation.getPropertyPath().toString();
String message = violation.getMessage();
errors.put(propertyPath, message);
});
return ResponseEntity.badRequest().body(errors);
}
}自定义校验注解
1. 创建注解
java
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
@Documented
public @interface Phone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}2. 实现校验器
java
public class PhoneValidator implements ConstraintValidator<Phone, String> {
private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true; // 使用@NotBlank单独校验
}
return PHONE_PATTERN.matcher(value).matches();
}
}3. 使用自定义注解
java
public class User {
@Phone(message = "手机号格式不正确")
private String phone;
}分组校验
用于不同场景下应用不同的校验规则。
1. 定义分组接口
java
public interface CreateGroup {}
public interface UpdateGroup {}2. 使用分组
java
public class User {
@NotNull(groups = UpdateGroup.class, message = "更新时ID不能为空")
private Long id;
@NotBlank(groups = {CreateGroup.class, UpdateGroup.class}, message = "用户名不能为空")
@Size(min = 3, max = 20, groups = {CreateGroup.class, UpdateGroup.class})
private String username;
@NotBlank(groups = CreateGroup.class, message = "密码不能为空")
private String password;
}3. Controller中指定分组
java
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<User> createUser(
@Validated(CreateGroup.class) @RequestBody User user) {
return ResponseEntity.ok(userService.save(user));
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(
@PathVariable Long id,
@Validated(UpdateGroup.class) @RequestBody User user) {
return ResponseEntity.ok(userService.update(id, user));
}
}方法级校验
1. 启用方法级校验
java
@Configuration
@EnableWebMvc
public class WebConfig {
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}2. 使用方法级校验
java
@Service
@Validated
public class UserService {
public User getUserById(@NotNull(message = "用户ID不能为空") Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("用户不存在"));
}
public void deleteUser(@Min(value = 1, message = "ID必须大于0") Long id) {
userRepository.deleteById(id);
}
}完整示例
实体类
java
@Data
public class UserRegistrationDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 30, message = "密码长度必须在6-30之间")
private String password;
@NotBlank(message = "确认密码不能为空")
private String confirmPassword;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@Phone(message = "手机号格式不正确")
private String phone;
@Past(message = "生日必须是过去的日期")
private LocalDate birthday;
@Min(value = 18, message = "年龄必须大于等于18岁")
@Max(value = 120, message = "年龄必须小于等于120岁")
private Integer age;
@AssertTrue(message = "必须同意用户协议")
private Boolean agreeTerms;
// 自定义校验逻辑
@AssertTrue(message = "两次密码输入不一致")
public boolean isPasswordMatch() {
return password != null && password.equals(confirmPassword);
}
}Controller
java
@RestController
@RequestMapping("/api/users")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register")
public ResponseEntity<?> register(
@Valid @RequestBody UserRegistrationDTO dto) {
try {
User user = userService.register(dto);
return ResponseEntity.ok(user);
} catch (Exception e) {
log.error("注册失败", e);
return ResponseEntity.badRequest().body("注册失败: " + e.getMessage());
}
}
}全局异常处理
java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationExceptions(
MethodArgumentNotValidException ex) {
log.error("参数校验失败", ex);
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error -> {
errors.put(error.getField(), error.getDefaultMessage());
});
ErrorResponse response = ErrorResponse.builder()
.status(HttpStatus.BAD_REQUEST.value())
.message("参数校验失败")
.errors(errors)
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.badRequest().body(response);
}
}
@Data
@Builder
public class ErrorResponse {
private int status;
private String message;
private Map<String, String> errors;
private LocalDateTime timestamp;
}最佳实践
选择合适的校验注解
- @NotNull:检查不为null
- @NotEmpty:检查不为null且不为空(字符串、集合)
- @NotBlank:检查不为null且trim后不为空(仅字符串)
提供清晰的错误信息
java@NotBlank(message = "用户名不能为空") @Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")使用分组校验
- 针对不同场景使用不同的校验规则
嵌套对象校验
- 使用@Valid注解进行级联校验
自定义校验器
- 复杂的业务逻辑使用自定义校验注解
全局异常处理
- 统一处理校验异常,提供一致的错误响应
前后端双重校验
- 前端校验提升用户体验
- 后端校验保证数据安全
总结
数据校验是Web应用开发中的重要环节,Spring MVC提供了强大的校验框架:
- 使用标准注解进行常规校验
- 自定义注解处理特殊需求
- 分组校验应对不同场景
- 全局异常处理提供统一的错误响应
合理使用数据校验可以大大提高应用的健壮性和安全性。