Spring Validation校验

使用 JSR 303 (Bean Validation) 校验接口参数

JSR 303,也称为Bean Validation规范,提供了一种在Java应用程序中执行验证的标准化方式。它允许你通过注解直接在领域或者DTO(数据传输对象)类上定义校验规则。

1. 添加依赖

首先需要在项目中添加相关依赖:

xml 复制代码
<!-- 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. 在实体类上添加校验注解

java 复制代码
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 校验请求体

java 复制代码
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 校验路径变量和请求参数

java 复制代码
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(
        @PathVariable @Min(1) Long id,
        @RequestParam @NotBlank String type) {
    // 业务逻辑
    return ResponseEntity.ok(userDTO);
}

4. 全局异常处理

java 复制代码
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. 分组校验

可以定义不同的校验组,在不同场景下应用不同的校验规则:

java 复制代码
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. 自定义校验注解

当内置注解不能满足需求时,可以自定义校验注解:

java 复制代码
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}$");
    }
}

使用自定义注解:

java 复制代码
public class UserDTO {
    @ValidPhoneNumber
    private String phone;
}

8. 结合 Hutool 工具自定义

  • 身份证号码正确性校验:
java 复制代码
@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);
        }
    }
}
  • 电话号码正确性校验:
java 复制代码
@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 目录下创建文件:

wiki 复制代码
ValidationMessages.properties         # 默认消息文件
ValidationMessages_zh_CN.properties   # 中文消息文件
ValidationMessages_en_US.properties   # 英文消息文件
ValidationMessages_ja_JP.properties   # 日文消息文件

9.2 文件内容示例

ValidationMessages.properties

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

properties 复制代码
user.id.null=用户ID不能为空
user.name.size=用户名长度必须在{min}到{max}个字符之间

9.3 在注解中引用消息

java 复制代码
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 参数化消息

消息中可以包含参数,参数会在运行时被替换:

properties 复制代码
user.name.size=用户名长度必须在{min}-{max}个字符之间
user.age.range=年龄必须在{min}到{max}岁之间

注解中的参数会自动填充到消息中:

java 复制代码
@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 解析器配置
java 复制代码
@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 自定义消息插值器
java 复制代码
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 注册自定义校验器
java 复制代码
@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. 多语言支持:为每种语言提供单独的消息文件

完毕。

相关推荐
苏三说技术1 分钟前
Claude Code从失控到起飞,只用了这些技巧
后端
长栎1 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode1 小时前
Redis 在生产项目的使用
前端·后端
用户559822481221 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode1 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战1 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha1 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn1 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户762352425911 小时前
ShardingJDBC
后端
行者全栈架构师2 小时前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端