一、JSR303校验简介
JSR303是Java EE 6中的一项规范,全称为"Bean Validation 1.0",它定义了一套基于注解的JavaBean校验机制。通过简单的注解,我们可以优雅地完成参数校验工作,避免在业务代码中编写大量的校验逻辑。
为什么需要参数校验?
-
安全性:防止恶意用户绕过前端校验提交非法数据
-
健壮性:确保系统处理的数据符合预期格式
-
可维护性:将校验逻辑与业务逻辑分离,代码更清晰
-
一致性:统一校验规则,避免重复代码
二、环境搭建
1. 引入依赖
对于Spring Boot项目,只需添加以下starter依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
也可以直接用starter-web包,包含了JSR303
对于非Spring Boot项目,需要手动添加:
<!-- JSR303规范接口 -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<!-- Hibernate实现(最流行的实现) -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.0.Final</version>
</dependency>
<!-- 表达式语言依赖(用于动态消息) -->
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.0</version>
</dependency>
三、基础使用
1. 内置校验注解
JSR303提供了一系列内置注解:
注解 | 适用类型 | 说明 |
---|---|---|
@Null | 任意 | 必须为null |
@NotNull | 任意 | 必须不为null |
@AssertTrue | Boolean | 必须为true |
@AssertFalse | Boolean | 必须为false |
@Min | 数字 | 必须大于等于指定值 |
@Max | 数字 | 必须小于等于指定值 |
@DecimalMin | 数字 | 必须大于等于指定值(字符串形式) |
@DecimalMax | 数字 | 必须小于等于指定值(字符串形式) |
@Size | 集合/字符串 | 大小必须在指定范围内 |
@Digits | 数字 | 必须是指定位数内的数字 |
@Past | 日期 | 必须是过去的日期 |
@Future | 日期 | 必须是将来的日期 |
@Pattern | 字符串 | 必须匹配正则表达式 |
@Email | 字符串 | 必须是电子邮件格式 |
2. 基本使用示例
public class UserDTO {
@NotNull(message = "用户ID不能为空")
private Long id;
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20个字符之间")
private String username;
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$",
message = "密码必须包含大小写字母和数字,且长度至少8位")
private String password;
@Email(message = "邮箱格式不正确")
private String email;
@Min(value = 18, message = "年龄必须大于等于18岁")
@Max(value = 120, message = "年龄必须小于等于120岁")
private Integer age;
// getters and setters
}
3. 在Controller中使用校验
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public ResponseEntity<String> createUser(@RequestBody @Valid UserDTO userDTO) {
// 业务逻辑
return ResponseEntity.ok("用户创建成功");
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(
@PathVariable @Min(1) Long id,
@RequestParam @Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}") String date) {
// 业务逻辑
return ResponseEntity.ok(new UserDTO());
}
}
四、高级特性
1. 分组校验
通过分组可以实现不同场景下的差异化校验:
public interface CreateGroup {}
public interface UpdateGroup {}
public class UserDTO {
@Null(groups = CreateGroup.class, message = "创建时ID必须为空")
@NotNull(groups = UpdateGroup.class, message = "更新时ID不能为空")
private Long id;
// 其他字段...
}
// 使用分组校验
@PostMapping
public ResponseEntity<String> createUser(@RequestBody @Validated(CreateGroup.class) UserDTO userDTO) {
// 业务逻辑
}
2. 级联校验
当对象包含其他对象时,可以使用@Valid
进行级联校验:
public class OrderDTO {
@Valid
private UserDTO user;
@Valid
private List<@Valid OrderItemDTO> items;
// 其他字段...
}
3. 自定义校验注解
当内置注解不能满足需求时,可以自定义校验注解:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface PhoneNumber {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, 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) {
return true; // 配合@NotNull使用
}
return PHONE_PATTERN.matcher(value).matches();
}
}
// 使用自定义注解
public class UserDTO {
@PhoneNumber
private String phone;
}
五、异常处理
校验失败会抛出MethodArgumentNotValidException
或ConstraintViolationException
,我们可以统一处理:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.badRequest().body(errors);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, String>> handleConstraintViolationException(
ConstraintViolationException ex) {
Map<String, String> errors = new HashMap<>();
ex.getConstraintViolations().forEach(violation -> {
String path = violation.getPropertyPath().toString();
String message = violation.getMessage();
errors.put(path, message);
});
return ResponseEntity.badRequest().body(errors);
}
}
六、最佳实践
-
合理使用message:提供清晰的错误提示,可以使用国际化消息
-
组合使用注解:如
@NotNull
和@Size
一起使用 -
避免过度校验:只在边界处进行严格校验
-
性能考虑:对于复杂校验,考虑异步处理
-
文档化:使用Swagger等工具展示校验规则
七、常见问题
1. 校验不生效怎么办?
-
检查是否添加了
@Valid
或@Validated
注解 -
确认依赖已正确引入
-
检查字段访问权限(需要getter方法)
2. 如何校验集合?
public ResponseEntity<?> batchCreate(
@RequestBody @Valid List<@Valid UserDTO> users) {
// 业务逻辑
}
3. 如何动态修改message?
@Size(min = 6, max = 20, message = "{password.size}")
private String password;
// 在messages.properties中
password.size=密码长度必须在{min}到{max}个字符之间
结语
JSR303校验为Java开发者提供了一套优雅的参数校验解决方案。通过本文的学习,你应该已经掌握了从基础使用到高级特性的全部内容。在实际项目中,合理使用参数校验可以显著提高代码质量和系统安全性。