zoukankan      html  css  js  c++  java
  • 一起来学SpringBoot(十七)优雅的参数校验

    参数校验
    在开发中经常需要写一些字段校验的代码,比如字段非空,字段长度限制,邮箱格式验证等等,写这些与业务逻辑关系不大的代码个人感觉有两个麻烦:

    验证代码繁琐,重复劳动
    方法内代码显得冗长
    每次要看哪些参数验证是否完整,需要去翻阅验证逻辑代码
    你看这样?我感觉不行 ~有啥好办法不

    public String test1(String name) {
    if (name == null) {
    throw new NullPointerException("name 不能为空");
    }
    if (name.length() < 2 || name.length() > 10) {
    throw new RuntimeException("name 长度必须在 2 - 10 之间");
    }
    return "success";
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    使用hibernate-validator
    spring-boot-starter-web包里面有hibernate-validator包,不需要引用hibernate validator依赖。在 pom.xml 中添加上 spring-boot-starter-web 的依赖即可

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    1
    2
    3
    4
    创建如下实体

    @Data
    public class Book {
    private Integer id;
    @NotBlank(message = "name 不允许为空")
    @Length(min = 2, max = 10, message = "name 长度必须在 {min} - {max} 之间")
    private String name;
    }
    1
    2
    3
    4
    5
    6
    7
    实体校验
    然后呢在 controller 中这样写即可验证

    验证加@RequestBody 的参数

    @RequestMapping("/test")
    public String test(@Validated @RequestBody Book book) {
    return "success";
    }
    1
    2
    3
    4
    这时候呢会出现MethodArgumentNotValidException异常可以在ControllerAdvice 做全局异常处理

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ModelMap> ex(Exception e) {
    log.error("请求参数不合法。", e);
    ModelMap modelMap = new ModelMap();
    if (e instanceof MethodArgumentNotValidException) {
    modelMap.put("message", getErrors(((MethodArgumentNotValidException) e).getBindingResult()));
    }
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(modelMap);
    }

    private Map<String, String> getErrors(BindingResult result) {
    Map<String, String> map = new HashMap<>();
    List<FieldError> list = result.getFieldErrors();
    for (FieldError error : list) {
    map.put(error.getField(), error.getDefaultMessage());
    }
    return map;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    如果不加呢?

    @RequestMapping("/test")
    public String test(@Validated Book book) {
    return "success";
    }
    1
    2
    3
    4
    则会出BindException 异常,则又可以在ControllerAdvice 中加入判断

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ModelMap> ex(Exception e) {
    log.error("请求参数不合法。", e);
    ModelMap modelMap = new ModelMap();
    if (e instanceof BindException) {
    modelMap.put("message", getErrors(((BindException) e).getBindingResult()));
    } else if (e instanceof MethodArgumentNotValidException) {
    modelMap.put("message", getErrors(((MethodArgumentNotValidException) e).getBindingResult()));
    }
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(modelMap);
    }
    private Map<String, String> getErrors(BindingResult result) {
    Map<String, String> map = new HashMap<>();
    List<FieldError> list = result.getFieldErrors();
    for (FieldError error : list) {
    map.put(error.getField(), error.getDefaultMessage());
    }
    return map;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    验证参数
    如果是get请求参数呢?

    @RequestMapping("/test")
    public String test(@Validated @NotBlank(message = "name 不允许为空") String name) {
    System.out.println("111");
    return "success";
    }
    1
    2
    3
    4
    5
    我们发现这样根本不好使,其实呢这种需要在类上加入

    @Validated
    @RestController
    public class TestController {
    @RequestMapping("/test")
    public String test(@NotBlank(message = "name 不允许为空") String name) {
    System.out.println("111");
    return "success";
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    这样才可以生效,此时呢返回ConstraintViolationException 异常可以在全局异常中这样处理

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ModelMap> ex(Exception e) {
    log.error("请求参数不合法。", e);
    ModelMap modelMap = new ModelMap();
    if (e instanceof HttpMediaTypeException) {
    modelMap.put("message", "请求体不对");
    } else if (e instanceof ConstraintViolationException) {
    ConstraintViolationException exs = (ConstraintViolationException) e;
    Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
    modelMap.put("message", getErrors(violations));
    }
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(modelMap);
    }
    private Map<String, String> getErrors(Set<ConstraintViolation<?>> violations) {
    Map<String, String> map = new HashMap<>();
    for (ConstraintViolation<?> item : violations) {
    map.put(item.getPropertyPath().toString(), item.getMessage());
    }
    return map;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    Model校验
    如过不是想验证传参呢?就是想验证一个实体怎么玩呢?

    这样就可以解决了

    @RestController
    public class TestController {
    @Autowired
    private Validator validator;
    @RequestMapping("/test")
    public Map<String, String> test() {
    Book book = new Book();
    book.setId(1).setName("");
    Set<ConstraintViolation<Book>> violationSet = validator.validate(book);
    return getErrors(violationSet);
    }
    private <T> Map<String, String> getErrors(Set<ConstraintViolation<T>> violations) {
    Map<String, String> map = new HashMap<>();
    for (ConstraintViolation<?> item : violations) {
    map.put(item.getPropertyPath().toString(), item.getMessage());
    }
    return map;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    对象级联校验
    在比如book那个实体中加入了一个具有对象这时候改怎么办呢?

    只需要在实体上加入@Valid 即可

    @Data
    @Accessors(chain = true)
    public class Book {

    private Integer id;
    @NotBlank(message = "name 不允许为空")
    @Length(min = 2, max = 10, message = "name 长度必须在 {min} - {max} 之间")
    private String name;
    @Valid
    private Author author;

    @Data
    @Accessors(chain = true)
    public static class Author {
    @NotBlank(message = "Author.name 不允许为空")
    private String name;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    hibernate-validator 的校验模式
    这时候要说点东西了

    上面例子中一次性返回了所有验证不通过的集合,通常按顺序验证到第一个字段不符合验证要求时,就可以直接拒绝请求了。Hibernate Validator有以下两种验证模式

    普通模式(默认是这个模式)
    普通模式(会校验完所有的属性,然后返回所有的验证失败信息)

    快速失败返回模式
    快速失败返回模式(只要有一个验证失败,则返回)

    ​ true 快速失败返回模式 false普通模式

    ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
    .configure()
    .failFast( true )
    .buildValidatorFactory();
    Validator validator = validatorFactory.getValidator();
    1
    2
    3
    4
    5
    或者这样配

    ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
    .configure()
    .addProperty( "hibernate.validator.fail_fast", "true" )
    .buildValidatorFactory();
    Validator validator = validatorFactory.getValidator();
    1
    2
    3
    4
    5
    这样配置就行了

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
    MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
    /**设置validator模式为快速失败返回*/
    postProcessor.setValidator(validator());
    return postProcessor;
    }

    @Bean
    public Validator validator(){
    ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
    .configure()
    .addProperty( "hibernate.validator.fail_fast", "true" )
    .buildValidatorFactory();
    Validator validator = validatorFactory.getValidator();
    return validator;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    分组校验
    分组顺序校验时,按指定的分组先后顺序进行验证,前面的验证不通过,后面的分组就不行验证。

    有这样一种场景,新增的时候,不需要验证Id(因为系统生成);修改的时候需要验证Id,这时候可用用户到validator的分组验证功能。

    设置validator为普通验证模式("hibernate.validator.fail_fast", "false"),用到的验证GroupA、GroupB和实体:

    GroupA、GroupB
    1
    public interface GroupA {
    }
    public interface GroupB {
    }
    1
    2
    3
    4
    然后改造一下Book实体

    @Data
    @Accessors(chain = true)
    public class Book {
    @NotBlank
    @Range(min = 1, max = Integer.MAX_VALUE, message = "必须大于0", groups = {GroupA.class})
    private Integer id;
    @NotBlank(message = "name 不允许为空")
    @Length(min = 4, max = 20, message = "name 长度必须在 {min} - {max} 之间", groups = {GroupB.class})
    private String name;
    @NotBlank
    @Range(min = 0, max = 100, message = "年龄必须在[0,100]", groups = {Default.class})
    private Integer age;
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    GroupA验证字段id;
    GroupB验证字段name;
    Default验证字段age(Default是Validator自带的默认分组)
    这样去验证

    @RequestMapping("/test")
    public void test() {
    Book book = new Book();
    /**GroupA验证不通过*/
    book.setId(-10086);
    /**GroupA验证通过*/
    //book.setId(10010);
    book.setName("a");
    book.setAge(110);
    Set<ConstraintViolation<Book>> validate = validator.validate(book, GroupA.class, GroupB.class);
    for (ConstraintViolation<Book> item : validate) {
    System.out.println(item);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    或者这样

    @RequestMapping("/test")
    public void test(@Validated({GroupA.class, GroupB.class}) Book book, BindingResult result) {
    if (result.hasErrors()) {
    List<ObjectError> allErrors = result.getAllErrors();
    for (ObjectError error : allErrors) {
    System.out.println(error);
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    当然这样验证务必要给 组一个序列,不然不行的还是无法实现

    @GroupSequence({GroupA.class, GroupB.class, Default.class})
    public interface GroupOrder {
    }
    1
    2
    3
    这样就好了然后这样玩

    Set<ConstraintViolation<Book>> validate = validator.validate(book, GroupOrder.class);
    1
    @Validated({GroupOrder.class})Book book, BindingResult result
    1
    注意项
    如果不想全局拦截异常想看到直观的错误可以在方法参数中加入BindingResult result

    单一的可以这样玩

    public void test()(@Validated DemoModel demo, BindingResult result)
    1
    验证多个的话可以这样玩

    public void test()(@Validated DemoModel demo, BindingResult result,@Validated DemoModel demo2, BindingResult result2)
    1
    自定义验证器
    一般情况,自定义验证可以解决很多问题。但也有无法满足情况的时候,此时,我们可以实现validator的接口,自定义自己需要的验证器。

    首先呢定义个注解,在注解上加入注解@Constraint 绑定验证类

    @Target({FIELD, PARAMETER})
    @Retention(RUNTIME)
    @Constraint(validatedBy = DateTimeValidator.class)
    public @interface DateTime {

    String message() default "格式错误";

    String format() default "yyyy-MM-dd";

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

    Class<? extends Payload>[] payload() default {};
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    然后看验证类 实现ConstraintValidator<A extends Annotation, T>即可 A是注解 T是标注的参数

    public class DateTimeValidator implements ConstraintValidator<DateTime, String> {

    private DateTime dateTime;

    @Override
    public void initialize(DateTime dateTime) {
    this.dateTime = dateTime;
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
    // 如果 value 为空则不进行格式验证,为空验证可以使用 @NotBlank @NotNull @NotEmpty 等注解来进行控制,职责分离
    if (value == null) {
    return true;
    }
    String format = dateTime.format();
    if (value.length() != format.length()) {
    return false;
    }
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format);
    try {
    simpleDateFormat.parse(value);
    } catch (ParseException e) {
    return false;
    }
    return true;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    然后这样用就行啦

    @Validated
    @RestController
    public class ValidateController {
    @GetMapping("/test")
    public String test(@DateTime(message = "您输入的格式错误,正确的格式为:{format}", format = "yyyy-MM-dd HH:mm") String date) {
    return "success";
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    JSR-303 注释介绍
    hibernate-validator均实现了 JSR-303 这里只列举了 javax.validation 包下的注解,同理在 spring-boot-starter-web 包中也存在 hibernate-validator 验证包,里面包含了一些 javax.validation 没有的注解,有兴趣的可以看看

    注解 说明
    @NotNull 限制必须不为null
    @NotEmpty 验证注解的元素值不为 null 且不为空(字符串长度不为0、集合大小不为0)
    @NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
    @Pattern(value) 限制必须符合指定的正则表达式
    @Size(max,min) 限制字符长度必须在 min 到 max 之间(也可以用在集合上)
    @Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
    @Max(value) 限制必须为一个不大于指定值的数字
    @Min(value) 限制必须为一个不小于指定值的数字
    @DecimalMax(value) 限制必须为一个不大于指定值的数字
    @DecimalMin(value) 限制必须为一个不小于指定值的数字
    @Null 限制只能为null(很少用)
    @AssertFalse 限制必须为false (很少用)
    @AssertTrue 限制必须为true (很少用)
    @Past 限制必须是一个过去的日期
    @Future 限制必须是一个将来的日期
    @Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过 integer,小数部分的位数不能超过 fraction (很少用)
    哦对,这些校验不仅能在controller层用 在任何地方都可以的

  • 相关阅读:
    虚拟化技术
    软件产业的知识经济 (蔡学墉)
    关于内存对齐
    Reverse Engineering
    [转]今天的操作系统 
    BasicBIOS & CMOS
    [bbk5355]第18集 Chapter 08 Handling Exceptions(01)
    [bbk1452]第1集 在Apache中使用SSL
    Linux>User Manager
    如何更新linux系统时间
  • 原文地址:https://www.cnblogs.com/hyhy904/p/10962034.html
Copyright © 2011-2022 走看看