当前位置: 移动技术网 > IT编程>开发语言>Java > SpringBoot 参数校验的方法

SpringBoot 参数校验的方法

2019年07月31日  | 移动技术网IT编程  | 我要评论

introduction

有参数传递的地方都少不了参数校验。在web开发中,前端的参数校验是为了用户体验,后端的参数校验是为了安全。试想一下,如果在controller层中没有经过任何校验的参数通过service层、dao层一路来到了数据库就可能导致严重的后果,最好的结果是查不出数据,严重一点就是报错,如果这些没有被校验的参数中包含了恶意代码,那就可能导致更严重的后果。

这里我们主要介绍在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层参数校验

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

  1. 单个参数校验
  2. 实体类参数校验

单个参数校验

@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;
    }
}

当处理get请求时或只传入少量参数的时候,我们可能不会建一个bean来接收这些参数,就可以像上面这样直接在controller方法的参数中进行校验。

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

这时候在postman输入请求:

http://localhost:8080/getuser?name=allan&age=101

调用方会收到springboot默认的格式报错:

{
    "timestamp": "2019-06-01t04:30:26.882+0000",
    "status": 500,
    "error": "internal server error",
    "message": "getuserstr.age: 不能大于99岁",
    "path": "/getuser"
}

后台会打印如下错误:

javax.validation.constraintviolationexception: getuserstr.age: 不能大于99岁
   at org.springframework.validation.beanvalidation.methodvalidationinterceptor.invoke(methodvalidationinterceptor.java:116)
   at org.springframework.aop.framework.reflectivemethodinvocation.proceed(reflectivemethodinvocation.java:185)
   at org.springframework.aop.framework.cglibaopproxy$dynamicadvisedinterceptor.intercept(cglibaopproxy.java:688)
   at io.shopee.bigdata.penalty.server.controller.pingcontroller$$enhancerbyspringcglib$$232cfd51.getuserstr(<generated>)
   ...

如果有很多使用这种参数验证的controller方法,我们希望在一个地方对constraintviolationexception异常进行统一处理,可以使用统一异常捕获,这需要借助@controlleradvice注解来实现,当然在springboot中我们就用@restcontrolleradvice(内部包含@controlleradvice和@responsebody的特性)

import org.springframework.http.httpstatus;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraintviolation;
import javax.validation.constraintviolationexception;
import javax.validation.validationexception;
import java.util.set;

/**
 * @author pengchengbai
 * @date 2019-06-01 14:09
 */
@restcontrolleradvice
public class globalexceptionhandler {

    @exceptionhandler(validationexception.class)
    @responsestatus(httpstatus.bad_request)
    public string handle(validationexception exception) {
        if(exception instanceof constraintviolationexception){
            constraintviolationexception exs = (constraintviolationexception) exception;

            set<constraintviolation<?>> violations = exs.getconstraintviolations();
            for (constraintviolation<?> item : violations) {
                //打印验证不通过的信息
                system.out.println(item.getmessage());
            }
        }
        return "bad request" ;
    }
}

当参数校验异常的时候,该统一异常处理类在控制台打印信息的同时把bad request的字符串和httpstatus.bad_request所表示的状态码400返回给调用方(用@responsebody注解实现,表示该方法的返回结果直接写入http response body 中)。其中:

  • @controlleradvice:控制器增强,使@exceptionhandler、@initbinder、@modelattribute注解的方法应用到所有的 @requestmapping注解的方法。
  • @exceptionhandler:异常处理器,此注解的作用是当出现其定义的异常时进行处理的方法,此例中处理validationexception异常。

实体类参数校验

当处理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中,我们这里写了一个统一的方法来处理这些结果,通过抛出异常的方式得到globalexceptionhandler的统一处理。

校验模式

在上面的例子中,我们使用bindingresult验证不通过的结果集合,但是通常按顺序验证到第一个字段不符合验证要求时,就可以直接拒绝请求了。这就涉及到两种校验模式的配置:

  1. 普通模式(默认是这个模式): 会校验完所有的属性,然后返回所有的验证失败信息
  2. 快速失败模式: 只要有一个验证失败,则返回
    如果想要配置第二种模式,需要添加如下配置类:
import org.hibernate.validator.hibernatevalidator;
import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import javax.validation.validation;
import javax.validation.validator;
import javax.validation.validatorfactory;

@configuration
public class validatorconf {
    @bean
    public validator validator() {
        validatorfactory validatorfactory = validation.byprovider( hibernatevalidator.class )
                .configure()
                .failfast( true )
                .buildvalidatorfactory();
        validator validator = validatorfactory.getvalidator();

        return validator;
    }
}

参数校验分组

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

  1. 定义表示组别的interface
public interface groupa {
}
  1. @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的参数校验生效。

  1. 在实体类的注解中标记这个哪个组所使用的参数;
@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;
}

级联参数校验

当参数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;

