使用 JSR 303 (Bean Validation) 校验接口参数
JSR 303
,也称为Bean Validation规范,提供了一种在Java应用程序中执行验证的标准化方式。它允许你通过注解直接在领域或者DTO(数据传输对象)类上定义校验规则。
1. 添加依赖
首先需要在项目中添加相关依赖:
<!-- Spring Boot 项目只需添加这个 starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 非 Spring Boot 项目需要添加这些 -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.13.Final</version>
</dependency>
2. 在实体类上添加校验注解
import javax.validation.constraints.*;
public class UserDTO {
@NotNull(message = "用户ID不能为空")
private Long id;
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20个字符之间")
private String username;
@Min(value = 18, message = "年龄必须大于等于18岁")
@Max(value = 120, message = "年龄必须小于等于120岁")
private Integer age;
@Email(message = "邮箱格式不正确")
private String email;
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$",
message = "密码必须包含大小写字母和数字,且长度至少8位")
private String password;
// getters and setters
}
3. 在 Controller 中使用校验
3.1 校验请求体
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<String> createUser(@RequestBody @Validated UserDTO userDTO) {
// 如果校验失败,会抛出 MethodArgumentNotValidException
// 业务逻辑处理
return ResponseEntity.ok("用户创建成功");
}
}
3.2 校验路径变量和请求参数
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(
@PathVariable @Min(1) Long id,
@RequestParam @NotBlank String type) {
// 业务逻辑
return ResponseEntity.ok(userDTO);
}
4. 全局异常处理
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@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);
}
}
5. 常用校验注解
注解 | 说明 |
---|---|
@NotNull | 值不能为null |
@NotEmpty | 字符串/集合不能为null或空 |
@NotBlank | 字符串不能为null且必须包含至少一个非空白字符 |
@Size | 字符串/集合/数组的大小必须在指定范围内 |
@Min | 数字最小值 |
@Max | 数字最大值 |
@DecimalMin | 小数值最小值 |
@DecimalMax | 小数值最大值 |
@Digits | 数字的整数和小数部分的位数限制 |
@Past | 日期必须在过去 |
@PastOrPresent | 日期必须在过去或现在 |
@Future | 日期必须在未来 |
@FutureOrPresent | 日期必须在未来或现在 |
@Pattern | 字符串必须匹配正则表达式 |
字符串必须是有效的电子邮件地址 | |
@Positive | 数字必须是正数 |
@PositiveOrZero | 数字必须是正数或零 |
@Negative | 数字必须是负数 |
@NegativeOrZero | 数字必须是负数或零 |
6. 分组校验
可以定义不同的校验组,在不同场景下应用不同的校验规则:
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) {
// 业务逻辑
}
7. 自定义校验注解
当内置注解不能满足需求时,可以自定义校验注解:
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
// 自定义注解
@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPhoneNumber {
String message() default "无效的手机号码";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 自定义校验规则
public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {
@Override
public boolean isValid(String phoneNumber, ConstraintValidatorContext context) {
// 实现校验逻辑
return phoneNumber != null && phoneNumber.matches("^1[3-9]\\d{9}$");
}
}
使用自定义注解:
public class UserDTO {
@ValidPhoneNumber
private String phone;
}
8. 结合 Hutool 工具自定义
- 身份证号码正确性校验:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IDCard.IDCardCheck.class)
public @interface IDCard {
boolean required() default true;
String message() default "请输入正确的身份证号码";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* 校验规则
*/
@Component
class IDCardCheck implements ConstraintValidator<IDCard, String> {
private boolean required;
@Override
public void initialize(IDCard constraintAnnotation) {
this.required = constraintAnnotation.required();
}
@Override
public boolean isValid(String idCard, ConstraintValidatorContext constraintValidatorContext) {
// 非必填
if (!required) {
return true;
}
// 使用 Hutool 的工具
return IdcardUtil.isValidCard(idCard);
}
}
}
- 电话号码正确性校验:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = Phone.PhoneCheck.class)
public @interface Phone {
boolean required() default true;
String message() default "请输入正确的手机号码";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* 校验规则
*/
@Component
class PhoneCheck implements ConstraintValidator<Phone, String> {
private boolean required;
@Override
public void initialize(Phone constraintAnnotation) {
this.required = constraintAnnotation.required();
}
@Override
public boolean isValid(String phone, ConstraintValidatorContext constraintValidatorContext) {
// 非必填
if (!required) {
return true;
}
// 使用 Hutool 的工具
return PhoneUtil.isPhone(phone);
}
}
}
9. 国际化支持
9.1 创建消息文件
在 src/main/resources
目录下创建文件:
ValidationMessages.properties # 默认消息文件
ValidationMessages_zh_CN.properties # 中文消息文件
ValidationMessages_en_US.properties # 英文消息文件
ValidationMessages_ja_JP.properties # 日文消息文件
9.2 文件内容示例
ValidationMessages.properties:
# 通用消息
user.id.null=用户ID不能为空
user.name.size=用户名长度必须在{min}-{max}个字符之间
user.age.range=年龄必须在{min}到{max}岁之间
user.email.invalid=请输入有效的电子邮件地址
user.password.pattern=密码必须包含大小写字母和数字,且长度至少8位
# 自定义注解消息
phone.invalid=手机号格式不正确,请输入11位有效手机号
ValidationMessages_zh_CN.properties:
user.id.null=用户ID不能为空
user.name.size=用户名长度必须在{min}到{max}个字符之间
9.3 在注解中引用消息
public class UserDTO {
@NotNull(message = "{user.id.null}")
private Long id;
@Size(min = 2, max = 20, message = "{user.name.size}")
private String username;
@Min(value = 18, message = "{user.age.range}")
@Max(value = 120, message = "{user.age.range}")
private Integer age;
@Email(message = "{user.email.invalid}")
private String email;
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$",
message = "{user.password.pattern}")
private String password;
@Phone(message = "{phone.invalid}") // 自定义注解
private String phone;
}
9.4 参数化消息
消息中可以包含参数,参数会在运行时被替换:
user.name.size=用户名长度必须在{min}-{max}个字符之间
user.age.range=年龄必须在{min}到{max}岁之间
注解中的参数会自动填充到消息中:
@Size(min = 2, max = 20, message = "{user.name.size}")
private String username; // 显示:用户名长度必须在2-20个字符之间
9.5 国际化关键实现
Spring Boot 会自动根据请求的 Accept-Language
头选择对应的消息文件:
- 请求头
Accept-Language: zh-CN
→ 使用ValidationMessages_zh_CN.properties
- 无匹配或默认 → 使用
ValidationMessages.properties
9.5.1 Locale 解析器配置
@Configuration
public class LocaleConfig {
// 基于请求头的解析器
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(Locale.ENGLISH);
return resolver;
}
// 消息源配置
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
source.setBasename("ValidationMessages");
source.setDefaultEncoding("UTF-8");
source.setUseCodeAsDefaultMessage(true);
return source;
}
}
9.5.2 自定义消息插值器
public class I18nMessageInterpolator implements MessageInterpolator {
private final MessageSource messageSource;
private final LocaleResolver localeResolver;
public I18nMessageInterpolator(MessageSource messageSource,
LocaleResolver localeResolver) {
this.messageSource = messageSource;
this.localeResolver = localeResolver;
}
@Override
public String interpolate(String messageTemplate, Context context) {
return interpolate(messageTemplate, context, Locale.getDefault());
}
@Override
public String interpolate(String messageTemplate, Context context, Locale locale) {
try {
// 解析消息键(去掉花括号)
if (messageTemplate.startsWith("{") && messageTemplate.endsWith("}")) {
String messageKey = messageTemplate.substring(1, messageTemplate.length() - 1);
return messageSource.getMessage(messageKey, resolveArguments(context), locale);
}
return messageTemplate;
} catch (NoSuchMessageException e) {
return messageTemplate;
}
}
private Object[] resolveArguments(Context context) {
// 从校验注解中提取参数(如@Size的min/max)
if (context.getConstraintDescriptor().getAnnotation() instanceof Size) {
Size size = (Size) context.getConstraintDescriptor().getAnnotation();
return new Object[] {
context.getPropertyPath().toString(), // 字段名
size.max(),
size.min()
};
}
return new Object[0];
}
}
9.5.3 注册自定义校验器
@Bean
public Validator validator(MessageSource messageSource, LocaleResolver localeResolver) {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
factoryBean.setMessageInterpolator(
new I18nMessageInterpolator(messageSource, localeResolver));
return factoryBean;
}
9.5.4 使用建议
- 统一管理:将所有校验消息集中到 ValidationMessages 文件中
- 命名规范:使用
对象.字段.校验类型
的命名方式(如user.email.invalid
) - 避免硬编码:不要在注解中直接写消息内容,全部通过消息键引用
- 参数化消息:利用
{min}
,{max}
等占位符使消息更灵活 - 多语言支持:为每种语言提供单独的消息文件
完毕。