Spring Boot 2 中的参数校验 spring-boot-starter-validation/Hibernate Validator

Spring Boot 2 中的参数校验 spring-boot-starter-validation/Hibernate Validator

Validation in Spring Boot

在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格式

一、添加依赖

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

这个starter依赖的是Hibernate Validator。

二、实体类参数校验

(一)实体类上加上注解

import lombok.Data;

import javax.validation.constraints.*;
import java.io.Serializable;

/**
 * @author chushiyan
 * @email  Y2h1c2hpeWFuMDQxNUAxNjMuY29t(base64)
 * @description
 */
@Data
public class User implements Serializable {

    private String id;

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

    @Min(value = 10, message = "年龄必须大于10")
    @Max(value = 150, message = "年龄必须小于150")
    private Integer age;

    @Email(message = "邮箱格式不正确")
    private String email;
}

(二)Controller中加上注解

在controller中使用@Valid 或者@Validated 注解校验

import com.chushiyan.validation_tutorial.entity.Result;
import com.chushiyan.validation_tutorial.pojo.User;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

/**
 * @author chushiyan
 * @email  Y2h1c2hpeWFuMDQxNUAxNjMuY29t(base64)
 * @description
 */
@RestController
@RequestMapping("/user")
public class UserController  {

    @PostMapping
    public Result test(@Valid  @RequestBody User user){
        System.out.println(user);
        return new Result(true,200,"");
    }
}

(三)测试

使用postman发送POST请求:http://localhost:10000/user

{
    "age":120,
    "email":"chushiyan"
}

控制台打印:

WARN 12476 --- [io-10000-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.chushiyan.validation_tutorial.entity.Result com.chushiyan.validation_tutorial.controller.UserController.test(com.chushiyan.validation_tutorial.pojo.User) with 2 errors: [Field error in object 'user' on field 'name': rejected value [null]; codes [NotNull.user.name,NotNull.name,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; arguments []; default message [name]]; default message [姓名不能为空]] [Field error in object 'user' on field 'email': rejected value [chushiyan]; codes [Email.user.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@20195c7a,.*]; default message [邮箱格式不正确]] ]

响应的数据:

{
    "timestamp": "2019-12-03T09:14:00.759+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "NotNull.user.name",
                "NotNull.name",
                "NotNull.java.lang.String",
                "NotNull"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                }
            ],
            "defaultMessage": "姓名不能为空",
            "objectName": "user",
            "field": "name",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotNull"
        },
        {
            "codes": [
                "Email.user.email",
                "Email.email",
                "Email.java.lang.String",
                "Email"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.email",
                        "email"
                    ],
                    "arguments": null,
                    "defaultMessage": "email",
                    "code": "email"
                },
                [],
                {
                    "arguments": null,
                    "defaultMessage": ".*",
                    "codes": [
                        ".*"
                    ]
                }
            ],
            "defaultMessage": "邮箱格式不正确",
            "objectName": "user",
            "field": "email",
            "rejectedValue": "chushiyan",
            "bindingFailure": false,
            "code": "Email"
        }
    ],
    "message": "Validation failed for object='user'. Error count: 2",
    "trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.chushiyan.validation_tutorial.entity.Result com.chushiyan.validation_tutorial.controller.UserController.test(com.chushiyan.validation_tutorial.pojo.User) with 2 errors: [Field error in object 'user' on field 'name': rejected value [null]; codes [NotNull.user.name,NotNull.name,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; arguments []; default message [name]]; default message [姓名不能为空]] [Field error in object 'user' on field 'email': rejected value [chushiyan]; codes [Email.user.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@20195c7a,.*]; default message [邮箱格式不正确]] \r\n\tat  (博主进行了省略......)",
    "path": "/user"
}

(四)全局处理异常

上面响应的错误肯定是不够友好的,所以需要进行异常处理。这里定义一个全局处理函数

import com.chushiyan.validation_tutorial.entity.Result;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author chushiyan
 * @email  Y2h1c2hpeWFuMDQxNUAxNjMuY29t(base64)
 * @description
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 处理所有校验失败的异常(MethodArgumentNotValidException异常)
     *
     * @param ex
     * @return
     */
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    // 设置响应状态码为400
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result handleBindGetException(MethodArgumentNotValidException ex) {

        Map<String, Object> body = new LinkedHashMap<String, Object>();
        body.put("timestamp", new Date());

        // 获取所有异常
        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(x -> x.getDefaultMessage())
                .collect(Collectors.toList());
        body.put("errors", errors);
        return new Result(false, 20001, "提交的数据校验失败", body);
    }
}