然后再继续在userinfo类中使用注解对每个参数进行校验。

但是我们再回过头来看看,在controller中对实体类进行校验的时候使用的@validated,在这里只能使用@valid,否则会报错。关于这两个注解的具体区别可以参考@valid 和@validated的关系,但是在这里我想说的是使用@valid就没办法对userinfo进行分组校验。这种时候我们就会想,如果能够定义自己的validator就好了,最好能支持分组,像函数一样调用对目标参数进行校验,就像下面的validobject方法一样:

import javax.validation.validator;

@restcontroller
public class pingcontroller {
    @autowired
    private validator validator;

    @postmapping("/setuser")
    public string setuser(@requestbody @validated userinfo user, bindingresult bindingresult) {
        validdata(bindingresult);
        parent parent = user.getparent();
        validobject(parent, validator, groupb.class, default.class);
        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());
        }
    }

    /**
     * 实体类参数有效性验证
     * @param bean 验证的实体对象
     * @param groups 验证组
     * @return 验证成功:返回true;验证失败:将错误信息添加到message中
     */
    public void validobject(object bean, validator validator, class<?> ...groups) {
        set<constraintviolation<object>> constraintviolationset = validator.validate(bean, groups);
        if (!constraintviolationset.isempty()) {
            stringbuilder sb = new stringbuilder();
            for (constraintviolation violation: constraintviolationset) {
                sb.append(violation.getmessage());
            }

            throw new validationexception(sb.tostring());
        }
    }
}


@data
public class parent {
    @notempty(message = "parent name cannot be empty", groups = {groupb.class})
    private string name;

    @email(message = "should be email format")
    private string email;
}

自定义参数校验

虽然jsr303和hibernate validtor 已经提供了很多校验注解,但是当面对复杂参数校验时,还是不能满足我们的要求,这时候我们就需要自定义校验注解。这里我们再回到上面的例子介绍一下自定义参数校验的步骤。private list<@notnull @valid userinfo> parents这种在容器中进行参数校验是bean validation2.0的新特性,假如没有这个特性,我们来试着自定义一个list数组中不能含有null元素的注解。这个过程大概可以分为两步:

  1. 自定义一个用于参数校验的注解,并为该注解指定校验规则的实现类
  2. 实现校验规则的实现类

自定义注解

定义@listnothasnull注解, 用于校验 list 集合中是否有null 元素

@target({elementtype.annotation_type, elementtype.method, elementtype.field})
@retention(retentionpolicy.runtime)
@documented
//此处指定了注解的实现类为listnothasnullvalidatorimpl
@constraint(validatedby = listnothasnullvalidatorimpl.class)
public @interface listnothasnull {

    /**
     * 添加value属性,可以作为校验时的条件,若不需要,可去掉此处定义
     */
    int value() default 0;

    string message() default "list集合中不能含有null元素";

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

    class<? extends payload>[] payload() default {};

    /**
     * 定义list,为了让bean的一个属性上可以添加多套规则
     */
    @target({method, field, annotation_type, constructor, parameter})
    @retention(runtime)
    @documented
    @interface list {
        listnothasnull[] value();
    }
}

注意:message、groups、payload属性都需要定义在参数校验注解中不能缺省

注解实现类

该类需要实现constraintvalidator

import org.springframework.stereotype.service;

import javax.validation.constraintvalidator;
import javax.validation.constraintvalidatorcontext;
import java.util.list;

public class listnothasnullvalidatorimpl implements constraintvalidator<listnothasnull, list> {

    private int value;

    @override
    public void initialize(listnothasnull constraintannotation) {
        //传入value 值,可以在校验中使用
        this.value = constraintannotation.value();
    }

    public boolean isvalid(list list, constraintvalidatorcontext constraintvalidatorcontext) {
        for (object object : list) {
            if (object == null) {
                //如果list集合中含有null元素,校验失败
                return false;
            }
        }
        return true;
    }
}

然后我们就能在之前的例子中使用该注解了:

@notempty
@listnothasnull
private list<@valid userinfo> parents;

其他

difference between @notnull, @notempty, and @notblank

@notnull

不能为null,但是可以为空字符串""

@notempty

不能为null,不能为空字符串"",其本质是charsequence, collection, map, or array的size或者length不能为0

@notblank

a constrained string is valid as long as it’s not null and the trimmed length is greater than zero

@nonnull

@notnull 是 jsr303(bean的校验框架)的注解,用于运行时检查一个属性是否为空,如果为空则不合法。
@nonnull 是jsr 305(缺陷检查框架)的注解,是告诉编译器这个域不可能为空,当代码检查有空值时会给出一个风险警告,目前这个注解只有idea支持。

@valid 注解和描述

参考资料

  1. spring5.0 中的@nonnull
  2. difference between @notnull, @notempty, and @notblank
  3. https://my.oschina.net/u/3773384/blog/1795869
  4. @valid 和@validated的关系

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网