
有这样一种场景,前端请求后端接口时,需要传递的是一个数组,数组的元素是一个对象,并且希望后台收到参数后可以对数组集合中的元素元素对象的属性进行校验,如果后台直接以List的来接收参数,约束注解的校验规则并不会触发,类似这样:
- @PostMapping("/add")
- public String add(@Validated(AddStuAndTeach.class) @RequestBody List
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
implements List { - @Valid
- @Delegate
- private List
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
teachers) { - System.out.println("添加老师:" + teachers.size());
- return "success";
- }
- }
上面分享了参数的基础校验以及一些特殊场景下的参数校验,比如嵌套校验、分组校验、集合校验,但是如果和业务相关的,需要查询数据库才能进行校验的业务参数校验是不是还得像开头说的,用大量的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
{ - @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
{ -
- 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
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更简单直观,所以通常不管是事务管理、还是参数校验,建议还是用声明式的这种,比较优雅。
通常情况下,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();
- }
- }
- }
以上hibernate-validator和spring validaton提供的关于参数校验的实战应用。
Springboot扩展点系列实现方式、工作原理集合:
Springboot扩展点之ApplicationContextInitializer
Springboot扩展点之BeanDefinitionRegistryPostProcessor
Springboot扩展点之BeanFactoryPostProcessor
Springboot扩展点之BeanPostProcessor
Springboot扩展点之InstantiationAwareBeanPostProcessor
Springboot扩展点之SmartInstantiationAwareBeanPostProcessor
Springboot扩展点之ApplicationContextAwareProcessor
Springboot扩展点之InitializingBean
Springboot扩展点之SmartInitializingSingleton
Springboot核心功能工作原理: