Spring Validation校验

发布于:2025-05-21 ⋅ 阅读:(16) ⋅ 点赞:(0)

使用 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 字符串必须匹配正则表达式
@Email 字符串必须是有效的电子邮件地址
@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 头选择对应的消息文件:

  1. 请求头 Accept-Language: zh-CN → 使用 ValidationMessages_zh_CN.properties
  2. 无匹配或默认 → 使用 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 使用建议
  1. 统一管理:将所有校验消息集中到 ValidationMessages 文件中
  2. 命名规范:使用 对象.字段.校验类型 的命名方式(如 user.email.invalid
  3. 避免硬编码:不要在注解中直接写消息内容,全部通过消息键引用
  4. 参数化消息:利用 {min}, {max} 等占位符使消息更灵活
  5. 多语言支持:为每种语言提供单独的消息文件

完毕。


网站公告

今日签到

点亮在社区的每一天
去签到