Springboot 参数校验

Springboot 参数校验

Java API规范(JSR303)定义了Bean校验的标准 validation-api,但没有提供实现。hibernate validation 是对这个规范的实现,并增加了校验注解如 @Email@Length 等。

Spring Validation 是对 hibernate validation 的二次封装,用于支持 spring mvc 参数自动校验。

引入依赖

​ 如果spring-boot版本小于2.3.xspring-boot-starter-web会自动传入hibernate-validator依赖。如果spring-boot版本大于2.3.x,则需要手动引入依赖:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.1.Final</version>
</dependency>

requestBody 参数校验

POSTPUT请求一般会使用requestBody传递参数,这种情况下,只要给 DTO 对象加上 @Validated 注解就能实现自动参数校验。

​ 比如,有一个保存Brand的接口,要求 name 长度是不能超过30字符。

/**
 * 参数实体上声明约束
 */
@Data
public class BrandDto {

    private Long id;

    @NotBlank(message = "品牌名不能为空!")
    @Length(max = 30, message = "品牌名太长了!")
    private String name;

    private String chineseSpell;

    private String englishName;
}

/**
  * 在方法参数上使用@Valid声明校验注解(使用@Valid和@Validated都可以)
  */
@PostMapping("/brand")
public Result saveBrand(@RequestBody @Valid BrandDto brand){
    return brandService.saveBrand(brand);
}

​ 校验失败,会抛出 MethodArgumentNotValidException 异常,Spring默认会将其转为400(Bad Request)请求。

image-20211121214954493
image-20211121215025114

requestParam/PathVariable 参数校验

​ 如果是 GET 请求一般会使用 requestParam/PathVariable 传参,此时要在类上标注 @Validated 注解,并在入参上声明约束注解。

@RestController
@RequestMapping("/v1/web/brand")
@Validated
public class BrandController {
 	@Autowired
    private BrandService brandService;

 	/**
     * 查询品牌
     */
   @GetMapping("/brand")
    public Result getBrand(@RequestParam("id")
                               @NotNull(message = "id不能为空!")
                               @Max(value = 10000, message = "id超过最大范围了!") Long id){
        return brandService.getBrand(id);
    }
}

​ 校验失败,会抛出 ConstraintViolationException 异常:

image-20211121214407611image-20211121214532689

统一异常处理

​ 如果校验失败,会抛出 MethodArgumentNotValidException 或者 ConstraintViolationException 异常。在实际项目开发中,通常会用统一异常处理根据业务情况来返回一个更友好的提示:

// 等价于 @ControllerAdvice + @ResponseBody
@RestControllerAdvice  
public class GlobalExectionHandler {


    /**
     * requestBody 接收参数,校验失败统一处理
     */
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException exception){
        BindingResult bindingResult = exception.getBindingResult();
        List<FieldError> fieldErrorList =  bindingResult.getFieldErrors();
        int size = fieldErrorList.size();

        StringBuilder sb = new StringBuilder("参数校验失败:");
        for (int i = 0; i < size; i++) {
            FieldError fieldError = fieldErrorList.get(i);
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage());
            if(i != (size-1)){
                sb.append(", ");
            }
        }
        String msg = sb.toString();
        return Result.parameterError().setMessage(msg);
    }


    /**
     * requestParam/PathVariable 接收参数,校验失败统一处理
     */
    @ExceptionHandler({ConstraintViolationException.class})
    public Result handleConstraintViolationException(ConstraintViolationException exception) {
        return Result.parameterError().setMessage("参数校验失败:" + exception.getMessage());
    }
}

快速失败

Spring Validation 默认会校验完所有字段,然后才抛出异常。可以开启 Fali Fast 模式,一旦校验失败就立即返回。

@Configuration
public class ValidateConfig {

    /**
     * 开启参数校验快速失败模式
     */
    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                // 快速失败模式
                .failFast(true)
                .buildValidatorFactory();
        return validatorFactory.getValidator();
    }
}

嵌套校验

​ 实际场景中,可能存在一个实体类中还嵌套着另外的实体类,即一个对象的某个属性是对另一个对象的引用。这种情况下可以使用嵌套校验,此时,该属性字段上必须加上 @Valid注解,下面是一个具体的例子:

