Spring Validation使用指南

Spring Validation使用指南

Bean Validation

jsr303 -> jsr349 -> jsr380

在日常的项目开发中,为了防止非法参数对业务造成的影响,需要对接口的参数做合法性校验。

Bean Validation中提供的constraints:

注解说明
@AssertFalse被注释的元素必须为false,支持boolean或Boolean,null元素被认为是有效的。
@AssertTrue被注释的元素必须为true,支持boolean或Boolean,null元素被认为是有效的。
@DecimalMax被注释的元素必须是一个数字,值必须小于或等于指定最大值的。支持BigDecimal、BigInteger、CharSequence、(byte、 short、 int、long及他们包装类),不支持double和float,null元素被认为是有效的。
@DecimalMin被注释的元素必须是一个数字,其值必须大于或等于指定的最小值。支持BigDecimal、BigInteger、CharSequence、(byte、 short、 int、long及他们包装类),不支持double和float,null元素被认为是有效的。
@Digits被注释的元素必须是可接受范围内的数字。支持BigDecimal、BigInteger、CharSequence、(byte、 short、 int、long及他们包装类),null元素被认为是有效的。
@Email字符串必须是格式正确的电子邮件地址,接受CharSequence,null元素被认为是有效的。
@Future被注释的元素必须是将来的instant,date或time。null元素被认为是有效的。支持的类型:
java.util.Date
java.util.Calendar
java.time.Instant
java.time.LocalDate
java.time.LocalDateTime
java.time.LocalTime
java.time.MonthDay
java.time.OffsetDateTime
java.time.OffsetTime
java.time.Year
java.time.YearMonth
java.time.ZonedDateTime
java.time.chrono.HijrahDate
java.time.chrono.JapaneseDate
java.time.chrono.MinguoDate
java.time.chrono.ThaiBuddhistDate
@FutureOrPresent被注释的元素必须是当前或未来的instant,date或time。null元素被认为是有效的。支持的类型:
java.util.Date
java.util.Calendar
java.time.Instant
java.time.LocalDate
java.time.LocalDateTime
java.time.LocalTime
java.time.MonthDay
java.time.OffsetDateTime
java.time.OffsetTime
java.time.Year
java.time.YearMonth
java.time.ZonedDateTime
java.time.chrono.HijrahDate
java.time.chrono.JapaneseDate
java.time.chrono.MinguoDate
java.time.chrono.ThaiBuddhistDate
@Max被注释的元素必须是一个值必须小于或等于指定最大值的数字。支持BigDecimal、BigInteger、(byte、 short、 int、long及他们包装类),不支持double和float,null元素被认为是有效的。
@Min被注释的元素必须是一个数值,其值必须大于或等于指定的最小值。支持BigDecimal、BigInteger、(byte、 short、 int、long及他们包装类),不支持double和float,null元素被认为是有效的。
@Negative被注释的元素必须为负数(即0被视为无效值)。支持BigDecimal、BigInteger、(byte、short、int、long、float、double及他们的包装类),null元素被认为是有效的。
@NegativeOrZero被注释的元素必须是负数或0。支持BigDecimal、BigInteger、(byte、short、int、long、float、double及他们的包装类),null元素被认为是有效的。
@NotBlank被注释的元素不能为null,并且必须至少包含一个非空白字符。接收CharSequence类型。
@NotEmpty被注释的元素不能为null或empty。支持的类型:
CharSequence (统计字符序列的长度)
Collection (统计集合大小)
Map (统计Map大小)
Array (统计数组长度)
@NotNull被注释的元素不能为null。接受任何类型。
@Null被注释的元素必须为空。接受任何类型。
@Past被注释的元素必须是过去的instant、date或time。null元素被认为是有效的。支持类型:
java.util.Date
java.util.Calendar
java.time.Instant
java.time.LocalDate
java.time.LocalDateTime
java.time.LocalTime
java.time.MonthDay
java.time.OffsetDateTime
java.time.OffsetTime
java.time.Year
java.time.YearMonth
java.time.ZonedDateTime
java.time.chrono.HijrahDate
java.time.chrono.JapaneseDate
java.time.chrono.MinguoDate
java.time.chrono.ThaiBuddhistDate
@PastOrPresent带注释的元素必须是过去或现在的instant、date或time。null元素被认为是有效的。支持类型:
java.util.Date
java.util.Calendar
java.time.Instant
java.time.LocalDate
java.time.LocalDateTime
java.time.LocalTime
java.time.MonthDay
java.time.OffsetDateTime
java.time.OffsetTime
java.time.Year
java.time.YearMonth
java.time.ZonedDateTime
java.time.chrono.HijrahDate
java.time.chrono.JapaneseDate
java.time.chrono.MinguoDate
java.time.chrono.ThaiBuddhistDate
@Pattern被注释的CharSequence必须与指定的正则表达式匹配。正则表达式遵循Java正则表达式约定。接收CharSequence。null元素被认为是有效的。
@Positive被注释的元素必须是正数(即0被视为无效值)。支持BigDecimal、BigInteger、(byte, short, int, long, float, double及他们的包装类)。null元素被认为是有效的。
@PositiveOrZero被注释的元素必须是正数或0。支持BigDecimal、BigInteger、(byte, short, int, long, float, double及他们的包装类)。null元素被认为是有效的。
@Size被注释的元素大小必须在指定边界(包括)之间。null元素被认为是有效的。支持的类型:
CharSequence (统计字符序列的长度)
Collection (统计集合大小)
Map (统计Map大小)
Array (统计数组长度)

