不推荐的校验

通过if判断一个个的判断必须的参数,这样的写法很臃肿。

  1. @Slf4j
  2. @RestController
  3. @RequestMapping("/user")
  4. public class UserController {
  5. @PostMapping("addUser")
  6. public ResponseEntity<String> add(@Valid @RequestBody UserParam userParam) {
  7. if (StringUtils.isBlank(userParam.getUserName())){
  8. ResponseEntity.badRequest().body("userName不能为空");
  9. }
  10. if (StringUtils.isBlank(userParam.getEmail())){
  11. ResponseEntity.badRequest().body("email不能为空");
  12. }
  13. if (StringUtils.isBlank(userParam.getNickName())){
  14. ResponseEntity.badRequest().body("nickName不能为空");
  15. }
  16. //……
  17. return ResponseEntity.ok("success");
  18. }
  19. }

优雅的校验

Java开者在Java API规范 (JSR303) 定义了Bean校验的标准 validation-api ,但没有提供实现。

hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等。

Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。

接下来,我们以springboot项目为例,介绍Spring Validation的使用。

  1. 导入依赖
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  1. 在入参映射的实体vo上添加注解
@Data
public class UserParam implements Serializable {
    private static final long serialVersionUID = 1L;

    @NotEmpty(message = "userName不能为空")
    private String userName;

    @NotEmpty(message = "email不能为空")
    @Email(message = "email格式异常,请检查")
    private String email;

    @NotEmpty(message = "nickName不能为空")
    @Length(min = 1, max = 10, message = "nick name should be 1-10")
    private String nickName;

    @NotNull(message = "sex不能为空")
    @Range(min = 0, max = 1, message = "sex 值是 0-1")
    private int sex;
}
  1. 编写controller
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("addUser")
    public ResponseEntity<String> add(@Valid @RequestBody UserParam userParam, BindingResult bindingResult) {
       //如果验证失败。取出
       if (bindingResult.hasErrors()) {
           List<ObjectError> errors = bindingResult.getAllErrors();
           errors.forEach(p -> {
               FieldError fieldError = (FieldError) p;
               log.error("Invalid Parameter : object - {},field - {},errorMessage - {}", fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage());
           });
           return ResponseEntity.badRequest().body(JSONUtil.toJsonStr(errors));
       }
        return ResponseEntity.ok("success");
    }
}

在要验证的参数类类型前添加@Valid表示开启验证。若果验证不通过,我们可以通过BindingResult取出异常返回给请求方。如果不在最后一项声明BindingResult,会被无情地抛错。

参数异常统一处理

通过上方可以看出,这种方式可以很优雅的解决了验证参数这一操作。但是controller中的方法中,我们每个方法都要编写固定的取出异常的代码,这样不太好,接下来我们通过全局异常进行优化。

  1. 在上方的基础之上添加全局异常类
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 1、处理 form data方式调用接口校验失败抛出的异常
    @ExceptionHandler(BindException.class)
    public ResultBody bindExceptionHandler(BindException e) {
        List<FieldError> fieldErrors = e.getFieldErrors();
        List<String> collect = fieldErrors.stream().map(o -> o.getDefaultMessage()).collect(Collectors.toList());
        return new ResultBody("500","验证错误",collect);
    }

    // 2、处理 json 请求体调用接口校验失败抛出的异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultBody handleError(MethodArgumentNotValidException e){
        log.info("GlobalExceptionHandler-handleError-e:{}",e);
        BindingResult bindingResult = e.getBindingResult();
        List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        List<String> collect = fieldErrors.stream().map(o -> o.getDefaultMessage()).collect(Collectors.toList());
        return new ResultBody("500","验证错误",collect);
    }
}
  1. 调整controller
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("addUser")
    public ResponseEntity<String> add(@Valid @RequestBody UserParam userParam) {
        return ResponseEntity.ok("success");
    }
}

删除BindingResult参数和内部取值逻辑,统一放到全局异常中进行处理。

分组校验

那么假设Controller有两个请求一个是添加数据,一个是修改用户数据的,需要在UserParam类上,添加一个userId(修改时需要),而在添加时不需要的,那么我们可以通过分组校验,去控制修改时,不能为空。

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("addUser")
    public ResponseEntity<String> addUser(@RequestBody UserParam userParam) {
        return ResponseEntity.ok("success");
    }

    @PostMapping("editUser")
    public ResponseEntity<String> editUser(@RequestBody UserParam userParam) {
        return ResponseEntity.ok("success");
    }
}
  1. 先定义两个分组
//添加分组
public interface AddValidationGroup {
}
//修改分组
public interface EditValidationGroup {
}
  1. 在UserParam添加userId
@Data
public class UserParam implements Serializable {
    @NotEmpty(message = "userId不能为空",groups = EditValidationGroup.class)
    private String userId;
    //……
}
  1. Controller中不同请求使用不同分组
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("addUser")
    public ResponseEntity<String> addUser(@Validated(AddValidationGroup.class) @RequestBody UserParam userParam) {
        return ResponseEntity.ok("success");
    }

    @PostMapping("editUser")
    public ResponseEntity<String> editUser(@Validated(EditValidationGroup.class) @RequestBody UserParam userParam) {
        return ResponseEntity.ok("success");
    }
}

:::warning 这里的开启验证注解一定要使用@Validated而不是@Valid

:::

测试结果:

请求editUser返回:
{"code":"500","msg":"验证错误","data":["userId不能为空"]}
请求addUser返回:
success

递归校验

如果在A类中引入了B类,而B类中的属性也需要校验,这就是递归校验。其实很简单,我们只需要在A类中声明的B类就可以自动进行递归校验。