/**
 * 参数实体上声明约束
 */
@Data
public class BrandDto {

    private Long id;

    @NotBlank(message = "品牌名不能为空!")
    @Length(max = 30, message = "品牌名太长了!")
    private String name;

    private String chineseSpell;

    private String englishName;
    
    /**
     * 嵌套校验,添加 @Valid
     */
    @Valid
    private EnterpriseBrandDto enterpriseBrandDto;

}


@Data
public class EnterpriseBrandDto {
    private Long id;

    @NotNull(message = "企业账号id不能为空!")
    private Long enterpriseId;

    @NotNull(message = "品牌id不能为空!")
    private Long brandId;

    private Date createDate;

    private Date updateDate;
}



@RestController
@RequestMapping("/v1/web/brand")
public class BrandController {
    /**
     * 在方法参数上使用@Valid声明校验注解
     */
    @PostMapping("/brand")
    public Result saveBrand(@RequestBody @Valid BrandDto brand){
        return brandService.saveBrand(brand);
    }
}

集合校验

​ 如果请求体直接传递了json 数组给后台,并希望对数组中的每一项都进行参数校验。此时要使用自定义 list 集合来接收参数:

@Data
public class ValidList<E> implements List<E> {

    @Delegate
    @Valid
    public List<E> list = new ArrayList<>();
}


@RestController
@RequestMapping("/v1/web/brand")
public class BrandController {
   /**
     * 集合校验
     */
    @PostMapping("/brands")
    public Result saveBrands(@RequestBody @Valid ValidList<BrandDto> brands){
        return brandService.saveBrands(brands);
    }
}

自定义校验

spring validation 支持扩展,可以根据实际业务场景,定制化校验规则。

​ 自定义校验主要分两步进行:

​ 1、自定义校验注解:

/**
 * 自定义注解,校验品牌名
 */
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
public @interface BrandName {

    /**
     * 默认错误消息
     */
    String message() default "品牌名不合法!";

    /**
     * 分组
     */
    Class<?>[] groups() default {};

    /**
     * 负载
     */
    Class<? extends Payload>[] payload() default {};

}

​ 2、实现 ConstraintValidator 接口编写校验器,制定校验规则:

/**
 * 自定义注解处理类必须实现 ConstraintValidator接口, 其中BrandName是自定义的注解, 而String是注解标注参数的类型
 */
public class BrandNameValidator implements ConstraintValidator<BrandName, String> {
    private String value;
    private static List<String> brandList = new ArrayList<>();

    /**
     * 初始化辱华品牌
     */
    static{
        brandList.add("H&M");
        brandList.add("范思哲");
        brandList.add("纪梵希");
        brandList.add("施华洛世奇");
        brandList.add("杜嘉班纳");
    }


    /**
     * 初始化,可以获取自定义校验注解中的属性
     */
    @Override
    public void initialize(BrandName brandName) {
        this.value = brandName.message();
    }


    /**
     *  校验逻辑,true:通过自定义注解的校验;false:没有通过自定义注解的校验,并返回自定义注解中 message的内容
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if(StringUtils.isEmpty(value)){
            return false;
        }

        if(brandList.contains(value)){
            // 禁止默认消息返回
            context.disableDefaultConstraintViolation();

            //自定义返回消息
            context.buildConstraintViolationWithTemplate("拒绝与辱华品牌合作,谢谢理解!").addConstraintViolation();
            return false;
        }
        return true;
    }
}

​ 3、在自定义注解上面指定使用的校验器类(通过@Constraint 指定):

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
/**
 * @Constraint 表示校验此注解的校验器类,可以有多个
 */
@Constraint(validatedBy = {BrandNameValidator.class})
public @interface BrandName{

    String message() default "品牌名不合法!";

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

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

​ 4、使用自定义校验注解:

@Data
public class BrandDto {

    private Long id;

    /**
     * 使用自定义校验注解校验
     */
    @BrandName
    private String name;
}

​ 5、运行结果如下:
image-20211122212158230

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值