简介

为什么要用

传统的表单校验需要使用大量的ifelse,这样虽然可以实现校验的功能,但是确实代码的可读性非常的差,而且,如下图:

使用

简单使用

引入 maven 依赖

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

创建验证对象

import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import java.io.Serializable;

@Data
public class TestForm {

@NotNull(message = "日期不能为空")
private Date Date;

@NotBlank(message = "进出不能为空")
private String io;

@NotNull(message = "类型不能为空")
private Integer type;
}

使用@Valid校验, 并将结果存在BindingResult

public R test(@Valid @RequestBody TestForm test, BindingResult bindingResult) {
if(bindingResult.hasErrors()){
for (ObjectError objectError: bindingResult.getAllErrors()) {
return R.error(objectError.getDefaultMessage());
}
}
return R.ok();
}

内部类校验

在要校验的属性上加上 @Valid 注解

import lombok.Data;
import javax.validation.Valid;
import java.util.Date;
import java.util.List;

@Data
public class UserVO {


/**
* 男性用户
*/
@Valid
private List<User> mans;

/**
* 女性用户
*/
@Valid
private List<User> womans;

@Data
public static class UserVO {

/**
* id
*/
private Long id;

/**
* 姓名
*/
@NotBlank(message = "姓名不能为空")
private String name;
}
}

spring 统一异常处理

全局异常拦截器

import cn.hutool.core.util.StrUtil;
import com.itran.fgoc.common.core.api.Response;
import com.itran.fgoc.common.core.api.ResultCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;

@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class MyGlobalExceptionHandler {

@ExceptionHandler(value = ApiException.class)
public Response handle(ApiException e) {
if (e.getErrorCode() != null) {
if(StrUtil.isNotBlank(e.getMsg())){
log.info(e.getMsg(), e);
return Response.failed(e.getErrorCode(), e.getMsg());
}
return Response.failed(e.getErrorCode());
}
log.info(e.getMessage(), e);
return Response.failed(e.getMessage());
}

@ExceptionHandler(value = BindException.class)
public Response validExceptionHandler(BindException e) {
log.info(e.getMessage(), e);
FieldError fieldError = e.getBindingResult().getFieldError();
return Response.failed(ResultCode.VALIDATE_FAILED, fieldError.getDefaultMessage());
}

@ExceptionHandler(value = ConstraintViolationException.class)
public Response constraintViolationExceptionHandler(ConstraintViolationException e) {
log.info(e.getMessage(), e);
return Response.failed(ResultCode.VALIDATE_FAILED, e.getConstraintViolations().iterator().next().getMessage());
}

@ExceptionHandler(value = {MethodArgumentNotValidException.class})
public Response methodArgumentNotValidHandler(MethodArgumentNotValidException e) {
log.info(e.getMessage(), e);
FieldError fieldError = e.getBindingResult().getFieldError();
return Response.failed(ResultCode.VALIDATE_FAILED, fieldError.getDefaultMessage());
}

@ExceptionHandler(value = {DuplicateKeyException.class})
public Response handleDuplicateKeyException(DuplicateKeyException e) {
log.error(e.getMessage(), e);
return Response.failed(ResultCode.DUPLICATE_KEY);
}

@ExceptionHandler(value = {Exception.class})
public Response handle(Exception e) {
log.error(e.getMessage(), e);
return Response.failed(ResultCode.FAILED);
}

}

业务统一异常

import com.itran.fgoc.common.core.api.IErrorCode;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiException extends RuntimeException {
/**
* 异常码
*/
private IErrorCode errorCode;

/**
* 自定义错误描述
*/
private String msg;

/**
* Instantiates a new Api exception.
*
* @param iErrorCode the error code
*/
public ApiException(IErrorCode iErrorCode){
this.errorCode = iErrorCode;
}
}

统一异常码接口

public interface IErrorCode {
/**
* 异常编码
* @return
*/
long getCode();

/**
* 异常信息
* @return
*/
String getMessage();
}

