Spring Validation参数效验的各种使用姿势总结

目录
  • 前言
  • 基本概念
  • @Valid和@Validated区别
  • 基本使用
    • 加入依赖
    • 对象参数使用
      • 使用 @RequestBody
      • 不使用 @RequestBody
    • 基本类型使用
    • 测试
      • save方法测试
      • save2
      • get方法测试
    • 全局异常处理
      • save方法测试
      • save2方法测试
      • get方法测试
    • 其余类型
  • 使用分组效验
    • 定义 ZfbPayGroup
    • 添加group
    • 使用 group
    • 测试
  • 嵌套校验
  • 集合校验
    • 方式一
      • 测试
    • 方式二
      • 测试
  • 自定义校验规则
  • 参考博文
  • 总结

前言

在日常的项目开发中,为了防止非法参数对业务造成的影响,需要对接口的参数做合法性校验,例如在创建用户时,需要效验用户的账号名称不能输入中文与特殊字符,手机号、邮箱格式是否准确。按照原始的处理逻辑需要对每个接口中的参数进行 if/else 处理,如果这样开发,后期代码难以维护,可读性极差。

为了解决上述问题,validation框架诞生了,代码量大大减少,参数的效验不再穿插业务逻辑代码中,代码美观又易于维护。

基本概念

@Valid 是 JSR303 声明的,JSR是Java Specification Requests的缩写,其中 JSR303 是JAVA EE 6 中的一项子规范,叫做 Bean Validation,为 JavaBean 验证定义了相应的元数据模型和 API,需要注意的是,JSR 只是一项标准,它规定了一些校验注解的规范,但没有实现,而 Hibernate validation 对其进行实现。

Spring Validation 验证框架对参数的验证机制提供了@Validated(Spring JSR-303规范,是标准JSR-303的一个变种)。

@Valid和@Validated区别

区别 @Valid @Validated
来源 JSR-303规范 Spring
是否支持分组 不支持 支持
标注位置 METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE TYPE,METHOD,PARAMETER
嵌套校验 支持 不支持

基本使用

加入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.3.12.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.10</version>
</dependency>

注:从 boot-2.3.x开始,spring-boot-starter-web不再引入 spring-boot-starter-validation,所以需要额外手动引入validation依赖,而 2.3之前的版本只需要引入 web 依赖。

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

<!-- <dependency>
	<groupId>org.hibernate.validator</groupId>
	<artifactId>hibernate-validator</artifactId>
	<version>6.0.18.Final</version>
	<scope>compile</scope>
</dependency>-->

以上两个依赖都是可以实现功能的。hibernate-validator、spring-boot-starter-validation底层都引入了 jakarta.validation-api依赖。

在实际开发的过程中,请求参数的格式一般有如下几种情况:

对象参数使用

使用对象参数接收分为两种,一种是使用 @RequestBody注解的application/json提交,还有一种不使用 @RequestBody注解的 form-data提交。

  • 使用对象接收参数,在需要校验对象的参数加上 @NotBlank注解,message是校验不通过的提示信息。
@Data
public class UserReq {
    @NotBlank(message = "name为必传参数")
    private String name;
    @NotBlank(message = "email为必传参数")
    private String email;
}

使用 @RequestBody

  • Api,在需要校验的对象前面加 @RequestBody注解以及@Validated或者@Valid注解,如果校验失败,会抛出MethodArgumentNotValidException异常。
@RestController
public class GetHeaderController {
    @PostMapping("save")
    public void save(@RequestBody @Validated UserReq req){}
}

不使用 @RequestBody

只需要校验的对象前面加@Validated注解或者@Valid注解,如果校验失败,会抛出BindException异常。

@PostMapping("save2")
public void save2(@Validated UserReq req){
}

基本类型使用

  • 其实也就是路径传参,在参数前面加上相对应的校验注解,还必须在Controller类上加 @Validated注解。如果校验失败,会抛出ConstraintViolationException异常。
