Springboot优雅的参数校验(二)

7. 集合校验

有这样一种场景,前端请求后端接口时,需要传递的是一个数组,数组的元素是一个对象,并且希望后台收到参数后可以对数组集合中的元素元素对象的属性进行校验,如果后台直接以List的来接收参数,约束注解的校验规则并不会触发,类似这样:

@PostMapping("/add")
public String add(@Validated(AddStuAndTeach.class) @RequestBody List<Teacher> teachers) {
    System.out.println("添加老师:" + teachers.size());
    return "success";
}
@Data
public class Teacher {
    @NotNull(message = "学生的老师姓名不能为空",groups = AddStuAndTeach.class)
    private String tecName;
    @NotNull(message = "教授科目不能为空",groups = AddStuAndTeach.class)
    private String subject;
}

那么应该怎么办呢?

1.需要重新实现List接口,并且在实现类里声明一个List类型变量,并且用@Valid声明;@Delegate是lombok的注解,其作用就是为变量生成一些常用方法,和@Data比较类似,具体可以自行检索lombok相关用法;(当然也可以不用这个注解,但是需要自行实现List接口的相关方法);

2.集合内元素对象上的约束注解的用法和参数基础校验一样;

3.controller层方法内要用重新实现List接口的类来接收前台传过来的参数,并且添加@Validated @RequestBody注解;

总结:这种用法感觉有些奇怪,但是很有效,也算是解决了集合类参数校验的问题了。

@Data
public class ValidationList<E> implements List<E> {
    @Valid
    @Delegate
    private List<E> list = new ArrayList<>();
}
@Data
public class Teacher {
    @NotNull(message = "学生的老师姓名不能为空",groups = AddStuAndTeach.class)
    private String tecName;
    @NotNull(message = "教授科目不能为空",groups = AddStuAndTeach.class)
    private String subject;
}
@RestController
@RequestMapping("/teacher")
public class TeacherController {
    @PostMapping("/add")
    public String add(@Validated(AddStuAndTeach.class) @RequestBody ValidationList<Teacher> teachers) {
        System.out.println("添加老师:" + teachers.size());
        return "success";
    }
}

8. 自定义校验

上面分享了参数的基础校验以及一些特殊场景下的参数校验,比如嵌套校验、分组校验、集合校验,但是如果和业务相关的,需要查询数据库才能进行校验的业务参数校验是不是还得像开头说的,用大量的if进行啰嗦的判断,答案是否定的,java API除了定义了一些标准的用法,同是也对外暴露了校验验证器接口(ConstraintValidator),让用户自己实现一些自定义的校验逻辑。具体怎么用呢?下面让慢慢道来:

1.需要声明一个自定义约束注解,如@SexValid(校验性别格式是否正确)、@StuCodeValid(校验学生是否重复),@NotNull是java API已经定义好的,可以参考一下看看人家是怎么定义的;

2.实现校验验证器接口(ConstraintValidator),并且重写有效性校验逻辑;(这里需要特别注意一下,如果校验通过,返回true; 如果校验校验不通过就返回false,剩下抛出异常、捕获异常就不管了);

3.把我自定义的好的约束注解应用到controller层方法参数对象的属性上;

我用两个例子来说明一下

