SpringBoot2.X 实战8 -- Valid & Validated

一.前言

当提供一个接口对外提供服务时,数据校验是必须需要考虑的事情。很多时候,必须在每个单独的验证框架中实现完全相同的验证。为了避免在每一层重新实现这些验证,许多开发人员会将验证直接捆绑到他们的类中,用复制的验证代码将它们混杂在一起。
这个JSR将为JavaBean验证定义一个元数据模型和API。

二.Valid 参数校验

Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。

JSR 303 校验框架注解类:
• @NotNull 注解元素必须是非空
• @Null 注解元素必须是空
• @Digits 验证数字构成是否合法
• @Future 验证是否在当前系统时间之后
• @Past 验证是否在当前系统时间之前
• @Max 验证值是否小于等于最大指定整数值
• @Min 验证值是否大于等于最小指定整数值

• @Pattern 验证字符串是否匹配指定的正则表达式
• @Size 验证元素大小是否在指定范围内
• @DecimalMax 验证值是否小于等于最大指定小数值
• @DecimalMin 验证值是否大于等于最小指定小数值
• @AssertTrue 被注释的元素必须为true
• @AssertFalse 被注释的元素必须为false

Hibernate Validator扩展注解类:

• @Email 被注释的元素必须是电子邮箱地址
• @Length 被注释的字符串的大小必须在指定的范围内
• @NotEmpty 被注释的字符串的必须非空
• @Range 被注释的元素必须在合适的范围内

校验结果保存在BindingResult或Errors对象中

• 这两个类都位于org.springframework.validation包中
• 需校验的表单对象和其绑定结果对象或错误对象是成对出现的
• Errors接口提供了获取错误信息的方法,如getErrorCount()获取错误的数量, getFieldErrors(String field)得到成员属性的校验错误列表
• BindingResult接口扩展了Errors接口,以便可以使用Spring的org.springframeword.validation.Validator对数据进行校验,同时获取数据绑定结果对象的信息


@Data
public class UserComplex {

    @NotEmpty(message = "名字不能为空")
    @Size(min = 2, max = 6, message = "名字长度在2-6位") //字符串,集合,map限制大小
    @Length(min = 2, max = 6, message = "名字长度在2-6位")
    private String name;

    @Length(min = 3, max = 3, message = "pass 长度不为3")
    private String pass;

    @DecimalMin(value = "10", inclusive = true, message = "salary 低于10") // 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    private int salary;

    @Range(min = 5, max = 10, message = "range 不在范围内")
    private int range;

    @NotNull(message = "年龄不能为空")
    @Min(value = 18, message = "年龄不能小于18")
    @Max(value = 70, message = "年龄不能大于70")
    private int age;

    @Email
    private String email;

    @AssertTrue
    private boolean flag;

    @Past
    private Date birthday;

    @Future
    private Date expire;

    @URL(message = "url 格式不对")
    private String url;

    @AnnoValidator(value = "1,2,3")
    private String anno;
    //@Pattern(regex=,flag=)  被注释的元素必须符合指定的正则表达式

    @Size(min = 2, max = 6, message = "长度在2-6位") //字符串,集合,map限制大小
    private List list;

}



三.Validated 参数校验

Validated是 Spring 对 Valid 的封装,是 Valid 的加强版,支持更多特性
1.只要类路径上有JSR-303实现(比如Hibernate验证器),Bean validation 1.1支持的方法验证特性就会自动启用。这让bean方法可以用javax进行注释。对其参数和/或返回值的验证约束。使用这种带注释的方法的目标类需要在类型级别上使用@Validated注释进行注释,以便搜索它们的方法以找到内联约束注释。
Validated 支持对 PathVariable 参数校验,以及 RequestParam 参数校验
但是注解必须写在类上:

@RestController
// 注解必须写在这里,参数校验不过会有 ConstraintViolationException 异常
@Validated
public class Valid2Controller {

    @GetMapping("valid4/{data}")
    public String getPathVariable(@Size(min = 3, max = 6) @PathVariable String data) {
        return data;
    }

    @GetMapping("valid4")
    public String getRequestParam(@Size(min = 3, max = 6) @RequestParam(value = "name", defaultValue = "0") String name) {
        return name;
    }

}

2.分组检验
根据需要校验特定字段,应用场景:SpringDataJPA 中 save 方法没有 ID 字段就是保持新的数据,如果有 ID 字段就是跟新数据。
使用方法:首先写一些接口,这里的接口是标记用的,就像 JsonView 一样。、
BaseA

public interface BaseA {
}

BaseB

public interface BaseB {
}

被校验的实体类

@Data
public class UserSimple {

    //在AAAA分组时,判断不能为空
    @NotEmpty(groups = {BaseA.class})
    private String id;

    //name字段不为空,且长度在3-8之间
    @NotEmpty(message = "{user.name.notBlank}", groups = {BaseB.class})
    @Size(min = 3, max = 8, message = "{user.name.notBlank}", groups = {BaseB.class})
    private String name;

    private int age;

}