@RestController
@Validated
public class GetHeaderController {
    @PostMapping("get")
    public void get(@NotBlank(message = "名称 is required") String name,@NotBlank(message = "邮箱 is required") String email) throws JsonProcessingException {

    }
}

测试

save方法测试

save2

get方法测试

全局异常处理

通过前面的测试,我们知道如果参数校验失败,三种使用场景会抛出三种异常或者警告,分别是MethodArgumentNotValidException、ConstraintViolationException、BindException异常,每种异常的响应格式又不一致。所以在项目开发中,通常会使用统一异常处理来返回一个统一格式并友好的提示。

@RestControllerAdvice
public class GlobalExceptionHandler {
    /**
     * @RequestBody 上校验失败后抛出的异常是 MethodArgumentNotValidException 异常。
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();
        String messages = bindingResult.getAllErrors()
                .stream()
                .map(ObjectError::getDefaultMessage)
                .collect(Collectors.joining(";"));
        return messages;
    }
    /**
     * 不加 @RequestBody注解,校验失败抛出的则是 BindException
     */
    @ExceptionHandler(value = BindException.class)
    public String exceptionHandler(BindException e){
        String messages = e.getBindingResult().getAllErrors()
                .stream()
                .map(ObjectError::getDefaultMessage)
                .collect(Collectors.joining(";"));
        return messages;
    }

    /**
     *  @RequestParam 上校验失败后抛出的异常是 ConstraintViolationException
     */
    @ExceptionHandler({ConstraintViolationException.class})
    public String methodArgumentNotValid(ConstraintViolationException exception) {
        String message = exception.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";"));
        return message;
    }
}

save方法测试

save2方法测试

get方法测试

可以发现它是将类中所有的属性进行效验完成之后,才抛出异常的,但其实这有点消耗性能,那能不能只要检测到一个效验不通过的,就抛出异常呢?只需要在容器提供如下代码:

@Configuration
public class ParamValidatorConfig {
    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                //failFast:只要出现校验失败的情况,就立即结束校验,不再进行后续的校验。
                .failFast(true)
                .buildValidatorFactory();
        return validatorFactory.getValidator();
    }

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
        methodValidationPostProcessor.setValidator(validator());
        return methodValidationPostProcessor;
    }

}

MethodValidationPostProcessor是Spring提供的来实现基于方法Method的JSR校验的核心处理器,最终会由 MethodValidationInterceptor进行校验拦截。

  • 测试如下:

其余类型

上面举例使用了NotBlank注解,但肯定不只一个!我们进入到 @NotBlank注解所在的包路径。

哦豁,这么多呀!小杰一个一个来介绍一下作用。