统一异常码实现类

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public enum ResultCode implements IErrorCode {

SUCCESS(200, "操作成功"),
FAILED(500, "操作失败"),
UNAUTHORIZED(401, "暂未登录或token已经过期"),
VALIDATE_FAILED(402, "参数检验失败"),
FORBIDDEN(403, "没有相关权限"),
LOGIN_FAILED(405, "登录失败,帐号或密码错误"),
LOGIN_CODE_TIMEOUT(405, "登录失败,验证码已过期"),
LOGIN_CODE_FAILED(405, "登录失败,验证码输入错误"),
DATA_IS_NULL(501, "操作的数据异常"),
REMOTE_CALL_FAILED(502, "远程调用失败"),
INSERT_ERROR(5000,"插入数据失败!"),
UPDATE_ERROR(5001,"修改数据失败!"),
DELETE_ERROR(5002,"删除数据失败!"),
DUPLICATE_KEY(5003,"数据已存在!"),

// 1001 用户相关
CODE_1001001(1001001, "用户不存在"),
CODE_1001002(1001002, "用户名不能为空"),

// 1002 权限相关
CODE_1002001(1002001, "无操作权限"),
;
private long code;
private String message;

}

自定义表单校验

注解

import com.itran.fgoc.common.core.var.NullVar;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

/**
* 校验字段是否是枚举中,null值不校验
* 校验顺序:intEnumValues -> longEnumValues -> stringEnumValues -> enumClass
*/
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Enum.List.class)
@Documented
@Constraint(
validatedBy = EnumValidator.class
)
public @interface Enum {
String message() default "枚举字段不正确";

Class<?> enumClass() default NullVar.class;

int[] intEnumValues() default {};

long[] longEnumValues() default {};

String[] stringEnumValues() default {};

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

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

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface List {
Enum[] value();
}
}

注解解析器

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ReflectUtil;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;

public class EnumValidator implements ConstraintValidator<Enum, Object> {

private Object[] enumValues;
private int[] intEnumValues;
private long[] longEnumValues;
private String[] stringEnumValues;

public EnumValidator() {
}

@Override
public void initialize(Enum constraintAnnotation) {
enumValues = ReflectUtil.getFieldsValue(constraintAnnotation.enumClass());
intEnumValues = constraintAnnotation.intEnumValues();
longEnumValues = constraintAnnotation.longEnumValues();
stringEnumValues = constraintAnnotation.stringEnumValues();
}

public boolean isValid(Object object, ConstraintValidatorContext constraintValidatorContext) {
if(object == null){
return true;
}
if(!ArrayUtil.isEmpty(intEnumValues)){
return ArrayUtil.contains(intEnumValues, (int)object);
}
if(!ArrayUtil.isEmpty(longEnumValues)){
return ArrayUtil.contains(longEnumValues, (long)object);
}
if(!ArrayUtil.isEmpty(stringEnumValues)){
return ArrayUtil.contains(stringEnumValues, (long)object);
}
return Arrays.asList(enumValues).contains(object);
}
}

枚举示例

import cn.hutool.core.map.MapUtil;
import java.util.Map;

public interface ContainerTypeVar{

/**
* 轮廓
*/
interface Contour {
int RECTANGULAR_SOLID = 0;
int LOWER_LEFT_TRAPEZOID = 1;
int UPPER_LEFT_TRAPEZOID = 2;
int LOWER_TRAPEZOID = 3;
int UPPER_TRAPEZOID = 4;

Map<Object, Object> MSG = MapUtil.builder()
.put(RECTANGULAR_SOLID, "长方体")
.put(LOWER_LEFT_TRAPEZOID, "单切左下")
.put(UPPER_LEFT_TRAPEZOID, "单切左上")
.put(LOWER_TRAPEZOID, "双切下")
.put(UPPER_TRAPEZOID, "双切上")
.build()
;
}
}

NullVar

public interface NullVar {

}

使用

// 4种方式任选其一,优先级从上往下
// 使用自定义int数组校验
@Enum(intEnumValues = {1,2}, message = "轮廓数据异常")
private Integer contour;

// 使用自定义long数组校验
@Enum(longEnumValues = {1,2}, message = "轮廓数据异常")
private Integer contour;

// 使用自定义string数组校验
@Enum(stringEnumValues = {"1","2"}, message = "轮廓数据异常")
private Integer contour;

// 使用枚举类校验
@Enum(enumClass = ContainerTypeVar.Contour.class, message = "轮廓数据异常")
private Integer contour;

默认表单校验

限制 说明
@Null 限制只能为null
@NotNull 限制必须不为null
@AssertFalse 限制必须为false
@AssertTrue 限制必须为true
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Future 限制必须是一个将来的日期
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字
@Past 限制必须是一个过去的日期
@Pattern(regexp) 限制必须符合指定的正则表达式
@Size(max,min) 限制字符长度必须在min到max之间
@Past 验证注解的元素值(日期类型)比当前时间早
@NotEmpty 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式

参考链接

https://www.cnblogs.com/cjsblog/p/8946768.html
https://www.jianshu.com/p/8ea600893d87