转载

SpringBoot自定义请求参数校验

最近在工作中遇到写一些API,这些API的请求参数非常多,嵌套也非常复杂,如果参数的校验代码全部都手动去实现,写起来真的非常痛苦。正好 Spring 轮子里面有一个 Validation ,这里记录一下怎么使用,以及怎么自定义它的返回结果。

一、Bean Validation基本概念

Bean Validation 是Java中的一项标准,它通过一些 注解 表达了对实体的限制规则。通过提出了一些API和扩展性的规范,这个规范是没有提供具体实现的,希望能够 Constrain once, validate everywhere 。现在它已经发展到了2.0,兼容Java8。

hibernate validation 实现了Bean Validation标准,里面还增加了一些注解,在程序中引入它我们就可以直接使用。

Spring MVC 也支持 Bean Validation ,它对 hibernate validation 进行了二次封装,添加了自动校验,并将校验信息封装进了特定的 BindingResult 类中,在SpringBoot中我们可以添加 implementation('org.springframework.boot:spring-boot-starter-validation') 引入这个库,实现对bean的校验功能。

二、基本用法

gradle dependencies 如下:

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-validation')
    implementation('org.springframework.boot:spring-boot-starter-web')
}

定义一个示例的Bean,例如下面的 User.java

public class User{

    @NotBlank
    @Size(max=10)
    private String name;
    
    private String password;

    public String getName(){
        return name;
    }

    public void setName(String name){
        this.name = name;
    }

    public String getPassword(){
        return password;
    }

    public void setPassword(String password){
        this.password = password;
    }
}

name 属性上,添加 @NotBlank@Size(max=10) 的注解,表示 User 对象的 name 属性不能为字符串且长度不能超过10个字符。

然后我们暂时不添加任何多余的代码,直接写一个 UserController 对外提供一个RESTful的 GET 接口,注意接口的参数用到了 @Validated注解

// UserController.java,省略其他代码
@RestController
public class UserController{

    @RequestMapping(value = "/validation/get", method = RequestMethod.GET)
    public ServiceResponse validateGet(@Validated User user){
        ServiceResponse serviceResponse = new ServiceResponse();
        serviceResponse.setCode(200);
        serviceResponse.setMessage("test");
        return serviceResponse;
    }
}

// ServiceResponse.java,简单包含了code、message字段返回结果。
public class ServiceResponse{

    private int code;
    private String message;
    ... 省略getter、setter ...
}

启动SpringBoot程序,发一个测试请求看一下:

http://127.0.0.1:8080/validation/get?name=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&password=1

返回的结果是,注意此时的 HTTP STATUS CODE = 400

SpringBoot自定义请求参数校验

此时已经可以实现参数的校验了,但是返回的结果不太友好,下面看一下怎么定制返回的消息。在定制返回结果前,先看下一下内置的校验注解有哪些,在这里我不一个个去贴了,写代码的时候根据需要进入到源码里面去看即可。

SpringBoot自定义请求参数校验

三、自定义校验逻辑和返回值

3.1 定制返回码和消息

在第二节,我定义了一个 ServiceResponse ,其实作为一个开放的API,我们希望不论用户传入任何参数,返回的结果都应该是预先定义好的,并且可以在接口文档写明给到调用方的,所以即使发生了校验失败,我们也希望返回一个包含 codemessage 字段

{
    "code": 51000,
    "message": "Param 'name' must be less than 10 characters."
}

的结果,而 HTTP STATUS CODE 一直都是 200

为了实现这个目的,我们加一个全局异常处理方法。

@RestControllerAdvice
public class ServiceExceptionHandler{

    @ExceptionHandler(BindException.class)
    public ServiceResponse handleBindException(BindException ex){
        FieldError fieldError = ex.getFieldError();
        String message = "Param %s error";
        if (fieldError != null && fieldError.getDefaultMessage() != null) {
            message = String.format(fieldError.getDefaultMessage(), fieldError.getField());
        }
        // 生成返回结果
        ServiceResponse errorResult = new ServiceResponse();
        errorResult.setCode(400);
        errorResult.setMessage(message);
        return errorResult;
    }
}

在上面的方法中,我们处理了 BindException ,并获取到了Bean对象出现错误的属性,然后取出它的 defaultMessage ,并包装成统一的 ServiceResponse 返回。由于默认的消息内容是有注解默认的 DefaultMessage 决定的,为了按照自定义的描述返回,在Bean对象的注解上需要手动赋值为我们希望返回的消息内容。