注解 备注 适用类型 示例
@AssertFalse 被注释的元素必须为 false,null 值是有效的。 boolean 和 Boolean @AssertFalse(message = "该参数必须为 false")
@AssertTrue 被注释的元素必须为 true,null 值是有效的。 boolean 和 Boolean @AssertTrue(message = "该参数必须为 true")
@DecimalMax 被注释的元素必须是一个数字,其值必须小于或等于指定的最大值,null 值是有效的。 BigDecimal、BigInteger、CharSequence、byte、short、int、long以及包装类型 @DecimalMax(value = "100",message = "该参数不能大于 100")
@DecimalMin 被注释的元素必须是一个数字,其值必须大于或等于指定的最小值,null 值是有效的。 BigDecimal、BigInteger、CharSequence、byte、short、int、long以及包装类型 @DecimalMax(value = "0",message = "该参数不能小于 0")
@Digits 被注释的元素必须是可接受范围内的数字,null 值是有效的。 BigDecimal、BigInteger、CharSequence、byte、short、int、long以及包装类型 @Digits(integer = 3,fraction = 2,message = "该参数整数位数不能超出3位,小数位数不能超过2位")
@Max 被注释的元素必须是一个数字,其值必须小于或等于指定的最大值,null 值是有效 BigDecimal、BigInteger、byte、short、int、long以及包装类型 @Max(value = 200,message = "最大金额不能超过 200")
@Min 被注释的元素必须是一个数字,其值必须大于或等于指定的最小值,null 值是有效的。 BigDecimal、BigInteger、byte、short、int、long以及包装类型 @Min(value = 0,message = "最小金额不能小于 0")
@Negative 被注释的元素必须是负数,null 值是有效 BigDecimal、BigInteger、byte、short、int、long、float、double 以及包装类型 @Negative(message = "必须是负数")
@NegativeOrZero 被注释的元素必须是负数或 0,null 值是有效的。 BigDecimal、BigInteger、byte、short、int、long、float、double 以及包装类型 @NegativeOrZero(message = "必须是负数或者为0")
@Positive 被注释的元素必须是正数,null 值是有效的。 BigDecimal、BigInteger、byte、short、int、long、float、double 以及包装类型 @Positive(message = "必须是正数")
@PositiveOrZero 被注释的元素必须是正数或0,null 值是有效的。 BigDecimal、BigInteger、byte、short、int、long、float、double 以及包装类型 @PositiveOrZero(message = "必须是正数或者为0")
@Future 被注释的元素必须是未来的日期(年月日),null 值是有效的。 基本所有的时间类型都支持。常用的:Date、LocalDate、LocalDateTime、LocalTime、Instant @Future(message = "预约日期要大于当前日期")
@FutureOrPresent 被注释的元素必须是现在或者未来的日期(年月日),null 值是有效的。 基本所有的时间类型都支持。常用的:Date、LocalDate、LocalDateTime、LocalTime、Instant @FutureOrPresent(message = "预约日要大于当前日期")
@Past 被注释的元素必须是过去的日期,null 值是有效的。 基本所有的时间类型都支持。常用的:Date、LocalDate、LocalDateTime、LocalTime、Instant @Past(message = "出生日期要小于当前日期")
@PastOrPresent 被注释的元素必须是过去或者现在的日期,null 值是有效的。 基本所有的时间类型都支持。常用的:Date、LocalDate、LocalDateTime、LocalTime、Instant @PastOrPresent(message = "出生时间要小于当前时间")
@NotBlank 被注释的元素不能为空,并且必须至少包含一个非空白字符 CharSequence @NotBlank(message = "name为必传参数")
@NotEmpty 被注释的元素不能为 null 也不能为空 CharSequence、Collection、Map、Array @NotEmpty(message = "不能为null或者为空")
@NotNull 被注释的元素不能为null 任意类型 @NotNull(message = "不能为null")
@Null 被注释的元素必须为null 任意类型 @Null(message = "必须为null")
@Email 被注释的元素必须是格式正确的电子邮件地址,null 值是有效的。 CharSequence @Email(message = "email格式错误,请重新填写")
@Pattern 被注释的元素必须匹配指定的正则表达式,null 值是有效的。 CharSequence @Pattern(regexp = "^1[3456789]\d{9}$",message = "手机号格式不正确")
@Size 被注释的元素大小必须在指定范围内,null 值是有效的。 CharSequence、Collection、Map、Array @Size(min = 5,max = 20,message = "字符长度在 5 -20 之间")

以上注解有几个需要注意一下,因为经常用到,也经常使用错误

  • @NotNull:适用于任何类型,不能为null,但可以是 (""," ")
  • @NotBlank:只能用于 String,不能为null,而且调用 trim() 后,长度必须大于0,必须要有实际字符。
  • @NotEmpty:用于 String、Collection、Map、Array,不能为null,长度必须大于0。

使用分组效验

有些小伙伴说使用 @Validated校验的对象不能复用,这我只能说学的还不够深入。

