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<Integer> 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":"shaopro@qq.com","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<String> 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<? extends Payload>[] payload() default {};

}

注解处理类

public class AnnoValidatorClass implements ConstraintValidator<AnnoValidator, Object> {

    private String value;

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

    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        List<String> 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<? extends Payload>[] payload() default {};
}

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

@Data
public class UserDynamic {

    private String name;

    @Dynamic
    private Set<String> books;

}

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

public class DynamicHandler implements ConstraintValidator<Dynamic, Set<String>> {

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

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

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

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

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

@Slf4j
@Component
@EnableScheduling
public class DynamicSchedule {


    @Scheduled(fixedDelay = 10000)
    public void autoSync() {
        CopyOnWriteArraySet<String> 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容