10、参数数据校验及全局异常

1、参数校验

1.1、前提准备

一个健壮的系统都要对外部提交的数据进行完整性、合法性的校验。 校验是我们程序开发中必不可少的过程。 即使开发一个不面对最终用户的工具包,也需要对传入的数据进行缜密的校验来防止引发底层难以追踪的问题

1、数据校验先引入数据校验jar包

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2、JSR提供的校验注解:

@Null 被注释的元素必须为null

@NotNull 被注释的元素必须不为null

@AssertTrue 被注释的元素必须为true

@AssertFalse 被注释的元素必须为false

@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值

@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值

@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值

@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值

@Size(max=,min=) 被注释的元素的大小必须在指定的范围内

@Digits(integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内

@Past 被注释的元素必须是一个过去的日期

@Future 被注释的元素必须是一个将来的日期

@Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式

1.2、controller层参数校验

在controller层的参数校验可以分为两种场景:

  1. 单个参数校验

  2. 实体类参数校验

1、对单个参数校验

@RestController
@Validated
public class PingController {
 
    @GetMapping("/getUser")
    public String getUserStr(@NotNull(message = "name 不能为空") String name,
                             @Max(value = 99, message = "不能大于99岁") Integer age) {
        return "name: " + name + " ,age:" + age;
    }
}

注意:这里一定要在方法所在的controller类上加入@Validated注解,不然没有任何效果。

2、对实体类参数校验

当处理post请求或者请求参数较多的时候我们一般会选择使用一个bean来接收参数,然后在每个需要校验的属性上使用参数校验注解

@Data
public class UserInfo {
    @NotNull(message = "username cannot be null")
    private String name;
 
    @NotNull(message = "sex cannot be null")
    private String sex;
 
    @Max(value = 99L)
    private Integer age;
}

然后在controller方法中用@RequestBody表示这个参数接收的类:

@RestController
public class PingController {
    @Autowired
    private Validator validator;
 
    @GetMapping("metrics/ping")
    public Response<String> ping() {
        return new Response<>(ResponseCode.SUCCESS, null,"pang");
    }
 
    @PostMapping("/getUser")
    public String getUserStr(@RequestBody @Validated({GroupA.class, Default.class}) UserInfo user, BindingResult bindingResult) {
        validData(bindingResult);
 
        return "name: " + user.getName() + ", age:" + user.getAge();
    }
 
    private void validData(BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            StringBuffer sb = new StringBuffer();
            for (ObjectError error : bindingResult.getAllErrors()) {
                sb.append(error.getDefaultMessage());
            }
            throw new ValidationException(sb.toString());
        }
    }
}

需要注意的是,如果想让UserInfo中的参数注解生效,还必须在Controller参数中使用@Validated注解。这种参数校验方式的校验结果会被放到BindingResult中,其中还用到了分组,也可以不用

3、参数校验分组

在实际开发中经常会遇到这种情况:想要用一个实体类去接收多个controller的参数,但是不同controller所需要的参数又有些许不同,而你又不想为这点不同去建个新的类接收参数。比如有一个/setUser接口不需要id参数,而/getUser接口又需要该参数,这种时候就可以使用参数分组来实现。

  1. 定义表示组别的interface

public interface GroupA {
}

  2.在@Validated中指定使用哪个组;

@RestController
public class PingController {
    @PostMapping("/getUser")
    public String getUserStr(@RequestBody @Validated({GroupA.class, Default.class}) UserInfo user, BindingResult bindingResult) {
        validData(bindingResult);
        return "name: " + user.getName() + ", age:" + user.getAge();
    }
 
    @PostMapping("/setUser")
    public String setUser(@RequestBody @Validated UserInfo user, BindingResult bindingResult) {
        validData(bindingResult);
        return "name: " + user.getName() + ", age:" + user.getAge();
    }

其中Defaultjavax.validation.groups中的类,表示参数类中其他没有分组的参数,如果没有,/getUser接口的参数校验就只会有标记了GroupA的参数校验生效。