小杰使用 PayReq来举例,该对象是一个公用的请求体,对接了微信、支付宝两个渠道方,对接微信 payName参数是非必传的,对接支付宝是必传参数。payAmount是两个渠道必传参数。其实就跟平常写新增方法、修改方法一样的,用的是同一个 ReqDTO,但是其中 id 字段新增是不用传递的,而修改时是必传的。

定义 ZfbPayGroup

定义ZfbPayGroup的分组接口,继承 Default接口。

public interface ZfbPayGroup extends Default {
}

添加group

在需要区分组的字段上加 groups 参数。在本例中在 payName加了groups 参数,值为 ZfbPayGroup.class,代表对组为 ZfbPayGroup的进行payName参数校验。

@Data
public class PayReq {
    @NotBlank(message = "支付名称不能为空",groups = {ZfbPayGroup.class})
    private String payName;

    @NotNull(message = "支付金额不能为空")
    private BigDecimal payAmount;

}

注意:ZfbPayGroup 要继承 Default接口,不然 payAmount字段的效验会对ZfbPayGroup这个组失效,payAmount默认的组为 Default。

使用 group

创建两个接口,在 zfbPaySave接口中声明@Validated校验组,wxbPaySave接口正常编写。

@PostMapping("zfbPaySave")
public void zfbPaySave(@RequestBody @Validated(value = {ZfbPayGroup.class}) PayReq req) throws JsonProcessingException {
    ObjectMapper mapper = new ObjectMapper();

    System.out.println( mapper.writeValueAsString(req));
}
@PostMapping("wxbPaySave")
public void wxbPaySave(@RequestBody @Validated PayReq req) throws JsonProcessingException {
    ObjectMapper mapper = new ObjectMapper();

    System.out.println( mapper.writeValueAsString(req));
}

测试

  • zfbPaySave:提示支付名称不能为空

  • wxbPaySave:没被校验拦截。

嵌套校验

什么是嵌套使用呢?就是一个对象中包含另外一个对象,另外一个对象的字段也是需要进行校验。示例如下:

  • UserReq
@Data
public class UserReq {
    @NotBlank(message = "name为必传参数")
    private String name;

    private String email;

    @NotNull(message = "proReq对象不能为空")
    @Valid
    private ProReq proReq;
}

嵌套校验需要在效验的对象加上 @Valid 注解

  • ProReq
@Data
public class ProReq {
    @NotBlank(message = "proName为必传参数")
    private String proName;
}
  • 测试

集合校验

在某些场景下,我们需要使用集合接收前端传递的参数,并对集合中的每个对象都进行参数校验。但是这时我们的参数校验并不会生效!如下写法:

@PostMapping("save3")
public String save3(@RequestBody @Validated List<UserReq> req){
    return "成功";
}

下面介绍两种方式对集合进行效验!

方式一

@Validated + @Valid两个注解同时使用!缺点:不能使用分组效验!如果该实体不需要用到分组功能,可以使用该方式!

@RestController
@Validated
public class GetHeaderController {
    @PostMapping("save3")
    public String save3(@RequestBody @Valid @NotEmpty(message = "该集合不能为空") List<UserReq> req){
        return "成功";
    }
}

测试

方式二

  • 自定义一个List
@Data
public class ValidList<E> implements List<E> {
    // 使用该注解就不需要手动重新 List 中的方法了
    @Delegate
    @Valid
    public List<E> list = new ArrayList<>();

}
  • @Delegate,为 lombok 的注解,表示该属性的所有对象的实例方法都将被该类代理。
  • 编码如下:
@PostMapping("save4")
public String save4(@RequestBody @Validated @NotEmpty(message = "该集合不能为空") ValidList<UserReq> req){
    return "成功";
}

测试

自定义校验规则

