SpringBoot系列之表单参数校验整理

x33g5p2x  于2022-02-07 转载在 Spring  
字(14.5k)|赞(0)|评价(0)|浏览(214)

1、前言

表单的校验在一些对接的接口,要求比较多,使用较多的是Hibernate的表单校验进行JSR-303验证,在springboot项目中,有封装的spring-boot-starter-validation这个starter,也是基于Hibernate的表单校验,下面通过一个RestFul风格的接口来看看

2、环境搭建

开发环境

  • JDK 1.8

  • SpringBoot2.2.1

  • MybatisPlus3.4.3.4

  • Maven 3.2+

  • Mysql5.7.36

  • 开发工具

  • IntelliJ IDEA

  • smartGit

使用阿里云提供的脚手架快速创建项目:
https://start.aliyun.com/bootstrap.html

也可以在idea里,将这个链接复制到Spring Initializr这里,然后创建项目

jdk版本选择jdk8

加上必要的pom配置

表单校验选择Validation

也可以创建一个springboot项目之后,自己在pom配置文件加上配置:

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

3、RestFul例子

下面以一个用户注册的例子,写一些RestFul的api接口

package com.example.validated.model.dto;

import com.example.validated.common.validated.EnumValueValidator;
import com.example.validated.model.enumCls.SexTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.io.Serializable;

@Data
@SuperBuilder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class UserDto implements Serializable {

    private Long id;

    @NotBlank( message = "用户名name必须传!")
    private String name;

    private Integer age;

    @Email( message = "email invalid!")
    @NotBlank(message = "邮箱email必须传!")
    private String email;

    @NotBlank( message = "手机号contactNumber必须传")
    @Pattern(regexp = "1[3|4|5|7|8][0-9]\\d{8}" ,message = "手机号格式不对")
    private String contactNumber;

    @NotBlank(message = "密码password必须传!")
    @Size(min = 6 ,message = "密码password必须6位以上")
    private String password;

    @EnumValueValidator(enumClass = SexTypeEnum.class , enumMethod = "isValueValid" ,message = "性别类型校验有误!")
    private Integer sex;

}

启动校验的一般有两种方法:

  1. @Valid加在请求正文之前,可以加在@RequestBody注解之前
  2. @Validated 是JSR-303 的@Valid的变体,可以支持组校验,什么是组校验?
@PostMapping(value = "/user")
public ResultBean<User> save(@Valid @RequestBody UserDto userDto) {
    User user = BeanUtil.copyProperties(userDto , User.class);
    boolean flag = userService.save(user);
    if (flag) return ResultBean.ok(user);
    return ResultBean.badRequest("新增失败");
}

在postman调用测试接口:

默认返回的异常信息:

{
    "timestamp": "2022-01-12T06:37:45.076+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "NotBlank.userDto.name",
                "NotBlank.name",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "arguments": [
                {
                    "codes": [
                        "userDto.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                }
            ],
            "defaultMessage": "姓名必须传!",
            "objectName": "userDto",
            "field": "name",
            "rejectedValue": "",
            "bindingFailure": false,
            "code": "NotBlank"
        }
    ],
    "message": "Validation failed for object='userDto'. Error count: 1",
    "path": "/api/user"
}

所以可以自己定义一个全局的异常类,捕获MethodArgumentNotValidException异常信息:

package com.example.validated.common.exception;
import com.example.validated.common.rest.ResultBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.util.List;
import java.util.stream.Collectors;

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ResponseBody
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler({Exception.class})
    public ResultBean<?> exception(Exception e) {
        log.error("服务器错误,{}" , e);
        return ResultBean.serverError(e.getMessage());
    }

    @ResponseBody
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public ResultBean<?> exception(MethodArgumentNotValidException e) {
        log.error("参数校验异常,{}" , e.getMessage());
        List<String> res = e.getBindingResult().getAllErrors().stream()
                .map(err -> String.format("%s->%s", ((FieldError) err).getField()  , err.getDefaultMessage()))
                .collect(Collectors.toList());
        return ResultBean.badRequest("参数校验异常" , res);
    }
}

自定义的ResultBean类:

package com.example.validated.common.rest;

import lombok.Data;
import org.springframework.http.HttpStatus;

@Data
public class ResultBean<T> {

	/**
	 * 状态
	 * */
	private int status;
	/**
	 * 描述
	 * */
	private String desc;
	/**
	 * 数据返回
	 * */
	private T data;

	public ResultBean(int status, String desc, T data) {
		this.status = status;
		this.desc = desc;
		this.data = data;
	}

	public ResultBean(T data) {
		this.status = HttpStatus.OK.value();
		this.desc = "处理成功";
		this.data = data;
	}

	public static <T> ResultBean<T> ok(T data) {
		return new ResultBean(data);
	}

	public static <T> ResultBean<T> ok() {
		return new ResultBean(null);
	}

	public static <T> ResultBean<T> badRequest(String desc,T data) {
		return new ResultBean(HttpStatus.BAD_REQUEST.value(), desc, data);
	}

