集成验证框架实现统一参数校验

前言

在我们实际项目开发过程中,避免不了的就是参数的校验,一般参数的校验,分为如下几种情况;1.前端直接验证;2. 在Controller层单独验证;3. 通过集成验证框架验证;显然3种里面,我们一般建议1+3结合的方式进行参数的校验比较合理和安全。在本章我们将围绕HTTP请求中参数校验的应用

Controller的几种接收参数的方式

在介绍验证框架之前,我们来介绍下通过Controller接收参数的方式;现在大部分都是通过注解的方式进行参数的接收,主流的有如下几种

请求路径参数

  • @PathVariable 获取路径参数。即url/{id}这种形式。
  • @RequestParam 获取查询参数。即url?name=这种形式

代码形式如下:

GET http://localhost:8080/demo/SN20201011021211?name=hank

对应的接收代码如下:

@GetMapping("/demo/{sn}")
public void demo(@PathVariable(name = "sn") String sn, @RequestParam(name = "name") String name) {
    System.out.println("sn="+sn);
    System.out.println("name="+name);
}

Body参数

Body参数一般是POST请求,主要有两种方式

  • 以JSON格式接收可通过@RequestBody获取对应的参数
  • 以form表单形式提交的,暂无注解适配,可直接对象接收

JSON参数接收

例如:添加用户的接口,PostMan 请求信息如下

image.png

对应后端代码1,通过Map接收

@PostMapping(value = "/user/map")
public ResultVO createUser(@RequestBody Map<String,Object> user){
    String name=user.get("name").toString();
    return RV.success(user);
}

对应后代代码2 通过定义的对象接收

@PostMapping(value = "/user")
public ResultVO createUser2(@RequestBody User user){
    System.out.println("User Info:"+user.toString());
    return RV.success(user);
}

FORM 参数接收

form方式提交,在PostMan中对应参数如下

image.png

对应后端代码如下:

@PostMapping(value = "/user/form")
public ResultVO createUser3(User user){
    System.out.println("User Info:"+user.toString());
    return RV.success(user);
}

请求头和Cookie参数

  • @RequestHeader,是直接获取请求头HttpServletRequest对象里面Header中的参数
  • @CookieValue 可以直接获取HttpServletRequest对象里面Cookies中的参数

通过注解的方式获取,分别如下

@GetMapping("demo3")
public void demo3(@RequestHeader(name = "myHeader") String myHeader,
        @CookieValue(name = "myCookie") String myCookie) {
    System.out.println("myHeader=" + myHeader);
    System.out.println("myCookie=" + myCookie);
}

通过硬编码的获取方式为

@GetMapping("/demo3")
public void demo3(HttpServletRequest request) {
    System.out.println(request.getHeader("myHeader"));
    for (Cookie cookie : request.getCookies()) {
        if ("myCookie".equals(cookie.getName())) {
            System.out.println(cookie.getValue());
        }
    }
}

由上可以看出,通过注解的方式可以极大简化编码,使我们的代码变得美观大方,如果通过request对象直接获取,也是可以的,这个主要是看什么样的需求,一般情况下,通过注解的方式基本上能满足我们的绝大部分需求,所以在项目中我们比较推荐直接通过注解的方式获取参数。

文件上传

文件上传主要是基于@RequestParam 结合MultipartFile对象;如下示例;

这里需要注意,前端的传入需要用表单方式提交,并且在Head的Content-Type里面加入,如下信息

Content-Type: application/x-www-form-urlencoded

image.png

对应后端代码如下

@Value("${file.upload.url}")
private String filePath;

@RequestMapping("/upload")
public ResultVO httpUpload(@RequestParam("files") MultipartFile files[]){
    for(int i=0;i<files.length;i++){
        String fileName = files[i].getOriginalFilename();  // 文件名
        File dest = new File(filePath+'/'+fileName);
        if (!dest.getParentFile().exists()) {
            dest.getParentFile().mkdirs();
        }
        try {
            files[i].transferTo(dest);
        } catch (Exception e) {
            log.error("{}",e);
            return RV.result(ErrorCode.UPLOAD_FILE_ERROR,e);
        }
    }
    return RV.success(null);
}

参数校验

在Controller层的参数校验可以分为两种场景,单个参数校验和实体参数校验

单个参数校验

后端代码如下

@RestController
public class ValidateController {

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

在Postman请求如下,可以看到请求后,返回了系统异常,应该是被全局异常拦截了

image.png

我们再看后台打印的日志

image.png

如果我们系统中有很多地方都用到了参数校验,那么我们最好是能够在全局异常拦截处直接拦截ConstraintViolationException异常,并进行统一处理,可以在我们前面章节讲到的,在加了@RestControllerAdvice注解的GlobalException类中加入如下拦截的新方法;由于ConstraintViolationException继承了ValidationException异常类,所以我们可以直接拦截父类异常,直接进行多个不同的校验异常进行处理

@RestControllerAdvice
public class GlobalException {