第一个:假如在添加学生的时候需要校验学号是否已经分配给其他学生了

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Constraint(validatedBy = StuCodeValidator.class)
public @interface StuCodeValid {
    String[] value() default {};
    String message() default "";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
@Component
public class StuCodeValidator implements ConstraintValidator<StuCodeValid,String> {
    @Autowired
    private StudentService studentService;
    /**
     * 参数有效性校验
     * @param value
     * @param context
     * 校验规则:
     * 如果学生学号发生重复为无效返回false;
     * 如果学生学号不重复会有效,则返回true;
     * @return
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value==null) {
            return false;
        }
        //查询学号是否重复,如果重复返回true,否则近回false;
        boolean flag = studentService.queryStuCodeRepeat(value);
        return !flag;
    }
}
@Data
public class Student implements Serializable {

    @NotNull(message = "学生id不能为空",groups = QueryDetail.class)
    private Integer id;
    @NotNull(message = "学号不能为空",groups = AddStudent.class)
    @Length(min = 2, max = 4, message = "学号的长度范围是(2,4)")
    @StuCodeValid(groups = AddStudent.class,message = "学生的学号不能重复")
    private String stuCode;
    @NotNull(message = "姓名不能为空",groups = AddStudent.class)
    @Length(min = 2, max = 3, message = "姓名的长度范围是(2,3)",groups = AddStudent.class)
    private String stuName;
}
@PostMapping("/add")
public Student add(@Validated(AddStudent.class) @RequestBody Student student) {
    System.out.println(student.getStuName());
    return student;
}

第二个:假如在添加学生的时候需要校验学生的性别必须是“男”或“女”

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Documented
@Constraint(validatedBy = SexValidator.class)
public @interface SexValid {
    //定义注解的里值
    String[] value() default {"男","女"};
    //定义异常信息
    String message() default "性别格式错误,请更正";
    //如果是需要分组校验,这个属性就用得上了
    Class<?>[] groups() default {};
    //这个可以携带无
    Class<? extends Payload>[] payload() default {};
}
public class SexValidator implements ConstraintValidator<SexValid,String> {

    private String[] values;
    @Override
    public void initialize(SexValid constraintAnnotation) {
        this.values=constraintAnnotation.value();
    }
    /**
     * 参数有效性校验
     * @param value
     * @param context
     * @return 如果参数有效,返回true;否则false
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        List<String> list = Arrays.asList(values);
        if (value==null) {
            return  false;
        }
        if (list.contains(value)) {
            return true;
        }
        return false;
    }
}
@Data
public class Student implements Serializable {

    @NotNull(message = "学生id不能为空",groups = QueryDetail.class)
    private Integer id;
    @NotNull(message = "学号不能为空",groups = AddStudent.class)
    @Length(min = 2, max = 4, message = "学号的长度范围是(2,4)")
    private String stuCode;
    @NotNull(message = "姓名不能为空",groups = AddStudent.class)
    @Length(min = 2, max = 3, message = "姓名的长度范围是(2,3)",groups = AddStudent.class)
    private String stuName;
    @SexValid(groups = AddStudent.class)
    private String sex;
}
@PostMapping("/add")
public Student add(@Validated(AddStudent.class) @RequestBody Student student) {
    System.out.println(student.getStuName());
    return student;
}

总结:通过自己实现校验验证器,弥补了一些特殊场景下的校验需求,再也不用if esle了,代码复用性、可阅读性都大大提高了,整个controller看起来都无比清爽了。当然,方法虽妙,也要特别注意一下ConstraintValidator#isValid()方法的校验逻辑是:只有校验通过才返回true,false表示触发校验规则了,校验不通过,后面要抛出异常提示了。

自定义校验除了通过注解这种声明式的实现外,还有一种编程式的实现,就好像spring的事务管理有两种:一种是声明式的,通过注解实现;另外一种是编程式,就硬编码来管理事务;事实上,如果非要硬编码,不如还用if else更简单直观,所以通常不管是事务管理、还是参数校验,建议还是用声明式的这种,比较优雅。

9. 快速失败

通常情况下,Spring Validation默认为校验完所有的字段,然后才抛出异常;当然,如果你希望一旦校验失败就马上返回,不等校验完所有字段,那么就需要手动开启快速失败的模式:

@Configuration
public class ValidCofing {

    @Bean
    public Validator validator(AutowireCapableBeanFactory springFactory) {
        try (ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
                .configure()
                // 快速失败
                .failFast(true)
                // 解决 SpringBoot 依赖注入问题
                .constraintValidatorFactory(new SpringConstraintValidatorFactory(springFactory))
                .buildValidatorFactory()) {
            return factory.getValidator();
        }
    }
}

总结

  1. Java API规范(JSR303)定义了Bean校验的标准validation-api,但没有提供实现。
  2. hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等。
  3. Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验
  4. @Validated是spring validation提供的能力,支持分组校验,不支持嵌套校验;
  5. @Valid是hibernate validation提供的能力,支持嵌套校验,不支持分组校验;
  6. 最后一点很坑,困惑我很久,@Validated:可以用在类型、方法和方法参数上,@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上,事实上是这样不?当然是否定的,从注解上看,确实是这样,但是如果没有对应的具体实现,可以也只是“可以”而已。

        以上hibernate-validator和spring validaton提供的关于参数校验的实战应用。

Springboot扩展点系列实现方式、工作原理集合:

Springboot扩展点之ApplicationContextInitializer

Springboot扩展点之BeanDefinitionRegistryPostProcessor

Springboot扩展点之BeanFactoryPostProcessor

Springboot扩展点之BeanPostProcessor

Springboot扩展点之InstantiationAwareBeanPostProcessor

Springboot扩展点之SmartInstantiationAwareBeanPostProcessor

Springboot扩展点之ApplicationContextAwareProcessor

Springboot扩展点之@PostConstruct

Springboot扩展点之InitializingBean

Springboot扩展点之DisposableBean

Springboot扩展点之SmartInitializingSingleton

Springboot核心功能工作原理:

Springboot实现调度任务的工作原理

Springboot事件监听机制的工作原理

上一篇:Springboot优雅的参数校验(一)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凡夫贩夫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值