分组检验。当接口中(@RequestBody @Validated({BaseB.class}) 时只会校验实体类中被(groups = {BaseB.class})标记的字段。

@RestController
public class Valid1Controller {

    // {"name":"shao","pass":"333","salary":11,"range":6,"age":20,"email":"[email protected]","flag":true,"birthday":"2018-08-07T16:25:44.000+0000","expire":"2018-12-01T10:12:24.000+0000","url":"http://www.baidu.com","anno":"1","list":[1,2]}
    @PostMapping("valid1")
    public UserComplex postUser1(@RequestBody @Valid UserComplex user) {
        return user;
    }

    @PostMapping("valid2")
    public UserSimple postGroup(@RequestBody @Validated({BaseB.class}) UserSimple simpleUser) {
        return simpleUser;
    }

    @PutMapping("valid2")
    public UserSimple putGroup(@RequestBody @Validated({BaseA.class}) UserSimple simpleUser) {
        return simpleUser;
    }

}

四.参数校验异常处理

1.比较 low 的方式是在接口中使用 BindingResult 对象去接受这个校验结果
例如:

    @RequestMapping("login")  
    public String login(@Valid User user, BindingResult result) {  
       if (result.hasErrors())  
           return "user/login";  
       return "redirect:/";  
    }  
    

先去判断是否有异常
2.比较好方式是使用同一异常处理

@RestControllerAdvice
public class CheckAdvice {

    private static final ObjectMapper MAPPER = new ObjectMapper();

    /**
     * 请求的 JSON 参数在请求体内的参数校验
     *
     * @param e 异常信息
     * @return 返回数据
     * @throws JsonProcessingException jackson 的异常
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity handleBindException1(MethodArgumentNotValidException e) throws JsonProcessingException {
        e.getBindingResult().getAllErrors().forEach(System.out::println);
        return new ResponseEntity<>("cuowu:" + MAPPER.writeValueAsString(e.getBindingResult().getAllErrors()), HttpStatus.BAD_REQUEST);
    }

    /**
     * 请求的 URL 参数检验
     *
     * @param e 异常信息
     * @return 返回提示信息
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(ConstraintViolationException.class)
    public String handleBindException2(ConstraintViolationException e) {
        e.getConstraintViolations().forEach(System.out::println);
        return "ConstraintViolationException";
    }

}

五.自定义参数校验

在很多情况下,JSR303 不能满足我们的校验需求,我们需要自定义一些校验逻辑,当然不是使用 if else 去判断。可以定义一些注解以及注解处理工具嵌入到 Spring 框架中自动调用。
这里我们定义了注解 AnnoValidator 用来校验 anno 字段,只能存放1,2,3之中的一个字符串数字

    @AnnoValidator(value = "1,2,3")
    private String anno;

注解的内容:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
//指定注解的处理类
@Constraint(validatedBy = AnnoValidatorClass.class)
public @interface AnnoValidator {

    String value();

    String message() default "AnnoValidator 不存在";

    Class[] groups() default {};

    Class[] payload() default {};

}

注解处理类

public class AnnoValidatorClass implements ConstraintValidator {

    private String value;

    @Override
    public void initialize(AnnoValidator constraintAnnotation) {
        this.value = constraintAnnotation.value();
    }

    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        List list = Arrays.asList(value.split(","));
        final AtomicBoolean flag = new AtomicBoolean(false);
        list.forEach(one -> {
            if (one.equals(o)) {
                flag.set(true);
            }
        });
        return flag.get();
    }

}

六.动态改变校验数据

经常某些字段的校验数据是动态改变的(例如手机的号码段扩充,某些操作的操作码增加),所以这些特殊情况我们不能讲校验的数据写死到代码中。下面展示了动态改变校验数据的方法。

自定义校验注解


@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
//指定注解的处理类
@Constraint(validatedBy = DynamicHandler.class)
public @interface Dynamic {

    String value() default "";

    String message() default "Dynamic 不存在";

    Class[] groups() default {};

    Class[] payload() default {};
}

需要校验的实体类,假设 books 内的数据只能在某个范围内。

@Data
public class UserDynamic {

    private String name;

    @Dynamic
    private Set books;

}

注解的处理类。需要注意的点:
1.被 static 关键字修饰的字段是属于整个类的;
2.存储可变字段的 Set 必须是线程安全的,当对容器中元素进行遍历同时增加数据时会抛出 fail-fast 错误。

public class DynamicHandler implements ConstraintValidator> {

    // 得用线程安全的容器,当对容器中元素进行遍历同时增加数据时会抛出 fail-fast 错误
    private volatile static CopyOnWriteArraySet dynamicSet;

    @Override
    public boolean isValid(Set set, ConstraintValidatorContext constraintValidatorContext) {
        return dynamicSet.containsAll(set);
    }

    @Override
    public void initialize(Dynamic constraintAnnotation) {
        // nothing to do
    }

    public static void setSet(CopyOnWriteArraySet set) {
        DynamicHandler.dynamicSet = set;
    }
}

使用定时任务来模拟校验数据的改变,每十秒钟改变一下校验的数据。真实环境中应该是从数据库中获取。

@Slf4j
@Component
@EnableScheduling
public class DynamicSchedule {


    @Scheduled(fixedDelay = 10000)
    public void autoSync() {
        CopyOnWriteArraySet dynamicSet = new CopyOnWriteArraySet<>();
        Random random = new Random();
        for (int i = 0; i < 3; i++) {
            dynamicSet.add(random.nextInt(10) + "");
        }
        DynamicHandler.setSet(dynamicSet);
        log.info(dynamicSet.toString());
    }

}

七.代码路径

https://github.com/shaopro/SpringBootValidated

你可能感兴趣的:(SpringBoot2.X 实战8 -- Valid & Validated)