从Spring Boot 2.3.X开始,spring-boot-starter-web不再引入spring-boot-starter-validation,所以需要额外手动引入。

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>

@Validated与@Valid区别

@Validated是只用Spring Validator校验机制使用,是@Valid 的一次封装。

两者最大的区别在于@Validated支持分组,@Valid支持用于成员属性上。

其次:

@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性上,不支持嵌套检测;

@Valid:可以用在方法、构造方法、方法参数和成员属性上,支持嵌套检测。

这里提供一个Spring Validation具体示例:

创建一个SpringBoot项目,版本2.6.12,pom.xml文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.12</version>
        <relativePath/>
    </parent>
    <groupId>com.lwy.it</groupId>
    <artifactId>spring-validation</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-validation</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

实体主要包括书籍、作者、出版社,关系及属性如下:

在这里插入图片描述

全局返回结果统一如下(使用ResultDTO来规范返回值):

import com.fasterxml.jackson.annotation.JsonInclude;
import com.lwy.it.enums.ErrorCodeEnum;
import lombok.Data;

import java.io.Serializable;

/**
 * 使用ResultDTO来规范返回值
 *
 * @author 帅喵
 */
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResultDTO implements Serializable {
    /**
     * 后端是否处理成功
     */
    private boolean success;

    /**
     * 给前端的返回值
     */
    private Object data;

    /**
     * 错误码
     */
    private String code;

    /**
     * 错误消息
     */
    private String errorMessage;

    /**
     * 成功返回结果,无数据
     *
     * @return
     */
    public static final ResultDTO success() {
        ResultDTO ResultDTO = new ResultDTO();
        ResultDTO.setSuccess(true);
        return ResultDTO;
    }

    /**
     * 成功返回结果,包含数据
     *
     * @param data 需要返回的数据
     * @return
     */
    public static final ResultDTO success(Object data) {
        ResultDTO ResultDTO = new ResultDTO();
        ResultDTO.setSuccess(true);
        ResultDTO.setData(data);
        return ResultDTO;
    }

    /**
     * 失败返回结果,无数据
     *
     * @param errorCodeEnum 错误枚举定义
     * @return
     */
    public static final ResultDTO failure(ErrorCodeEnum errorCodeEnum) {
        ResultDTO ResultDTO = new ResultDTO();
        ResultDTO.setSuccess(false);
        ResultDTO.setCode(errorCodeEnum.getCode());
        ResultDTO.setErrorMessage(errorCodeEnum.getMessage());
        return ResultDTO;
    }

    /**
     * 失败返回结果,包含数据
     *
     * @param errorCodeEnum 错误枚举定义
     * @param data          需要返回的数据
     * @return
     */
    public static final ResultDTO failure(ErrorCodeEnum errorCodeEnum, Object data) {
        ResultDTO ResultDTO = new ResultDTO();
        ResultDTO.setSuccess(false);
        ResultDTO.setCode(errorCodeEnum.getCode());
        ResultDTO.setErrorMessage(errorCodeEnum.getMessage());
        ResultDTO.setData(data);
        return ResultDTO;
    }
}

