SpringBoot Validation小结

Validation

官网:Jakarta Bean Validation specification

基础介绍

@Validation是一套帮助我们继续对传输的参数进行数据校验的注解,通过配置Validation可以很轻松的完成对数据的约束。

@Validated作用在类、方法和参数上

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
    Class<?>[] value() default {};
}

使用

maven依赖:

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

gradle依赖:

implementation("org.springframework.boot:spring-boot-starter-validation")

常用注解

image-20220619113008286

异常处理

package spring.validation.advice;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import spring.validation.entity.Result;

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

/**
 * @author liuyang
 * 创建时间: 2022-05-23 9:04
 */
@RestControllerAdvice
public class GlobalExceptionAdvice {

    private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionAdvice.class);

    @ExceptionHandler(value = Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result handleRuleException(Exception e) {
        LOGGER.error(e.getMessage(), e);
        Result result = Result.failure("999999", e.toString());
        return result;
    }

    /**
     * spring 封装的参数验证异常,处理 json 请求体调用接口校验失败抛出的异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result handle(MethodArgumentNotValidException e) {
        LOGGER.error(e.getMessage(), e);
        //获取参数校验错误集合
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        //格式化以提供友好的错误提示
        String data = String.format("参数校验错误(%s):%s", fieldErrors.size(),
                fieldErrors.stream()
                        .map(FieldError::getDefaultMessage)
                        .collect(Collectors.joining(";")));
        //参数校验失败响应失败个数及原因
        return Result.failure("888888", "校验失败", data);
    }

    /**
     * 请求参数校验(PathVariables、RequestParameters、RequestHeader等)校验失败
     */
    @ExceptionHandler(ServletRequestBindingException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result constraintViolationExceptionHandler(ServletRequestBindingException e) {
        LOGGER.error(e.getMessage(), e);
        return Result.failure("888887", "校验失败," + e.getMessage());
    }
}

@Valid和@Validated 区别

通过源码分析:

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Valid {
}
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
    Class<?>[] value() default {};
}

@Valid:没有分组的功能。

@Valid:可以用在方法、构造函数、方法参数和**成员属性(字段)**上

@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制

@Validated:可以用在类型、方法和方法参数上。但是**不能用在成员属性(字段)**上

两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能,如果需要嵌套验证,则字段上必须增加 @Valid 注解

嵌套验证

比如我们现在有个实体叫做Item:

public class Item {
    @NotNull(message = "id不能为空")
    @Min(value = 1, message = "id必须为正整数")
    private Long id;

    @NotNull(message = "props不能为空")
    @Size(min = 1, message = "至少要有一个属性")
    private List<Property> properties;
}

Item带有很多属性,属性里面有属性id,属性值id,属性名和属性值,如下所示:

public class Property {

    @NotNull(message = "pid不能为空")
    @Min(value = 1, message = "pid必须为正整数")
    private Long pid;

    @NotNull(message = "name不能为空")
    private String name;

}

属性这个实体也有自己的验证机制,比如属性和属性值id不能为空,属性名和属性值不能为空等。

现在我们有个ItemController接受一个Item的入参,想要对Item进行验证,如下所示:

@RestController
public class ItemController {

    @RequestMapping("/item/add")
    public void addItem(@Validated Item item) {
        //doSomething();
    }
}

在上图中,如果Item实体的props属性不额外加注释,只有@NotNull和@Size,无论入参采用@Validated还是@Valid验证,Spring Validation框架只会对Item的id和props做非空和数量验证,不会对props字段里的Prop实体进行字段验证,也就是**@Validated@Valid**加在方法参数前,都不会自动对参数进行嵌套验证。也就是说如果传的List中有Prop的pid为空或者是负数,入参验证不会检测出来。

为了能够进行嵌套验证,必须手动在Item实体的props字段上明确指出这个字段里面的实体也要进行验证。由于@Validated不能用在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且@Valid类注解上也说明了它支持嵌套验证功能,那么我们能够推断出:@Valid加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证。

我们修改Item类如下所示:

public class Item {
    @NotNull(message = "id不能为空")
    @Min(value = 1, message = "id必须为正整数")
    private Long id;

    /**
     * 嵌套验证必须用@Valid
     */
    @Valid
    @NotNull(message = "properties不能为空")
    @Size(min = 1, message = "至少要有一个属性")
    private List<Property> properties;
}

然后我们在ItemController的addItem函数上再使用@Validated或者@Valid,就能对Item的入参进行嵌套验证。此时Item里面的props如果含有Prop的相应字段为空的情况,Spring Validation框架就会检测出来。

分组校验

  1. 定义一个分组类(或接口),如果需要使用默认分组的注解(如@Email等),则可继承Default 接口,建议新建分组加上 extend javax.validation.groups.Default
  2. 在校验注解上添加 groups 属性指定分组
  3. @Validated 注解添加激活(或使用)的分组类
package javax.validation.groups;