	public static <T> ResultBean<T> badRequest(String desc) {
		return new ResultBean(HttpStatus.BAD_REQUEST.value(), desc, null);
	}

	public static <T> ResultBean serverError(String desc, T data){
		return new ResultBean(HttpStatus.INTERNAL_SERVER_ERROR.value(),"服务器内部异常:"+desc,data);
	}

	public static <T> ResultBean serverError(String desc){
		return new ResultBean(HttpStatus.INTERNAL_SERVER_ERROR.value(),"服务器内部异常:"+desc,null);
	}

}

不自定义ResultBean类,可以加到ResponseEntity类:

@ResponseBody
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler({MethodArgumentNotValidException.class})
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
                                                                  HttpHeaders headers, HttpStatus status, WebRequest request) {

        Map<String, Object> body = new HashMap<>(16);
        body.put("timestamp", new Date());
        body.put("status", status.value());

        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(err -> String.format("%s->%s", ((FieldError) err).getField()  , err.getDefaultMessage()))
                .collect(Collectors.toList());

        body.put("errors", errors);
        return new ResponseEntity<>(body, headers, status);
    }

4、@Valid@Validated区别

在 Spring 中,我们喜欢使用 JSR-303 的@Valid注解进行校验,同时还有@Validated这个注解,两个注解看起来差不多,然后具体有什么差别?现在对比一下

  • 注解地方

  • @Validated:可以用在类型、方法和方法参数上,但是不能用在成员属性上

  • @Valid:可以用在方法、构造函数、方法参数和成员属性

综上其实最明显的区别还是能不能用在成员属性上,@Validated是不支持的,在IDEA里加上去试试,马上会变红提示

  • 分组校验
    组校验,@Valid是不支持的,但是可以使用@Validated实现,举个场景,假如一个接口要进行分阶段同步数据,第一步同步基本的信息,第二步再同步附加信息,但是@Valid是不支持这种分组校验的,下面例子参考https://www.baeldung.com/spring-valid-vs-validated

新增一个基础信息的接口

package com.example.validated.interfaces
// copy from https://github.com/eugenp/tutorials/blob/master/spring-boot-modules/spring-boot-mvc-3/src/main/java/com/baeldung/springvalidation/interfaces/BasicInfo.java
public interface BasicInfo {
    // validation group marker interface

}

所有用户信息接口

package com.example.validated.interfaces

public interface UserInfo {
    // validation group marker interface

}

原来的DTO类,加上groups 进行分组

package com.example.validated.model.dto;
import com.example.validated.common.validated.EnumValueValidator;
import com.example.validated.interfaces.BasicInfo;
import com.example.validated.interfaces.UserInfo;
import com.example.validated.model.enumCls.SexTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.io.Serializable;

@Data
@SuperBuilder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class UserGroupDto implements Serializable {

    private Long id;

    @NotBlank( message = "用户名name必须传!" ,groups = {BasicInfo.class , UserInfo.class})
    private String name;

    @NotBlank(message = "密码password必须传!")
    @Size(min = 6 ,message = "密码password必须6位以上" ,groups = {BasicInfo.class , UserInfo.class})
    private String password;

    private Integer age;

    @Email( message = "email invalid!",groups = {UserInfo.class})
    @NotBlank(message = "邮箱email必须传!" ,groups = {UserInfo.class})
    private String email;

    @NotBlank( message = "手机号contactNumber必须传" ,groups = {UserInfo.class})
    @Pattern(regexp = "1[3|4|5|7|8][0-9]\\d{8}" ,message = "手机号格式不对",groups = {UserInfo.class})
    private String contactNumber;

    @EnumValueValidator(enumClass = SexTypeEnum.class , enumMethod = "isValueValid" ,message = "性别类型校验有误!",groups = {UserInfo.class})
    private Integer sex;

}

@Validated只指定BasicInfo这个组的校验,也只校验用户名和密码,其它的不校验,@Validated(BasicInfo.class)

@PostMapping(value = "/user/basic")
    public ResultBean<User> saveBasic(@Validated(BasicInfo.class) @RequestBody UserGroupDto userDto) {
        User user = BeanUtil.copyProperties(userDto , User.class);
        boolean flag = userService.save(user);
        if (flag) return ResultBean.ok(user);
        return ResultBean.badRequest("新增失败");
    }

先同步基本信息数据,邮箱等等信息没填,也可以不报错

  • 嵌套校验
    嵌套校验?下面还是举个例子说明,原来的userDto类加上一个地址信息列表,代码如下
@NotNull(message = "地址必须填!" )
@Size(min = 1 , message = "地址至少要有一个")
private List<AddressDto> address;

AddressDto 也要校验

package com.example.validated.model.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import org.springframework.validation.annotation.Validated;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;

@Data
@SuperBuilder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class AddressDto {

    @NotBlank(message = "邮政编码必须传!")
    private String postalCode;

    @NotBlank(message = "地址描述必须传!")
    private String addressDesc;

}

