通常我们在使用 spring 框架编写接口时,对于部分接口的参数我们要进行判空或者格式校验来避免程序出现异常。
那时我们一般都是使用 if-else 逐个对参数进行校验。这种方法按逻辑来说也是没有问题的,同样也能实现预期效果。
但是,这样的代码从可读性以及美观程序来看,是非常糟糕的。
那么,我们就可以使用@valid 注解来帮助我们优雅的校验参数。
一、如何使用 Validation 相关注解进行参数校验
① 为实体类中的参数或者对象添加相应的注解;
② 在控制器层进行注解声明,或者手动调用校验方法进行校验;
③ 对异常进行处理;
二、Validation 类的相关注解及描述
嵌套注解 @Valid(javax.validation.Valid)
空和非空检查: @Null、@NotNull、@NotBlank、@NotEmpty
注解 |
支持 Java 类型 |
备注 |
@Null |
Object |
验证元素值为 null |
@NotNull |
Object |
验证元素值不能为 null |
@NotBlank |
CharSequence |
验证元素值不为 null 且移除两边空格后长度大于 0 |
@NotEmpty |
CharSequence,Collection,Map and Arrays |
验证元素值不为 null 且不为空(字符串长度不为 0、集合大小不为 0) |
Boolean 值检查: @AssertTrue、@AssertFalse
注解 |
支持 Java 类型 |
备注 |
@AssertTrue |
Boolean, boolean |
验证元素值必须为 true,否则抛异常 |
@AssertFalse |
Boolean, boolean |
验证元素值必须为 flase |
长度检查: @Size、@Length
注解 |
支持 Java 类型 |
备注 |
@Size |
String,Collection,Map,arrays,CharSequence |
验证元素个数包含在一个区间 |
日期检查: @Future、@FutureOrPresent、@Past、@PastOrPresent
注解 |
支持 Java 类型 |
备注 |
@Future |
java.util.Date, java.util.Calendar |
验证日期为当前时间之后 |
@FutureOrPresent |
java.util.Date, java.util.Calendar |
验证日期为当前时间或之后一个时间 |
@Past |
java.util.Date, java.util.Calendar |
验证日期为当前时间之前 |
@PastOrPresent |
java.util.Date, java.util.Calendar |
验证日期为当前时间或之前 |
其它检查: @Email、@CreditCardNumber、@URL、@Pattern、@ScriptAssert、@UniqueElements
注解 |
支持 Java 类型 |
备注 |
@Email |
CharSequence |
验证邮箱 |
@CreditCardNumber |
CharSequence |
验证信用卡 |
@URL |
CharSequence |
验证 url |
@Pattern |
CharSequence |
验证正则,如:@Pattern(regexp = “^[1][3,4,5,6,7,8,9][0-9]{9}$”, message = “手机号格式有误”) |
@Valid |
Object |
验证关联对象元素进行递归校验检查 |
@UniqueElements |
Collection |
校验集合中的元素必须保持唯一 否则异常 |
数值检查: @Min、@Max、@Range、@DecimalMin、@DecimalMax、@Digits
注解 |
支持 Java 类型 |
备注 |
@Min |
BigDecimal, BigInteger, byte, short,int, long,Number,String(数字字符串) |
检验当前数值大于等于指定值 |
@Max |
BigDecimal, BigInteger, byte, short,int, long,Number,String(数字字符串) |
检验当前数值小于等于指定值 |
@DecimalMin |
BigDecimal, BigInteger, byte, short,int, long,Number,String(数字字符串) |
验证数值是否大于等于指定值 |
@DecimalMax |
BigDecimal, BigInteger, byte, short,int, long,Number,String(数字字符串) |
验证数值是否小于等于指定值 |
@Digits(integer = 3, fraction = 2) |
|
验证注解的元素值的整数位数和小数位数上限 |
PS:@Valid 和@Validated 的区别
- @Valid 能作用在属性上,支持嵌套验证,不支持分组
- @Validated 时@Valid 的封装,不能作用属性上,不支持嵌套,支持分组
- 若分组需要有序进行,比如分组 1 后才校验分组 2,可定义第三个接口 Three 使用@GroupSequence({First.class,Second.class}) ,@Validated 指定分组 Three.class
三、使用 Validation API 进行参数效验步骤
第一种:在 Controller 方法参数前加@Valid 注解——校验不通过时直接抛异常,get 请求直接在平面参数前添加相应的校验规则注解,使用这种的话一般结合统一异常处理进行处理;
第二种:在 Controller 方法参数前加@Valid 注解,参数后面定义一个 BindingResult 类型参数——执行时会将校验结果放进 bindingResult 里面,用户自行判断并处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
@PostMapping("/testBindingResult") public String testBindingResult(@RequestBody @Valid UserInfo userInfo, BindingResult bindingResult) { if (bindingResult.hasErrors()) { String messages = bindingResult.getAllErrors() .stream() .map(ObjectError::getDefaultMessage) .reduce((m1, m2) -> m1 + ";" + m2) .orElse("参数输入有误!"); throw new IllegalArgumentException(messages); } return "操作成功!";
}
|
这里我们是直接抛出了异常,如果没有进行全局异常处理的话,接口将会返回如下信息:
四、SpringBoot 项目中实战演练
Spring Boot 2.3 后,starter-web 不再包含子依赖 starter-validation,需要手动导入。
1.对实体类的变量进行注解标注
实体类中添加 @Valid 相关验证注解,并在注解中添加出错时的响应消息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Data public class User {
@NotBlank(message = "姓名不能为空") private String username;
@NotBlank(message = "密码不能为空") @Length(min = 6, max = 16, message = "密码长度为6-16位")
private String password; @Pattern(regexp = "0?(13|14|15|17|18|19)[0-9]{9}", message = "手机号格式不正确") private String phone;
@Valid @NotNull(message = "userinfo不能为空") private UserInfo userInfo;
}
|
2.创建自定义异常
自定义异常类,方便我们处理手动抛出的异常。
1 2 3 4 5 6 7 8
| public class ParamaErrorException extends RuntimeException { public ParamaErrorException() {
} public ParamaErrorException(String message) { super(message); } }
|
3.自定义响应枚举类
定义一个返回信息的枚举类,方便我们快速响应信息,不必每次都写返回消息和响应码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public enum ResultEnum { SUCCESS(1000, "请求成功"), PARAMETER_ERROR(1001, "请求参数有误!"), UNKNOWN_ERROR(9999, "未知的错误!"); private Integer code; private String message; ResultEnum(Integer code, String message) { this.code = code; this.message = message; } public Integer getCode() { return code; } public String getMessage() { return message; } }
|
4.自定义响应对象类
创建用于返回调用方的响应信息的实体类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Data public class ResponseResult { private Integer code; private String msg; public ResponseResult() {} public ResponseResult(ResultEnum resultEnum) { this.code = resultEnum.getCode(); this.msg = resultEnum.getMessage(); }
public ResponseResult(Integer code, String msg) { this.code = code; this.msg = msg; } }
|
5.接口类中添加相关注解
处理 get 请求直接在参数前添加验证注解,处理 post 请求时在对象前添加@Valid 注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| @Validated @RestController @Api(value = "测试使用validation验证参数") public class TestController {
@ApiOperation(value = "测试get方法", notes = "输入用户名") @GetMapping("/testGet") public ResponseResult testGet(String username) { if (username == null || "".equals(username)) { throw new ParamaErrorException("username 不能为空"); } return new ResponseResult(ResultEnum.SUCCESS); }
@ApiOperation(value = "测试get方法", notes = "输入用户名") @GetMapping("/testGetByValidated") public ResponseResult testGetByValidated(@Length(max = 4) @RequestParam("username") String username) { return new ResponseResult(ResultEnum.SUCCESS); }
@ApiOperation(value = "post方法传入单个对象", notes = "传入json对象") @PostMapping("/testUserInfo") public ResponseResult testUserInfo(@Valid @RequestBody UserInfo userInfo) { return new ResponseResult(ResultEnum.SUCCESS); }
@ApiOperation(value = "post方法传入对象,手动测试", notes = "单个对象") @PostMapping("/checkByMethod") public ResponseResult checkByMethod(@RequestBody UserInfo userInfo) { MyValidationUtils.validate(userInfo); return new ResponseResult(ResultEnum.SUCCESS); }
@ApiOperation(value = "post方法传入多个对象", notes = "多个对象") @PostMapping("/testUserList") public ResponseResult testUserList(@Valid @RequestBody List<UserInfo> userInfo) { return new ResponseResult(ResultEnum.SUCCESS); }
@ApiOperation(value = "测试对象中嵌套对象的情况") @PostMapping("/checkUser") public ResponseResult checkUser(@Valid @RequestBody User user) { return new ResponseResult(ResultEnum.SUCCESS); }
@PostMapping("/testBindingResult") public String testBindingResult(@RequestBody @Valid UserInfo userInfo, BindingResult bindingResult) { if (bindingResult.hasErrors()) { String messages = bindingResult.getAllErrors() .stream() .map(ObjectError::getDefaultMessage) .reduce((m1, m2) -> m1 + ";" + m2) .orElse("参数输入有误!"); throw new IllegalArgumentException(messages); } return "操作成功!"; } }
|
补充:使用自定义参数注解 1.我们这里创建一个身份证校验注解
1 2 3 4 5 6 7 8 9
| @Documented @Target({ElementType.PARAMETER, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = IdentityCardNumberValidator.class) public @interface IdentityCardNumber { String message() default "身份证号码不合法"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
|
这个注解是作用在 Field 字段上,运行时生效,触发的是 IdentityCardNumber 这个验证类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
public class ListValConstraintValidator implements ConstraintValidator<ListValue, Integer> { Set<Integer> set = new HashSet<>(); @Override public void initialize(ListValue constraintAnnotation) { int[] vals = constraintAnnotation.vals(); for (int val : vals) { set.add(val); } }
@Override public boolean isValid(Integer value, ConstraintValidatorContext context) { return set.contains(value); } }
|
1 2 3
| message 定制化的提示信息,主要是从ValidationMessages.properties里提取,也可以依据实际情况进行定制 groups 这里主要进行将validator进行分类,不同的类group中会执行不同的validator操作 payload 主要是针对bean的,使用不多。
|