...
@NotBlank(message ="Param 'name' can't be blank.")
@Size(max=10,message ="Param 'name' must be less than 10 characters.")
private String name;
...

这样当 name 参数长度超过10时,就会返回

{
    "code": 51000,
    "message": "Param 'name' must be less than 10 characters."
}

这里的 FieldError fieldError = ex.getFieldError(); 只会随机返回一个出错的属性,如果Bean对象的多个属性都出错了,可以调用 ex.getFieldErrors() 来获得,由此可以看出Spring Validation在参数校验时不会在第一次碰到参数错误时就返回,而是会校验完成所有的参数。

3.2 自定义校验注解

显然除了自带的 NotNullNotBlankSize 等注解,实际业务上还会需要特定的校验规则。

假设我们有一个参数 address ,必须以 Beijing 开头,那我们可以定义一个注解和一个自定义的 Validator

// StartWithValidator.java
public class StartWithValidatorimplements ConstraintValidator<StartWithValidation,String>{

    private String start;

    @Override
    public void initialize(StartWithValidation constraintAnnotation){
        start = constraintAnnotation.start();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context){
        if (!StringUtils.isEmpty(value)) {
            return value.startsWith(start);
        }
        return true;
    }
}


// StartWithValidation.java

@Documented
@Constraint(validatedBy = StartWithValidator.class)
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface StartWithValidation {

    Stringmessage()default "不是正确的性别取值范围";

    Stringstart()default "_";

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

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

    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        GenderValidation[] value();
    }
}

然后在 User.java 中增加一个 address 属性,并给它加上上面这个自定义的注解,这里我们定义了一个可以传入 start 参数的注解,表示应该以什么开头。

...
@StartWithValidation(message = "Param 'address' must be start with 'Beijing'.", start = "Beijing")
private String address;
...

3.3 分组校验

有的时候,我们会有两个不同的接口,但是会使用到同一个 Bean 来作为 VO ,而在不同的接口上,对Bean的校验需求可能不一样,比如接口2需要校验 studentId ,而接口1不需要。那么此时就可以用到校验注解的分组 groups

// User.java
public class User{
    ... 省略其他属性
    // 指明在groups={Student.class}时才需要校验studentId
    @NotNull(groups = {Student.class}, message = "Param 'studentId' must not be null.")
    private Long studentId;

    // 增加Student interface
    public interface Student{
    }
}

// UserController.java,增加了一个/getStudent接口
@RestController
public class UserController{

    @RequestMapping(value = "/validation/get", method = RequestMethod.GET)
    public ServiceResponse validateGet(@Validated User user){
        ServiceResponse serviceResponse = new ServiceResponse();
        serviceResponse.setCode(200);
        serviceResponse.setMessage("test");
        return serviceResponse;
    }

    @RequestMapping(value = "/validation/getStudent", method = RequestMethod.GET)
    public ServiceResponse validateGetStudent(@Validated({User.Student.class})User user){
        ServiceResponse serviceResponse = new ServiceResponse();
        serviceResponse.setCode(200);
        serviceResponse.setMessage("test");
        return serviceResponse;
    }
}

到这里,也可以带一下 ValidValidated 注解的区别,其实看代码注释就知道后者就是对前者的一个扩展,支持了 group 分组的功能。

3.4 一一对应的返回码和消息

其实还有一种比较典型的自定义返回,就是错误码( code )和消息( message )是一一对应的,比如:

  • 51001:字符串长度过长
  • 51002:参数取值过大

这种情况可以结合上面3.1和3.2的方法来实现处理,具体思路是:

  • 自定义校验注解:包含默认的 codemessage 属性。
  • 自定义 Validator :在 isValid 方法中实现校验,并抛出一个自定义的异常 ValiationException ,这个异常包含了注解默认的 codemessage
  • 自定义异常处理方法:在捕获 ValiationException 时,取出 codemessage ,以 ServiceResponse 返回。

如此,只要封装一个注解和Validator的库,那么不同业务Service错误码就可以统一使用了,这种方式比较适合一个团队中对外有一份统一的API文档,包含了统一的错误码,但是却有不同的成员在开发不同的API。

四、小结

其实在实际的工作中,肯定还有更复杂的校验逻辑,但是不一定非要都用注解去实现,注解的实现应该是一个比较简单通用的校验,能够达到复用,减少重复的劳动。而更加复杂的逻辑校验,一定是存在具体业务当中的,最好是在业务代码里面实现校验。

原文  http://unclechen.github.io/2018/12/15/SpringBoot自定义请求参数校验/
正文到此结束
Loading...