ok,测试例子,发现仅仅@Valid @RequestBody UserDto userDto,将@Valid或者@Validated加在@RequestBody只校验地址非空而且必须有一个,对于AddressDTO里的具体校验都不生效

然后,需要怎么办?只要加上@Valid即可,加上@Validated可以?前面的学习知道是不可以的,所以@Validated不支持这种嵌套校验的

@NotNull(message = "地址必须填!" )
@Size(min = 1 , message = "地址至少要有一个")
@Valid
private List<AddressDto> address;

综上所述,@Valid支持加在成员属性上,所以可以支持嵌套校验,不支持组校验;@Validated不支持加在成员属性上,所以不支持嵌套校验,但是可以支持组校验

5、常用校验注解

这里资料摘录自https://blog.csdn.net/ln840434235/article/details/103878887,稍作修改

附录常用注解:

  • @Null 被注释的元素必须为 null
  • @NotNull 被注释的元素不能为 null
  • @AssertTrue 被注释的元素必须为 true
  • @AssertFalse 被注释的元素必须为 false
  • @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @Size(max=, min=) 被注释的元素的大小必须在指定的范围内
  • @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
  • @Past 被注释的元素必须是一个过去的日期
  • @Future 被注释的元素必须是一个将来的日期
  • @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式

Hibernate Validator提供的校验注解:

  • @NotBlank(message =) 验证字符串非null而且非空字符串
  • @Email 被注释的元素必须是电子邮箱地址
  • @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
  • @NotEmpty 被注释的字符串的必须非null,可以为空字符串
  • @Range(min=,max=,message=) 被注释的元素必须在合适的范围内
  • @AssertFalse 校验false
  • @AssertTrue 校验true
  • @DecimalMax(value=,inclusive=) 小于等于value,
    inclusive=true,是小于等于
  • @DecimalMin(value=,inclusive=) 与上类似
  • @Max(value=) 小于等于value
  • @Min(value=) 大于等于value
  • @NotNull 检查Null
  • @Past 检查日期
  • @Pattern(regex=,flag=) 正则
  • @Size(min=, max=) 字符串,集合,map限制大小
  • @Valid 对po实体类进行校验

6、自定义校验注解

前面的例子,都是使用api提供的注解校验,有一些特殊场景是不是可以自己写一个校验注解?下面以一个枚举数据的校验为例

性别类型编码枚举类:

package com.example.validated.model.enumCls;
import org.springframework.util.StringUtils;
public enum  SexTypeEnum {

    MAN(1,"男"),
    WOMAN(2, "女");

    private int code;
    private String desc;

    SexTypeEnum(int code , String desc) {
        this.code = code;
        this.desc = desc;
    }

    public Integer getCode() {
        return code;
    }

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

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public static boolean isValueValid(Integer value) {
        if(!StringUtils.isEmpty(value)){
            for (SexTypeEnum enumObj : SexTypeEnum.values()) {
                if (enumObj.getCode().equals(value)) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }

}

主要通过ConstraintValidator重写对应校验方法

package com.example.validated.common.validated;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValueValidator.Validator.class)
public @interface EnumValueValidator {

    Logger log = LoggerFactory.getLogger(EnumValueValidator.class);

    String message() default "参数有误";

    Class<? extends Enum<?>> enumClass();

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

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

    String enumMethod();
    
    class Validator implements ConstraintValidator<EnumValueValidator , Object> {
        private Class<? extends Enum<?>> enumClass;
        private String enumMethod;

        @Override
        public void initialize(EnumValueValidator constraintAnnotation) {
            enumMethod = constraintAnnotation.enumMethod();
            enumClass = constraintAnnotation.enumClass();
        }
        @Override
        public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
          // 值没传的情况,直接返回true
            if (StringUtils.isEmpty(o)) return Boolean.TRUE;
            if (enumClass == null || StringUtils.isEmpty(enumMethod)) return Boolean.TRUE;
            Class<?> vclass = o.getClass();
            try {
            // 反射机制获取具体的校验方法
                Method method = enumClass.getMethod(enumMethod,vclass);
                if (!Boolean.TYPE.equals(method.getReturnType()) &&
                        !Boolean.class.equals(method.getReturnType())) {
                    throw new RuntimeException("校验方法不是布尔类型!");
                }
                if (!Modifier.isStatic(method.getModifiers())) {
                    throw new RuntimeException("校验方法不是静态方法!");
                }
                method.setAccessible(true);
                // 调用具体的方法
                Boolean res = (Boolean) method.invoke(null,o);
                return res != null ? res : false;
            } catch (NoSuchMethodException e) {
                log.error("NoSuchMethodException:{}" ,e);
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                log.error("IllegalAccessException:{}" ,e);
                throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
                log.error("InvocationTargetException:{}" ,e);
                throw new RuntimeException(e);
            }
        }
    }

}

DTO类加上自定义的注解校验

@EnumValueValidator(enumClass = SexTypeEnum.class , enumMethod = "isValueValid" ,message = "性别类型校验有误!")
    private Integer sex;

本博客可以在GitHub找到下载链接

参考资料

相关文章

微信公众号

最新文章

更多