(五)再次测试

使用postman发送POST请求:http://localhost:10000/user

{
    "age":120,
    "email":"chushiyan"
}

响应的json数据:

{
    "flag": false,
    "code": 20001,
    "message": "提交的数据校验失败",
    "data": {
        "timestamp": "2019-12-03T09:35:02.815+0000",
        "errors": [
            "邮箱格式不正确",
            "姓名不能为空"
        ]
    }
}

三、单个参数校验

(一)直接在参数前加上校验注解:

package com.chushiyan.validation_tutorial.controller;

import com.chushiyan.validation_tutorial.entity.Result;
import com.chushiyan.validation_tutorial.pojo.User;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;

/**
 * @author chushiyan
 * @email  Y2h1c2hpeWFuMDQxNUAxNjMuY29t(base64)
 * @description
 */
@RestController
@RequestMapping("/user")
@Validated
public class UserController  {

    @GetMapping
    public Result test2(@NotNull(message = "name不能为空")  String name){
        System.out.println(name);
        return new Result(true,200,"");
    }
}

注意:需要在类上添加@Validated注解,否则不会校验。

(二)全局处理函数

import com.chushiyan.validation_tutorial.entity.Result;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author chushiyan
 * @email  Y2h1c2hpeWFuMDQxNUAxNjMuY29t(base64)
 * @description
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理所有参数校验时抛出的异常
     *
     * @param ex
     * @return
     */
    @ExceptionHandler(value = ValidationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result handleBindException(ValidationException ex) {

        Map<String, Object> body = new LinkedHashMap<String, Object>();
        body.put("timestamp", new Date());

        // 获取所有异常
        List<String> errors = new LinkedList<String>();
        if(ex instanceof ConstraintViolationException){
            ConstraintViolationException exs = (ConstraintViolationException) ex;
            Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
            for (ConstraintViolation<?> item : violations) {
                errors.add(item.getMessage());
            }
        }
        body.put("errors", errors);
        return new Result(true, 20001, "提交的参数校验失败", body);
    }
}

(二)测试

postman 测试http://localhost:10000/user GET

{
    "flag": true,
    "code": 20001,
    "message": "提交的参数校验失败",
    "data": {
        "timestamp": "2019-12-03T09:58:45.212+0000",
        "errors": [
            "name不能为空"
        ]
    }
}

四、参数校验分组

在实际开发中经常会遇到这种情况:添加用户时,id是由后端生成的,不需要校验id是否为空,但是修改用户时就需要校验id是否为空。如果在接收参数的User实体类的id属性上添加NotNull,显然无法实现。这时候就可以定义分组,在需要校验id的时候校验,不需要的时候不校验。

(一)定义表示组别的接口类

package com.chushiyan.validation_tutorial.validate;
public interface GroupA {
}

(二)在实体类的注解中标记id使用上面定义的组

给id属性添加分组:

package com.chushiyan.validation_tutorial.pojo;

import com.chushiyan.validation_tutorial.validate.GroupA;
import lombok.Data;

import javax.validation.constraints.*;
import java.io.Serializable;

/**
 * @author chushiyan
 * @email  Y2h1c2hpeWFuMDQxNUAxNjMuY29t(base64)
 * @description
 */
@Data
public class User implements Serializable {

    @NotNull(groups = GroupA.class, message = "id不能为空")
    private String id;

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

    @Min(value = 10, message = "年龄必须大于10")
    @Max(value = 150, message = "年龄必须小于150")
    private Integer age;

    @Email(message = "邮箱格式不正确")
    private String email;
}

(三)在controller中使用@Validated指定使用哪个组

    @PostMapping
    public Result add(@Validated @RequestBody User user) {
        return new Result(true, 200, "增加用户成功");
    }

    @PutMapping("/update")
    // 指定GroupA,这样就会校验id属性是否为空
    // 注意:还得必须添加Default.class,否则不会执行其他的校验(如我们案例中的@Email)
    public Result update(@Validated({GroupA.class, Default.class}) @RequestBody User user) {
        return new Result(true, 200, "修改用户成功");
    }

推荐阅读更多精彩内容