    /**
     * 描述:表单异常拦截
     * @param [e]
     * @date 2020/11/22
     * @Author Hank
     **/
    @ExceptionHandler(value = ValidationException.class)
    public  ResultVO validationException(ValidationException e){
        Map map=new HashMap();
        if(e instanceof ConstraintViolationException){
            ConstraintViolationException exs = (ConstraintViolationException) e;

            Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
            for (ConstraintViolation<?> item : violations) {
                //打印验证不通过的信息
                System.out.println(item.getMessage());
                map.put(item.getPropertyPath(),item.getMessage());
            }
        }
        return RV.result(ErrorCode.FORM_VALIDATION_ERROR,map) ;
    }
}

同样,再通过Postman请求后,得到如下结果

image.png

实体参数校验

一般我们在传入参数比较少的情况下,就直接用上面的方法进行了验证了,但是,如果我们传入的是一个对象,直接在方法上一个一个属性设置,就显得有点不美观了,因此我们一般都是封装一个接收参数的对象。如下面实例提供的,添加用户接口,我们对用户对象的变量设置了校验规则,如下

@Data
class Person {
    @NotNull(message = "名称不能为空")
    @Max(value = 30, message = "名称不能超过30个字符")
    String name;
    @Max(value = 200, message = "年龄不能超过200岁吧")
    @NotNull(message = "年龄不能为空")
    Integer age;
    @NotNull(message = "地址信息不能为空")
    String address;
}

在Controller类中我们增加添加用户的方法,这里我们需要注意,我们只需要在方法对应的参数Person对象前面加上@Valid注解即可

    /**
     * 描述:添加一个 Person
     * @param [person]
     * @date 2020/11/22
     * @Author Hank
     **/
    @PostMapping(value = "/person")
    public ResultVO<Person> addPerson(@RequestBody @Valid Person person) {
        //此处略过处理业务的代码...
        return RV.success(person);
    }

其实大家可能就会问,那要是参数校验不同过,这里应该是抛出什么异常呢,当我们执行如上代码,设定超出范围的参数后,在后端可以看到异常如下

image.png

可以看到,这里抛出的MethodArgumentNotValidException与上面的异常截然不同;相同道理,我依然在GlobalException类中加入对MethodArgumentNotValidException异常的全局拦截即可

    /**
     * 描述: 方法参数验证异常拦截
     * @param [e]
     * @date 2020/11/22
     * @Author Hank
     **/
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public  ResultVO methodArgumentNotValidException(MethodArgumentNotValidException e){
        List<ObjectError> errors = e.getBindingResult().getAllErrors();
        String[] errMessage=new String[errors.size()];
        for (int i = 0; i < errors.size(); i++) {
            ObjectError error=errors.get(i);
            errMessage[i]=error.getDefaultMessage();
            System.out.println(error.getCode()+":"+error.getDefaultMessage());
        }
        return RV.result(ErrorCode.METHOD_ARGS_VALID_ERROR,errMessage) ;
    }

Validated与Valid区别

  • @Validated: 用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性(字段)上,也无法提示框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证
  • @Valid:用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性(字段)上,提示验证框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。

如上Person对象,如果里面再有一个对象,比如还是Person,在提交的过程中,需要这种嵌套验证,就需要通过使用Valid的嵌套验证功能,即可将代码修改如下;

@Data
class Person {
    @NotNull(message = "名称不能为空")
    @Size(min = 2,max = 30, message = "名称长度限定在2-30之间的长度")
    String name;
    @Max(value = 200, message = "年龄不能超过200岁吧")
    @NotNull(message = "年龄不能为空")
    Integer age;
    @NotNull(message = "地址信息不能为空")
    String address;
    @Valid
    Person p;
}

常用参数校验的注解

这里我们主要介绍在springboot中的几种参数校验方式。常用的用于参数校验的注解如下:

  • @AssertFalse 所注解的元素必须是Boolean类型,且值为false
  • @AssertTrue 所注解的元素必须是Boolean类型,且值为true
  • @DecimalMax 所注解的元素必须是数字,且值小于等于给定的值
  • @DecimalMin 所注解的元素必须是数字,且值大于等于给定的值
  • @Digits 所注解的元素必须是数字,且值必须是指定的位数
  • @Future 所注解的元素必须是将来某个日期
  • @Max 所注解的元素必须是数字,且值小于等于给定的值
  • @Min 所注解的元素必须是数字,且值小于等于给定的值
  • @Range 所注解的元素需在指定范围区间内
  • @NotNull 所注解的元素值不能为null
  • @NotBlank 所注解的元素值有内容
  • @Null 所注解的元素值为null
  • @Past 所注解的元素必须是某个过去的日期
  • @PastOrPresent 所注解的元素必须是过去某个或现在日期
  • @Pattern 所注解的元素必须满足给定的正则表达式
  • @Size 所注解的元素必须是String、集合或数组,且长度大小需保证在给定范围之内
  • @Email 所注解的元素需满足Email格式

 

小结

本章主要围绕两方面内容作了详细的介绍

  • Controller的参数接收方式
    • 请求路径参数
    • Body参数(JSON\FORM\请求头和Cookie参数)
    • 文件上传
  • 参数的校验
    • 单个参数校验
    • 实体参数校验
    • 嵌套参数校验以及Validated与Valid的区别

以上我们分别用示例进行了详细阐述,希望对你后期的开发工作有所帮助;如果你在这方面有不同的实战经验,也欢迎在评论区进行留言。


想要了解更多信息,可关注本公众号(一起学开源);或请长按以下二维码添加助手。将拉你加入社区进行更多交流

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一起学开源

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值