/**
 * Default Jakarta Bean Validation group.
 * <p>
 * Unless a list of groups is explicitly defined:
 * <ul>
 *     <li>constraints belong to the {@code Default} group</li>
 *     <li>validation applies to the {@code Default} group</li>
 * </ul>
 * Most structural constraints should belong to the default group.
 *
 * @author Emmanuel Bernard
 */
public interface Default {
}

在编写Custom分组接口时,如果继承了 Default,下面两个写法就是等效的:

@Validated({Custom.class})
@Validated({Custom.class, Default.class})

自定义校验

  1. 自定义注解,并标记@Constraint来表示校验功能的实现类
  2. 自定义validator,继承 ConstraintValidator,实现isValid方法来判断校验是否成功

示例

校验两个变量至少存在一个(如父母亲手机号至少填一个)

接口定义:

    @PostMapping("/insertChildInfo")
    public ChildInfo insertChildInfo(@Validated @RequestBody ChildInfo info) {
        return info;
    }

自定义方法来判断

自定义被@AsserTrue标记的方法(private、public均可),内部自定义实现逻辑即可。

注: 被Asser修饰(标记)的方法一定要以is开头

package spring.validation.entity;

import lombok.Getter;
import lombok.Setter;
import org.springframework.util.StringUtils;

import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

/**
 * @author liuyang
 * 创建时间: 2022-06-19 17:09
 */
@Getter
@Setter
public class ChildInfo {

    @NotNull(message = "id不能为空")
    @Min(value = 1, message = "id必须为正整数")
    private int id;

    @NotNull(message = "name不能为空")
    private String name;

    private String fatherPhone;

    private String motherPhone;

    @AssertTrue(message = "method,父母亲手机号至少填写一个")
    private boolean isHasPhoneNum() {
        return !StringUtils.isEmpty(fatherPhone) || !StringUtils.isEmpty(motherPhone);
    }

}

image-20220619174206238

自定义脚本@ScriptAssert

可以通过@ScriptAssert自定义JavaScript脚本来实现,默认脚本上下文用_this表示

package spring.validation.entity;

import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.ScriptAssert;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

/**
 * @author liuyang
 * 创建时间: 2022-06-19 17:09
 */
@Getter
@Setter
//@ScriptAssert(lang = "javascript", script = "_this.fatherPhone != null || _this.motherPhone != null",
//        message = "javascript, fatherPhone 和 motherPhone 不能同时为空", reportOn = "fatherPhone")
//@ScriptAssert(lang = "javascript", alias = "_", script = "_.fatherPhone != null && _.motherPhone != null", message = "", reportOn = "fatherPhone")
@ScriptAssert(lang = "javascript", alias = "_", script = "!org.springframework.util.StringUtils.isEmpty(_.fatherPhone) || !org.springframework.util.StringUtils.isEmpty(_.motherPhone)",
        message = "javascript2, fatherPhone 和 motherPhone 不能同时为空", reportOn = "fatherPhone")
public class ChildInfo2 {

    @NotNull(message = "id不能为空")
    @Min(value = 1, message = "id必须为正整数")
    private int id;

    @NotNull(message = "name不能为空")
    private String name;

    private String fatherPhone;

    private String motherPhone;

}

使用 _this :

image-20220619174238868

使用StringUtils:

image-20220619175118063

自定义注解

通过自定义注解及校验方法实现

自定义注解OneOf
package spring.validation.annotations;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 至少存在一个(一个或多个都存在)
 *
 * @author liuyang
 * 创建时间: 2022-06-19 17:55
 */
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = OneOfValidator.class)
@Documented
public @interface OneOf {
    String message() default "{one.of.message}";

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

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

    String[] value();
}
自定义校验方法OneOfValidator
package spring.validation.annotations;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import util.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Optional;

/**
 * 自定义校验方法
 *
 * @author liuyang
 * 创建时间: 2022-06-19 17:56
 */
public class OneOfValidator implements ConstraintValidator<OneOf, Object> {
    private String[] fields;

    @Override
    public void initialize(OneOf annotation) {
        this.fields = annotation.value();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(value);
        int matches = countNumberOfMatches(wrapper);
        if (matches == 0) {
            setValidationErrorMessage(context);
            return false;
        }
        return true;
    }

    /**
     * 获取值不为空的field数量
     */
    private int countNumberOfMatches(BeanWrapper wrapper) {
        int matches = 0;
        for (String field : fields) {
            Object value = wrapper.getPropertyValue(field);
            boolean isPresent = detectOptionalValue(value);
            if (!StringUtils.isEmpty(value) && isPresent) {
                matches++;
            }
        }
        return matches;
    }

    private boolean detectOptionalValue(Object value) {
        if (value instanceof Optional) {
            return ((Optional) value).isPresent();
        }
        return true;
    }

    /**
     * 设置校验失败信息
     */
    private void setValidationErrorMessage(ConstraintValidatorContext context) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
                .addPropertyNode(fields[0]).addConstraintViolation();
    }
}
结果

image-20220619180119977

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值