ErrorCodeEnum为定义异常码和信息的枚举类:

package com.lwy.it.enums;

public enum ErrorCodeEnum {
    PARAMETER_ERROR("E1000", "参数输入错误");

    private String code;
    private String message;

    ErrorCodeEnum(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

全局异常处理如下(主要是为了捕获校验出的异常,会把具体的校验异常在data中返回):

package com.lwy.it.exception.handler;

import com.lwy.it.dto.ResultDTO;
import com.lwy.it.enums.ErrorCodeEnum;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Map;
import java.util.stream.Collectors;

/**
 * 使用 @ControllerAdvice 结合 @ExceptionHandler 实现全局异常处理
 *
 * @ControllerAdvice 用来开启全局的异常捕获
 */
@ControllerAdvice(basePackages = "com.lwy.it.controller")
@ResponseBody
public class GlobalExceptionHandler {
    /**
     * 说明捕获哪些异常,对哪些异常进行处理。
     * 专门用来捕获和处理Controller层的MethodArgumentNotValidException异常
     *
     * @param exception MethodArgumentNotValidException异常
     * @return 统一返回结果格式
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultDTO validationExceptionHandler(MethodArgumentNotValidException exception) {
        Map<String, String> map =
                exception.getBindingResult().getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField,
                        FieldError::getDefaultMessage));
        ResultDTO resultDto = ResultDTO.failure(ErrorCodeEnum.PARAMETER_ERROR, map);
        return resultDto;
    }
}

书籍校验BookDTO

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import lombok.Data;

import javax.validation.Valid;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.List;

/**
 * id:不能为空,值要在100000以上
 * bookName:长度在1个到10个字符之间
 * bookAuthor:至少有1个元素
 * description:不能为空
 * publishDate:不能为空,早于当前时间
 * publishingHouseDTO:包含出版社名称,地址,邮件均不能空
 */
@Data
public class BookDTO implements Serializable {
    @NotNull(message = "id不能为空")
    private Integer id;
    @NotBlank(message = "书名不能为空")
    @Size(min = 1, max = 10, message = "书名长度1~10个字符")
    private String bookName;

    /**
     * 注意 @Valid AuthorDTO 嵌套验证校验的是List里面的内容
     */
    @NotEmpty(message = "必须包含一个作者信息")
    private List<@Valid AuthorDTO> bookAuthor;
    @NotBlank(message = "描述不能为空")
    private String description;

    /**
     * @DateTimeFormat是spring包里面的注解,作用是接收从页面传到后台的日期值。注意:url拼接的参数才生效
     * @JsonFormat是jackson包里面的注解,作用是从后台向前台传递日期值。可以设置时区timezone=“GMT+8”
     */
    @NotNull(message = "出版日期不能为空")
    @Past(message = "出版日期不能超过当前日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @JsonDeserialize(using = LocalDateDeserializer.class)
    @JsonSerialize(using = LocalDateSerializer.class)
    private LocalDate publishDate;

    /**
     * 需要使用@Valid,才能实现嵌套验证
     */
    @NotNull(message = "出版社信息不能为空")
    @Valid
    private PublishingHouseDTO publishingHouseDTO;
}

对应作者信息AuthorDTO:

package com.lwy.it.dto;

import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.io.Serializable;

@Data
public class AuthorDTO implements Serializable {
    @NotBlank(message = "身份证号不能为空")
    @Size(min = 18, max = 18, message = "身份证号必须18位数字")
    private String id;
    @NotBlank(message = "作者名不能为空")
    private String name;
}

出版社信息PublishingHouseDTO:

import lombok.Data;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;

@Data
public class PublishingHouseDTO implements Serializable {
    @NotBlank(message = "出版社名称不能为空")
    private String name;
    @NotBlank(message = "出版社地址不能为空")
    private String address;
    @NotNull(message = "出版社邮箱地址不能为空")
    @Email(message = "必填是邮箱格式")
    private String email;
}

下面来写一个controller来进行验证:

@RestController
/**
 * @Validated 对本类中的方法,开启验证功能
 */
@Validated
public class BookController {

    @PostMapping("/addBookInfo")
    public ResultDTO addBookInfo(@RequestBody @Valid BookDTO bookDTO){
        System.out.println(bookDTO);
        return ResultDTO.success();
    }
}

注意:

  • 方法的返回值也可以做验证
  • 在Service层中做参数验证时,全局异常处理@ExceptionHandler捕获的异常类型(ConstraintViolationException)与Controller的异常类型(MethodArgumentNotValidException)不一致
  • 如果使用了接口,那么验证要写在接口上
分组验证

实际项目中经常遇到这种情况,由于某些ID是自动生成的,所以创建时为空,修改时不为空。

示例如下:

import lombok.Data;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import java.io.Serializable;

/**
 * 分组校验,对于id来说:
 * 1. 添加过程不能有值
 * 2. 修改过程id必须有值
 */
@Data
public class PublishingHouseDTO implements Serializable {
    // 在添加的时候生效
    @Null(groups = {CreatePublishingHouse.class}, message = "添加时id为空")
    // 在修改的时候生效
    @NotNull(groups = {UpdatePublishingHouse.class}, message = "修改时id不能为空")
    private String id;
    @NotBlank(message = "出版社名称不能为空")
    private String name;
    @NotBlank(message = "出版社地址不能为空")
    private String address;
    @NotNull(message = "出版社邮箱地址不能为空")
    @Email(message = "必填是邮箱格式")
    private String email;

    public interface CreatePublishingHouse {
    }

    public interface UpdatePublishingHouse {
    }
}

对应校验Controller如下:

import com.lwy.it.dto.PublishingHouseDTO;
import com.lwy.it.dto.ResultDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.groups.Default;

@RestController
/**
 * @Validated 对本类中的方法,开启验证功能
 */
@Validated
public class PublishingHouseController {

    /**
     * @Valid 校验后面的参数
     * 分组需要使用 @Validated 注解
     * 如果指定了验证组,那么该参数就只属于 指定的验证组
     * 如果没有指定了验证组,那么该参数就属于默认组 Default.class
     * 如果不添加 Default.class,默认组参数不会校验
     */
    @PostMapping("/create")
    public ResultDTO create(@RequestBody @Validated({PublishingHouseDTO.CreatePublishingHouse.class, Default.class}) PublishingHouseDTO dto) {
        System.out.println(dto);
        return ResultDTO.success();
    }

    /**
     * 同时校验UpdatePublishingHouse和Default组
     */
    @PostMapping("/update")
    public ResultDTO update(@RequestBody @Validated({PublishingHouseDTO.UpdatePublishingHouse.class, Default.class}) PublishingHouseDTO dto) {
        System.out.println(dto);
        return ResultDTO.success();
    }
}
自定义注解

场景如下:在AuthorDTO中增加一个国籍字段nationality,业务要求:nationality值必须为中国、美国、韩国 才能通过。

/**
 * 使用自定义注解 @Nationality 进行校验
 * 业务逻辑:nationality值必须为中国、美国、韩国 才能通过
 */
@Data
public class AuthorDTO implements Serializable {
    @NotBlank(message = "身份证号不能为空")
    @Size(min = 18, max = 18, message = "身份证号必须18位数字")
    private String id;
    @NotBlank(message = "作者名不能为空")
    private String name;
    @Nationality(message = "国家填写不符合规范")
    private String nationality;
}

自定义注解@Nationality定义:

@Target({FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {NationalityConstraintValidator.class})
public @interface Nationality {
    String message() default "参数国家限制:中国、美国、韩国";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

这里NationalityConstraintValidator.class指向了实现类:

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;

/**
 * ConstraintValidator<要验证哪个注解,验证的类型>
 */
public class NationalityConstraintValidator implements ConstraintValidator<Nationality, String> {
    private final Set<String> set = new HashSet<String>() {{
        add("中国");
        add("美国");
        add("韩国");
    }};

    //会在校验实例化后被调用,一般用于做些初始化工作。
    @Override
    public void initialize(Nationality constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 按照默认逻辑,null值不进行校验
        if (value == null) {
            return true;
        }
        if (set.contains(value)) {
            return true;
        }
        return false;
    }
}

在Controller中添加访问方法:

    @PostMapping("/addAuthor")
    public ResultDTO author(@RequestBody @Valid @NotNull AuthorDTO authorDTO){
        System.out.println(authorDTO);
        return ResultDTO.success();
    }

非Web环境下使用

环境搭建

引入依赖

        <!-- 引入bean validation -->
        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
            <version>3.0.2</version>
        </dependency>

        <!-- 引入hibernate-validator,包含jakarta.validation-api -->
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>8.0.0.Final</version>
        </dependency>

        <!-- 引入el规范和Tomcat的实现,用于解析message里面的表达式 -->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-el</artifactId>
            <version>10.1.0</version>
        </dependency>
使用示例
原理

Bean Validation与Hibernate Validation 实现原理,通过SPI机制(在hibernate-validator包下classpath路径下META-INF/services目录下jakarta.validation.spi.ValidationProvider文件,Service Provider为:org.hibernate.validator.HibernateValidator)

示例:以用户信息为例,创建UserInfoDTO类。

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.URL;

import java.time.LocalDate;

@Data
public class UserInfoDTO {
    private Long id;
    @NotBlank(message = "姓名必填")
    private String name;
    @NotNull(message = "年龄必填")
    @Min(value = 1, message = "年龄最小为1岁")
    @Max(value = 200, message = "年龄最大为200岁")
    /**
     * 与使用 @Range(min = 1, max = 200, message = "年龄范围在1岁至200岁之间") 同样效果
     */
    private Integer age;
    @NotBlank(message = "邮箱必填")
    @Email(message = "必须为邮箱格式")
    private String email;
    @NotBlank(message = "手机号码必填")
    @Length(min = 11, max = 11, message = "手机号码必须为11位数字")
    @Pattern(regexp = "^1(3|4|5|7|8|9)\\d{9}$", message = "手机号码必须符合规范")
    private String phone;
    @NotNull(message = "出生日期必填")
    @Past(message = "出生日期不能超过当前时间")
    private LocalDate birthday;
    @NotBlank(message = "个人主页地址必填")
    @URL(message = "个人主页必须合法")
    private String personalPage;
}

提供一个校验工具类:

import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class ValidationUtil {
    // 线程安全的校验对象
    private static Validator validator;

    static {
        // 通过静态初始化块初始化validator
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    // 校验方法
    public static List<ValidErrorResult> valid(Object object) {
        /**
         * 通过validate方法校验,返回校验不满足的结果
         * 如果被校验对象没有校验通过,则set里面就有校验信息
         */
        Set<ConstraintViolation<Object>> set = validator.validate(object);

        List<ValidErrorResult> validErrorResults = set.stream().map((mapper) -> {
            ValidErrorResult result = new ValidErrorResult();
            result.setField(mapper.getPropertyPath().toString());
            result.setValue(mapper.getInvalidValue());
            result.setMessage(mapper.getMessage());
            return result;
        }).collect(Collectors.toList());
        return validErrorResults;
    }
}

校验结果:

import lombok.Data;

import java.io.Serializable;

@Data
public class ValidErrorResult implements Serializable {
    // 验证的字段
    private String field;
    // 该字段的值
    private Object value;
    // 错误信息
    private String message;
}

Main方法示例:

import com.lwy.it.dto.UserInfoDTO;
import com.lwy.it.util.ValidErrorResult;
import com.lwy.it.util.ValidationUtil;

import java.time.LocalDate;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        // 通过构造UserInfoDTO的各种参数来进行验证
        UserInfoDTO userInfoDTO = new UserInfoDTO();
        userInfoDTO.setAge(18);
        userInfoDTO.setBirthday(LocalDate.now().minusYears(18));
        userInfoDTO.setEmail("123456789@qq.com");
        userInfoDTO.setId(1L);
        userInfoDTO.setPhone("18888888888");
        userInfoDTO.setName("法外狂徒张三");
        userInfoDTO.setPersonalPage("https://baidu.com");
        List<ValidErrorResult> list = ValidationUtil.valid(userInfoDTO);
        System.out.println(list);
    }
}
约束与校验类的绑定原理

XXValidator来校验XX约束注解,如:org.hibernate.validator.internal.constraintvalidators.hv.NotBlankValidator来校验@NotBlank注解。注解和注解的校验器绑定原理:org.hibernate.validator.internal.metadata.core.ConstraintHelper类中实现

		if ( enabledBuiltinConstraints.contains( JAKARTA_VALIDATION_CONSTRAINTS_NOT_BLANK ) ) {
			putBuiltinConstraint( tmpConstraints, NotBlank.class, NotBlankValidator.class );
		}

注意:一个注解约束,可能对应多个约束Validator。

自定义消息与消息模板

自定义消息,通过el表达式

    @NotNull(message = "年龄必填")
    @Min(value = 18, message = "年龄最小为{value}岁")
    @Max(value = 100, message = "年龄最大为{value}岁")
    private Integer age;

el表达式还没解析成值的时候就为消息模板

分组校验

示例代码升级:

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Null;
import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.URL;

import java.time.LocalDate;

@Data
public class UserInfoDTO {
    /**
     * 默认的组:jakarta.validation.groups.Default
     */
    @Null(groups = {AddUser.class}, message = "新增用户id必须为空") // 只适用于新增场景
    @NotNull(groups = {UpdateUser.class}, message = "修改用户id必须不为空") //只适用于修改场景
    private Long id;
    @NotBlank(message = "姓名必填")
    private String name;
    @NotNull(message = "年龄必填")
    @Min(value = 1, message = "年龄最小为{value}岁")
    @Max(value = 200, message = "年龄最大为{value}岁")
    /**
     * 与使用 @Range(min = 1, max = 200, message = "年龄范围在1岁至200岁之间")同样效果
     */
    private Integer age;
    @NotBlank(message = "邮箱必填")
    @Email(message = "必须为邮箱格式")
    private String email;
    @NotBlank(message = "手机号码必填")
    @Length(min = 11, max = 11, message = "手机号码必须为11位数字")
    @Pattern(regexp = "^1(3|4|5|7|8|9)\\d{9}$", message = "手机号码必须符合规范")
    private String phone;
    @NotNull(message = "出生日期必填")
    @Past(message = "出生日期不能超过当前时间")
    private LocalDate birthday;
    @NotBlank(message = "个人主页地址必填")
    @URL(message = "个人主页必须合法")
    private String personalPage;

    // 标记接口,新增组
    public interface AddUser {

    }

    // 标记接口,修改组
    public interface UpdateUser {

    }
}
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class ValidationUtil {
    // 线程安全的校验对象
    private static Validator validator;

    static {
        // 通过静态初始化块初始化validator
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    // 校验方法,新增groups参数
    public static List<ValidErrorResult> valid(Object object, Class<?>... groups) {
        /**
         * 通过validate方法校验,返回校验不满足的结果
         * 如果被校验对象没有校验通过,则set里面就有校验信息
         */
        Set<ConstraintViolation<Object>> set = validator.validate(object, groups);

        List<ValidErrorResult> validErrorResults = set.stream().map((mapper) -> {
            ValidErrorResult result = new ValidErrorResult();
            result.setField(mapper.getPropertyPath().toString());
            result.setValue(mapper.getInvalidValue());
            result.setMessage(mapper.getMessage());
            return result;
        }).collect(Collectors.toList());
        return validErrorResults;
    }
}
import com.lwy.it.dto.UserInfoDTO;
import com.lwy.it.util.ValidErrorResult;
import com.lwy.it.util.ValidationUtil;
import jakarta.validation.groups.Default;

import java.time.LocalDate;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        // 通过构造UserInfoDTO的各种参数来进行验证
        UserInfoDTO userInfoDTO = new UserInfoDTO();
        userInfoDTO.setAge(18);
        userInfoDTO.setBirthday(LocalDate.now().minusYears(18));
        userInfoDTO.setEmail("123456789@qq.com");
        userInfoDTO.setId(1L);
        userInfoDTO.setPhone("18888888888");
        userInfoDTO.setName("法外狂徒张三");
        userInfoDTO.setPersonalPage("https://baidu.com");
        // 注意添加默认组
        List<ValidErrorResult> list = ValidationUtil.valid(userInfoDTO, UserInfoDTO.AddUser.class, Default.class);
        System.out.println(list);
    }
}
自定义注解
import com.lwy.it.validator.NationalityValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 定义一个国籍注解
 */
@Documented
@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = {NationalityValidator.class}) //说明当前注解要被谁来完成校验工作
public @interface Nationality {
    /**
     * @return the error message template
     */
    String message() default "国籍必须为:中国、韩国、美国";

    /**
     * @return the groups the constraint belongs to
     */
    Class<?>[] groups() default {};

    /**
     * @return the payload associated to the constraint
     */
    Class<? extends Payload>[] payload() default {};
}
import com.lwy.it.annotation.Nationality;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

/**
 * 该怎么绑定要校验的约束注解呢?
 */
public class NationalityValidator implements ConstraintValidator<Nationality, String> {
    @Override
    public void initialize(Nationality constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 值为null时不进行校验
        if (Objects.isNull(value)) {
            return true;
        }
        Set<String> set = new HashSet<String>(3) {{
            add("中国");
            add("韩国");
            add("美国");
        }};
        if (set.contains(value)) {
            return true;
        }
        return false;
    }
}

增加国籍字段校验逻辑:

@NotBlank
// 国籍字段,假设必须为中国、韩国、美国之一
@Nationality(message = "国籍不正确")
private String nationality;
快速失败
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import org.hibernate.validator.HibernateValidator;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class ValidationUtil {
    // 线程安全的校验对象
    private static Validator validator;

    // 快速失败的
    private static Validator failFastValidator;

    static {
        // 通过静态初始化块初始化validator
        validator = Validation.buildDefaultValidatorFactory().getValidator();
        failFastValidator = Validation.byProvider(HibernateValidator.class)
                .configure()
                // 配置快速失败
                .failFast(true)
                .buildValidatorFactory()
                .getValidator();
    }

    /**
     * 快速失败的
     */
    public static List<ValidErrorResult> validFailFase(Object object, Class<?>... groups) {
        // 如果被校验对象没有校验通过,则set里面就有校验信息
        Set<ConstraintViolation<Object>> set = failFastValidator.validate(object, groups);
        List<ValidErrorResult> validErrorResults = set.stream().map((mapper) -> {
            ValidErrorResult result = new ValidErrorResult();
            result.setField(mapper.getPropertyPath().toString());
            result.setValue(mapper.getInvalidValue());
            result.setMessage(mapper.getMessage());
            return result;
        }).collect(Collectors.toList());
        return validErrorResults;
    }

    // 校验方法
    public static List<ValidErrorResult> valid(Object object, Class<?>... groups) {
        /**
         * 通过validate方法校验,返回校验不满足的结果
         * 如果被校验对象没有校验通过,则set里面就有校验信息
         */
        Set<ConstraintViolation<Object>> set = validator.validate(object, groups);

        List<ValidErrorResult> validErrorResults = set.stream().map((mapper) -> {
            ValidErrorResult result = new ValidErrorResult();
            result.setField(mapper.getPropertyPath().toString());
            result.setValue(mapper.getInvalidValue());
            result.setMessage(mapper.getMessage());
            return result;
        }).collect(Collectors.toList());
        return validErrorResults;
    }
}

调用方法:

import com.lwy.it.dto.UserInfoDTO;
import com.lwy.it.util.ValidErrorResult;
import com.lwy.it.util.ValidationUtil;
import jakarta.validation.groups.Default;

import java.time.LocalDate;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        // 通过构造UserInfoDTO的各种参数来进行验证
        UserInfoDTO userInfoDTO = new UserInfoDTO();
        userInfoDTO.setId(1L);
        userInfoDTO.setAge(18);
        userInfoDTO.setBirthday(LocalDate.now().minusYears(18));
        userInfoDTO.setEmail("123456789@qq.com");
        userInfoDTO.setPhone("18888888888");
        userInfoDTO.setName("法外狂徒张三");
        userInfoDTO.setPersonalPage("https://baidu.com");
        userInfoDTO.setNationality("日本");
        // 注意添加默认组
        List<ValidErrorResult> list = ValidationUtil.validFailFase(userInfoDTO, UserInfoDTO.AddUser.class,
                Default.class);
        System.out.println(list);
    }
}
非Bean校验
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.executable.ExecutableValidator;
import org.hibernate.validator.HibernateValidator;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class ValidationUtil {
    // 线程安全的校验对象
    private static Validator validator;

    // 快速失败的
    private static Validator failFastValidator;

    // 非Bean的
    private static ExecutableValidator executableValidator;

    static {
        // 通过静态初始化块初始化validator
        validator = Validation.buildDefaultValidatorFactory().getValidator();
        failFastValidator = Validation.byProvider(HibernateValidator.class)
                .configure()
                // 配置快速失败
                .failFast(true)
                .buildValidatorFactory()
                .getValidator();
        // 校验入参或返回值的
        executableValidator = validator.forExecutables();
    }

    /**
     * executableValidator
     * validateParameters():验证放置在给定方法的参数上的所有约束。
     * validateReturnValue():验证给定方法的所有返回值约束。
     * validateConstructorParameters():验证放置在给定构造函数参数上的所有约束。
     * validateConstructorReturnValue():验证给定构造函数的所有返回值约束。
     */
    public static <T> List<ValidErrorResult> validNotBean(T object,
                                                          Method method,
                                                          Object[] parameterValues,
                                                          Class<?>... groups) {
        Set<ConstraintViolation<T>> set = executableValidator.validateParameters(object, method,
                parameterValues, groups);
        List<ValidErrorResult> validErrorResults = set.stream().map((mapper) -> {
            ValidErrorResult result = new ValidErrorResult();
            result.setField(mapper.getPropertyPath().toString());
            result.setValue(mapper.getInvalidValue());
            result.setMessage(mapper.getMessage());
            return result;
        }).collect(Collectors.toList());
        return validErrorResults;
    }

    /**
     * 快速失败的
     *
     * @param object
     * @param groups
     * @return
     */
    public static List<ValidErrorResult> validFailFase(Object object, Class<?>... groups) {
        // 如果被校验对象没有校验通过,则set里面就有校验信息
        Set<ConstraintViolation<Object>> set = failFastValidator.validate(object, groups);
        List<ValidErrorResult> validErrorResults = set.stream().map((mapper) -> {
            ValidErrorResult result = new ValidErrorResult();
            result.setField(mapper.getPropertyPath().toString());
            result.setValue(mapper.getInvalidValue());
            result.setMessage(mapper.getMessage());
            return result;
        }).collect(Collectors.toList());
        return validErrorResults;
    }

    // 校验方法
    public static List<ValidErrorResult> valid(Object object, Class<?>... groups) {
        /**
         * 通过validate方法校验,返回校验不满足的结果
         * 如果被校验对象没有校验通过,则set里面就有校验信息
         */
        Set<ConstraintViolation<Object>> set = validator.validate(object, groups);

        List<ValidErrorResult> validErrorResults = set.stream().map((mapper) -> {
            ValidErrorResult result = new ValidErrorResult();
            result.setField(mapper.getPropertyPath().toString());
            result.setValue(mapper.getInvalidValue());
            result.setMessage(mapper.getMessage());
            return result;
        }).collect(Collectors.toList());
        return validErrorResults;
    }
}
import com.lwy.it.util.ValidErrorResult;
import com.lwy.it.util.ValidationUtil;
import jakarta.validation.constraints.NotBlank;

import java.lang.reflect.Method;
import java.util.List;

public class UserInfoService {
    /**
     * 方法非bean类型的入参校验
     * 1.方法参数前加注解
     * 2.执行入参校验
     */
    public String getUserByName(@NotBlank(message = "姓名参数不能为空") String name) {
      // 也可以通过反射方式获取
        StackTraceElement stackTraceElement = Thread.currentThread().getStackTrace()[1];
        String methodName = stackTraceElement.getMethodName();
        Method method = null;
        try {
            method = this.getClass().getDeclaredMethod(methodName, String.class);
        } catch (Exception exception) {
            exception.printStackTrace();
        }
        List<ValidErrorResult> validErrorResults = ValidationUtil.validNotBean(this, method, new Object[]{name});
        System.out.println(validErrorResults);
        return "SUCCESS";
    }
}
public static void main(String[] args) {
    UserInfoService userInfoService = new UserInfoService();
    userInfoService.getUserByName("");
}

使用时可以看到非常的繁琐,可以结合AOP的方式进行使用。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值