public class A{
    @Valid
    public B b;
}

public class B{
    @NotEmpty(message = "id不能为空")
    private String id;
}

自定义校验

Spring Validation允许用户自定义校验,实现方式也很简单,分两步:

  • 自定义校验注解
  • 编写校验实现
  1. 编写注解
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {HaveNoBlankValidator.class})// 标明由哪个类执行校验逻辑
public @interface HaveNoBlank {

    // 校验出错时默认返回的消息
    String message() default "字符串中不能含有空格";

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

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

    /**
     * 同一个元素上指定多个该注解时使用
     */
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        NotBlank[] value();
    }
}
  1. 编写注解实现
public class HaveNoBlankValidator implements ConstraintValidator<HaveNoBlank, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // null 不做检验
        if (value == null) {
            return true;
        }
        // 空 校验失败
        if (value.contains(" ")) {
            return false;
        }
        // 校验成功
        return true;
    }
}
  1. 使用定义的HaveNoBlank注解

只需要在相应字段添加即可,使用起来和内置的注解并无任何差异

@Data
public class UserParam implements Serializable {
    private static final long serialVersionUID = 1L;

    @HaveNoBlank(message = "userName不能为空")
    private String userName;

    //……
}
  1. 测试
@SpringBootTest
public class ValidationTest {

    @Test
    public void addUser(){
        UserParam userParam = new UserParam();
        userParam.setUserName(" ");
        userParam.setEmail("test@qq.coms");
        userParam.setNickName("test");
        userParam.setSex(1);
        String bodyStr = JSONUtil.toJsonStr(userParam);
        System.out.println(bodyStr);
        String result = HttpUtil.post("http://localhost:8001/user/addUser", bodyStr);
        System.out.println(result);
    }
}

常用注解

JSR303/JSR-349 : JSR303是一项标准,只提供规范不提供实现,规定一些校验规范即校验注解。JSR-349是其的升级版本,添加了一些新特性

@AssertFalse 被注释的元素只能为false
@AssertTrue 被注释的元素只能为true
@DecimalMax 被注释的元素必须小于或等于{value}
@DecimalMin 被注释的元素必须大于或等于{value}
@Digits 被注释的元素数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
@Email 被注释的元素不是一个合法的电子邮件地址
@Future 被注释的元素需要是一个将来的时间
@FutureOrPresent 被注释的元素需要是一个将来或现在的时间
@Max 被注释的元素最大不能超过{value}
@Min 被注释的元素最小不能小于{value}
@Negative 被注释的元素必须是负数
@NegativeOrZero 被注释的元素必须是负数或零
@NotBlank 被注释的元素不能为空
@NotEmpty 被注释的元素不能为空
@NotNull 被注释的元素不能为null
@Null 被注释的元素必须为null
@Past 被注释的元素需要是一个过去的时间
@PastOrPresent 被注释的元素需要是一个过去或现在的时间
@Pattern 被注释的元素需要匹配正则表达式”{regexp}”
@Positive 被注释的元素必须是正数
@PositiveOrZero 被注释的元素必须是正数或零
@Size 被注释的元素个数必须在{min}和{max}之间

hibernate validation :hibernate validation是对这个规范的实现,并增加了一些其他校验注解,如@Email,@Length,@Range等等

@CreditCardNumber 被注释的元素不合法的信用卡号码
@Currency 被注释的元素不合法的货币 (必须是{value}其中之一)
@EAN 被注释的元素不合法的{type}条形码
@Email 被注释的元素不是一个合法的电子邮件地址 (已过期)
@Length 被注释的元素长度需要在{min}和{max}之间
@CodePointLength 被注释的元素长度需要在{min}和{max}之间
@LuhnCheck 被注释的元素${validatedValue}的校验码不合法, Luhn模10校验和不匹配
@Mod10Check 被注释的元素${validatedValue}的校验码不合法, 模10校验和不匹配
@Mod11Check 被注释的元素${validatedValue}的校验码不合法, 模11校验和不匹配
@ModCheck 被注释的元素${validatedValue}的校验码不合法, ${modType}校验和不匹配 (已过期)
@NotBlank 被注释的元素不能为空 (已过期)
@NotEmpty 被注释的元素不能为空 (已过期)
@ParametersScriptAssert 被注释的元素执行脚本表达式”{script}”没有返回期望结果
@Range 被注释的元素需要在{min}和{max}之间
@SafeHtml 被注释的元素可能有不安全的HTML内容
@ScriptAssert 被注释的元素执行脚本表达式”{script}”没有返回期望结果
@URL 被注释的元素需要是一个合法的URL

spring validation :spring validation对hibernate validation进行了二次封装,在springmvc模块中添加了自动校验,并将校验信息封装进了特定的类中

@Validate和@Valid什么区别

在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:

分组

  • @Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,这个网上也有资料,不详述。@Valid:作为标准JSR-303规范,还没有吸收分组的功能。

注解地方

  • @Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
  • @Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上

嵌套类型

  • 比如本文例子中的address是user的一个嵌套属性, 只能用@Valid

公用代码

controller接收的入参实体

@Data
public class UserParam implements Serializable {
    private static final long serialVersionUID = 1L;
    private String userName;
    private String email;
    private String phone;
    private int sex;
}

统一返回VO

public class ResultBody {
    private String code;
    private String msg;
    private Object data;

    public ResultBody(String code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public ResultBody() {
    }

    public ResultBody(String code,String msg) {
        this.code = code;
        this.msg = msg;
        this.data = null;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

使用的第三方模块

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.1.1</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>