自定义校验规则小杰在工作当中用的比较少。大部分业务需求使用自带的注解已经够平常开发了。当然自定义validation规则也非常简单。这里使用校验电话号码是否合法来举例!别抬杠,说我为什么不用 @Pattern(regexp = "^1[3456789]\\d{9}$",message = "手机号格式不正确")直接实现。对不起,我不想每次写正则。

  • 自定义注解 Phone,跟着内置的注解照葫芦画瓢。不过 validatedBy的值要指定我们自定义的约束验证器
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { PhoneValidator.class })
public @interface Phone {

    String message() default "手机号码格式异常";

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

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

}
  • 实现ConstraintValidator约束验证器接口
public class PhoneValidator implements ConstraintValidator<Phone,String> {
    private static final String REGEX = "^1[3456789]\\\\d{9}$";
    /**
     *
     * @param value
     * @param context
     * @return:返回 true 表示效验通过
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 不为null才进行校验
        if (value != null) {
            return value.matches(REGEX);
        }
        return true;
    }
}
  • 接下来就可以使用 @Phone注解了。
@Data
public class UserReq {
    @NotBlank(message = "name为必传参数")
    private String name;

    @Email(message = "email格式错误,请重新填写")
    @NotBlank(message = "email为必传参数")
    private String email;

    @NotNull(message = "proReq对象不能为空")
    @Valid
    private ProReq proReq;

    @Phone
    @NotBlank(message = "手机号码为必传参数")
    private String tel;
}
  • 测试

参考博文

  • @Validated和@Valid区别

总结

到此这篇关于Spring Validation参数效验的各种使用姿势总结的文章就介绍到这了,更多相关Spring Validation参数效验内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 如何使用Spring Validation优雅地校验参数

    引言 不知道大家平时的业务开发过程中 controller 层的参数校验都是怎么写的?是否也存在下面这样的直接判断? public String add(UserVO userVO) { if(userVO.getAge() == null){ return "年龄不能为空"; } if(userVO.getAge() > 120){ return "年龄不能超过120"; } if(userVO.getName().isEmpty()){ return &q

  • SpringBoot集成validation校验参数遇到的坑

    公众号中分享了一篇文章,关于SpringBoot集成validation校验参数的,粉丝留言说有坑. 原留言如下: 有坑,你试试^A-\\d{12}-\\d{4}$,这条正则经过validate这个方法无论参数写的对不对都会报验证错误,而用main方法测试是正常的.. 话说,针对这种回复我是不太信了,直觉告诉我,肯定是这位粉丝用错了.但既然粉丝有疑问还是需要专门写一个demo来验证一下的.说写就写. SpringBoot集成validation 集成过程非常简单,在原项目的pom文件中直接引入如

  • spring boot validation参数校验实例分析

    本文实例讲述了spring boot validation参数校验.分享给大家供大家参考,具体如下: 对于任何一个应用而言在客户端做的数据有效性验证都不是安全有效的,这时候就要求我们在开发的时候在服务端也对数据的有效性进行验证. Spring Boot自身对数据在服务端的校验有一个比较好的支持,它能将我们提交到服务端的数据按照我们事先的约定进行数据有效性验证. 1 pom依赖 <dependency> <groupId>org.springframework.boot</gr

  • java validation 后台参数验证的使用详解

    一.前言 在后台开发过程中,对参数的校验成为开发环境不可缺少的一个环节.比如参数不能为null,email那么必须符合email的格式,如果手动进行if判断或者写正则表达式判断无意开发效率太慢,在时间.成本.质量的博弈中必然会落后.所以把校验层抽象出来是必然的结果,下面说下几种解决方案. 二.几种解决方案 1.struts2的valid可以通过配置xml,xml中描述规则和返回的信息,这种方式比较麻烦.开发效率低,不推荐 2.validation bean 是基于JSR-303标准开发出来的,使

  • Spring Validation参数效验的各种使用姿势总结

    目录 前言 基本概念 @Valid和@Validated区别 基本使用 加入依赖 对象参数使用 使用 @RequestBody 不使用 @RequestBody 基本类型使用 测试 save方法测试 save2 get方法测试 全局异常处理 save方法测试 save2方法测试 get方法测试 其余类型 使用分组效验 定义 ZfbPayGroup 添加group 使用 group 测试 嵌套校验 集合校验 方式一 测试 方式二 测试 自定义校验规则 参考博文 总结 前言 在日常的项目开发中,为了

  • Spring请求参数校验功能实例演示

    SpringMVC支持的数据校验是JSR303的标准,通过在bean的属性上打上@NotNull.@Max等进行验证.JSR303提供有很多annotation接口,而SpringMVC对于这些验证是使用hibernate的实现,所以我们需要添加hibernate的一个validator包: 依赖引用 compile 'javax.validation:validation-api:2.0.0.Final' compile 'org.hibernate:hibernate-validator:6

  • 详解使用spring validation完成数据后端校验

    前言 数据的校验是交互式网站一个不可或缺的功能,前端的js校验可以涵盖大部分的校验职责,如用户名唯一性,生日格式,邮箱格式校验等等常用的校验.但是为了避免用户绕过浏览器,使用http工具直接向后端请求一些违法数据,服务端的数据校验也是必要的,可以防止脏数据落到数据库中,如果数据库中出现一个非法的邮箱格式,也会让运维人员头疼不已.我在之前保险产品研发过程中,系统对数据校验要求比较严格且追求可变性及效率,曾使用drools作为规则引擎,兼任了校验的功能.而在一般的应用,可以使用本文将要介绍的vali

  • Spring Validation方法实现原理分析

    最近要做动态数据的提交处理,即需要分析提交数据字段定义信息后才能明确对应的具体字段类型,进而做数据类型转换和字段有效性校验,然后做业务处理后提交数据库,自己开发一套校验逻辑的话周期太长,因此分析了Spring Validation的实现原理,复用了其底层花样繁多的Validator,在此将分析Spring Validation原理的过程记录下,不深入细节 如何使用Spring Validation Spring Bean初始化时校验Bean是否符合JSR-303规范 1.手动添加BeanVali

  • Spring Boot参数校验及分组校验的使用教程

    目录 一  前言 1  什么是validator 二  注解介绍 1  validator内置注解 三  使用 1  单参数校验 2  对象参数校验 3  错误消息的捕获 总结 一  前言 做web开发有一点很烦人就是要对前端输入参数进行校验,基本上每个接口都要对参数进行校验,比如一些非空校验.格式校验等.如果参数比较少的话还是容易处理的一但参数比较多了的话代码中就会出现大量的if-else语句. 使用这种方式虽然简单直接,但是也有不好的地方,一是降低了开发效率,因为我们需要校验的参数会存在很多

  • spring validation多层对象校验教程

    目录 spring validation多层对象校验 1.第一层对象定义 2.第二层对象 3.Controller层校验使用 validation校验对象多个字段返回的消息内容顺序随机问题 问题描述 解决办法 spring validation多层对象校验 1.第一层对象定义 import java.io.Serializable; import javax.validation.Valid; /** * 请求参数 * @Title: ReqIn.java * @Package com.spri

  • Spring Validation实现数据校验的示例

    目录 一.什么是 Spring Validation 二.实现数据校验 准备相关jar包 Validator接口方式 基于注解方式(Bean Validation) 基于方法的方式 自定义校验 一.什么是 Spring Validation 在开发中,我们经常遇到参数校验的需求,比如用户注册的时候,要校验用户名不能为空.用户名长度不超过20个字符.手机号是合法的手机号格式等等.如果使用普通方式,我们会把校验的代码和真正的业务处理逻辑耦合在一起,而且如果未来要新增一种校验逻辑也需要在修改多个地方.

  • Spring自定义参数解析器代码实例

    这篇文章主要介绍了Spring自定义参数解析器代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 结合redis编写User自定义参数解析器UserArgumentResolver import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation

随机推荐