  3.在实体类的注解中标记这个哪个组所使用的参数;

@Data
public class UserInfo {
    @NotNull( groups = {GroupA.class}, message = "id cannot be null")
    private Integer id;
 
    @NotNull(message = "username cannot be null")
    private String name;
 
    @NotNull(message = "sex cannot be null")
    private String sex;
 
    @Max(value = 99L)
    private Integer age;
}
    }

4、级联参数校验

当参数bean中的属性又是一个复杂数据类型或者是一个集合的时候,如果需要对其进行进一步的校验需要考虑哪些情况呢?

@Data
public class UserInfo {
    @NotNull( groups = {GroupA.class}, message = "id cannot be null")
    private Integer id;
 
    @NotNull(message = "username cannot be null")
    private String name;
 
    @NotNull(message = "sex cannot be null")
    private String sex;
 
    @Max(value = 99L)
    private Integer age;
   
    @NotEmpty
    private List<Parent> parents;
}

比如对于parents参数,@NotEmpty只能保证list不为空,但是list中的元素是否为空、User对象中的属性是否合格,还需要进一步的校验。这个时候我们可以这样写:

 @NotEmpty
    private List<@NotNull @Valid UserInfo> parents;

5、其他应用

大小、空值校验

@NotNull(message = "姓名不能为空")
@Size(min = 1, max = 20, message = "姓名长度必须在1-20之间")
private String name;
@NotNull(message = "姓名不能为空")
@Size(min = 1, max = 20, message = "姓名长度必须在1-20之间")
private String name;

自定义邮箱正则

@Email(regexp = "^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$",message = "邮箱格式不合理")
private String email;

手机号码校验

@Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$",message = "手机号码格式错误")
@NotBlank(message = "手机号码不能为空")
private String phone;

2、全局异常控制

有时候不可避免服务器报错的情况,如果不配置异常处理机制,就会默认返回tomcat或者nginx的5XX页面,对普通用户来说,不太友好,用户也不懂什么情况。这时候需要我们程序员设计返回一个友好简单的格式给前端。

处理办法如下:通过使用@ControllerAdvice来进行统一异常处理,@ExceptionHandler(value = RuntimeException.class)来指定捕获的Exception各个类型异常 ,这个异常的处理,是全局的,所有类似的异常,都会跑到这个地方处理。

定义全局异常处理,@ControllerAdvice表示定义全局控制器异常处理,@ExceptionHandler表示针对性异常处理,可对每种异常针对性处理。

@RestControllerAdvice
@Slf4j
public class ControllerAdvice {
    
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public Result systemError(Exception e) {
        log.error(e.getMessage(), e);
        return new Result(0,e.getMessage(),null);
    }


    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.OK)
    public Result illegalParamsExceptionHandler(MethodArgumentNotValidException e) {
        log.error(e.getMessage(), e);
        return validation(e.getBindingResult());
    }

    @ExceptionHandler(HttpMessageNotReadableException.class)
    @ResponseStatus(HttpStatus.OK)
    public Result illegalStateException(HttpMessageNotReadableException e) {
        log.error(e.getMessage(), e);
        return new Result(0,e.getMessage(),null);
    }


    @ResponseBody
    private Result validation(BindingResult bindingResult) {
        List<ObjectError> errors = bindingResult.getAllErrors();
        StringBuilder sb = new StringBuilder("");
        if (!CollectionUtils.isEmpty(errors)) {
            for (ObjectError error : errors) {
                FieldError fieldError = (FieldError) error;
                sb.append(fieldError.getField() + fieldError.getDefaultMessage() + ",");
            }
        }
        return  new Result(0,sb.length()>0?sb.substring(0,sb.length()-1):sb.toString(),null);
    }

  /*  @ResponseBody
    @ExceptionHandler(BindException.class)
    public String exceptionHandler2(BindException exception) {
        BindingResult result = exception.getBindingResult();
        if (result.hasErrors()) {
            return result.getAllErrors().get(0).getDefaultMessage();
        }
        return "请求参数错误!";
    }*/
}

 

 

posted @ 2022-07-12 17:46  jason饼干大怪兽  阅读(199)  评论(0)    收藏  举报