一.前言
当提供一个接口对外提供服务时,数据校验是必须需要考虑的事情。很多时候,必须在每个单独的验证框架中实现完全相同的验证。为了避免在每一层重新实现这些验证,许多开发人员会将验证直接捆绑到他们的类中,用复制的验证代码将它们混杂在一起。
这个JSR将为JavaBean验证定义一个元数据模型和API。
二.Valid 参数校验
Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
JSR 303 校验框架注解类:
• @NotNull 注解元素必须是非空
• @Null 注解元素必须是空
• @Digits 验证数字构成是否合法
• @Future 验证是否在当前系统时间之后
• @Past 验证是否在当前系统时间之前
• @Max 验证值是否小于等于最大指定整数值
• @Min 验证值是否大于等于最小指定整数值
• @Pattern 验证字符串是否匹配指定的正则表达式
• @Size 验证元素大小是否在指定范围内
• @DecimalMax 验证值是否小于等于最大指定小数值
• @DecimalMin 验证值是否大于等于最小指定小数值
• @AssertTrue 被注释的元素必须为true
• @AssertFalse 被注释的元素必须为false
Hibernate Validator扩展注解类:
• @Email 被注释的元素必须是电子邮箱地址
• @Length 被注释的字符串的大小必须在指定的范围内
• @NotEmpty 被注释的字符串的必须非空
• @Range 被注释的元素必须在合适的范围内
校验结果保存在BindingResult或Errors对象中
• 这两个类都位于org.springframework.validation包中
• 需校验的表单对象和其绑定结果对象或错误对象是成对出现的
• Errors接口提供了获取错误信息的方法,如getErrorCount()获取错误的数量, getFieldErrors(String field)得到成员属性的校验错误列表
• BindingResult接口扩展了Errors接口,以便可以使用Spring的org.springframeword.validation.Validator对数据进行校验,同时获取数据绑定结果对象的信息
@Data
public class UserComplex {
@NotEmpty(message = "名字不能为空")
@Size(min = 2, max = 6, message = "名字长度在2-6位") //字符串,集合,map限制大小
@Length(min = 2, max = 6, message = "名字长度在2-6位")
private String name;
@Length(min = 3, max = 3, message = "pass 长度不为3")
private String pass;
@DecimalMin(value = "10", inclusive = true, message = "salary 低于10") // 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
private int salary;
@Range(min = 5, max = 10, message = "range 不在范围内")
private int range;
@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄不能小于18")
@Max(value = 70, message = "年龄不能大于70")
private int age;
@Email
private String email;
@AssertTrue
private boolean flag;
@Past
private Date birthday;
@Future
private Date expire;
@URL(message = "url 格式不对")
private String url;
@AnnoValidator(value = "1,2,3")
private String anno;
//@Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
@Size(min = 2, max = 6, message = "长度在2-6位") //字符串,集合,map限制大小
private List<Integer> list;
}
三.Validated 参数校验
Validated是 Spring 对 Valid 的封装,是 Valid 的加强版,支持更多特性
1.只要类路径上有JSR-303实现(比如Hibernate验证器),Bean validation 1.1支持的方法验证特性就会自动启用。这让bean方法可以用javax进行注释。对其参数和/或返回值的验证约束。使用这种带注释的方法的目标类需要在类型级别上使用@Validated注释进行注释,以便搜索它们的方法以找到内联约束注释。
Validated 支持对 PathVariable 参数校验,以及 RequestParam 参数校验
但是注解必须写在类上:
@RestController
// 注解必须写在这里,参数校验不过会有 ConstraintViolationException 异常
@Validated
public class Valid2Controller {
@GetMapping("valid4/{data}")
public String getPathVariable(@Size(min = 3, max = 6) @PathVariable String data) {
return data;
}
@GetMapping("valid4")
public String getRequestParam(@Size(min = 3, max = 6) @RequestParam(value = "name", defaultValue = "0") String name) {
return name;
}
}
2.分组检验
根据需要校验特定字段,应用场景:SpringDataJPA 中 save 方法没有 ID 字段就是保持新的数据,如果有 ID 字段就是跟新数据。
使用方法:首先写一些接口,这里的接口是标记用的,就像 JsonView 一样。、
BaseA
public interface BaseA {
}
BaseB
public interface BaseB {
}
被校验的实体类
@Data
public class UserSimple {
//在AAAA分组时,判断不能为空
@NotEmpty(groups = {BaseA.class})
private String id;
//name字段不为空,且长度在3-8之间
@NotEmpty(message = "{user.name.notBlank}", groups = {BaseB.class})
@Size(min = 3, max = 8, message = "{user.name.notBlank}", groups = {BaseB.class})
private String name;
private int age;
}
分组检验。当接口中(@RequestBody @Validated({BaseB.class}) 时只会校验实体类中被(groups = {BaseB.class})标记的字段。
@RestController
public class Valid1Controller {
// {"name":"shao","pass":"333","salary":11,"range":6,"age":20,"email":"shaopro@qq.com","flag":true,"birthday":"2018-08-07T16:25:44.000+0000","expire":"2018-12-01T10:12:24.000+0000","url":"http://www.baidu.com","anno":"1","list":[1,2]}
@PostMapping("valid1")
public UserComplex postUser1(@RequestBody @Valid UserComplex user) {
return user;
}
@PostMapping("valid2")
public UserSimple postGroup(@RequestBody @Validated({BaseB.class}) UserSimple simpleUser) {
return simpleUser;
}
@PutMapping("valid2")
public UserSimple putGroup(@RequestBody @Validated({BaseA.class}) UserSimple simpleUser) {
return simpleUser;
}
}
四.参数校验异常处理
1.比较 low 的方式是在接口中使用 BindingResult 对象去接受这个校验结果
例如:
@RequestMapping("login")
public String login(@Valid User user, BindingResult result) {
if (result.hasErrors())
return "user/login";
return "redirect:/";
}
先去判断是否有异常
2.比较好方式是使用同一异常处理
@RestControllerAdvice
public class CheckAdvice {
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 请求的 JSON 参数在请求体内的参数校验
*
* @param e 异常信息
* @return 返回数据
* @throws JsonProcessingException jackson 的异常
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleBindException1(MethodArgumentNotValidException e) throws JsonProcessingException {
e.getBindingResult().getAllErrors().forEach(System.out::println);
return new ResponseEntity<>("cuowu:" + MAPPER.writeValueAsString(e.getBindingResult().getAllErrors()), HttpStatus.BAD_REQUEST);
}
/**
* 请求的 URL 参数检验
*
* @param e 异常信息
* @return 返回提示信息
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConstraintViolationException.class)
public String handleBindException2(ConstraintViolationException e) {
e.getConstraintViolations().forEach(System.out::println);
return "ConstraintViolationException";
}
}
五.自定义参数校验
在很多情况下,JSR303 不能满足我们的校验需求,我们需要自定义一些校验逻辑,当然不是使用 if else 去判断。可以定义一些注解以及注解处理工具嵌入到 Spring 框架中自动调用。
这里我们定义了注解 AnnoValidator 用来校验 anno 字段,只能存放1,2,3之中的一个字符串数字
@AnnoValidator(value = "1,2,3")
private String anno;
注解的内容:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
//指定注解的处理类
@Constraint(validatedBy = AnnoValidatorClass.class)
public @interface AnnoValidator {
String value();
String message() default "AnnoValidator 不存在";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
注解处理类
public class AnnoValidatorClass implements ConstraintValidator<AnnoValidator, Object> {
private String value;
@Override
public void initialize(AnnoValidator constraintAnnotation) {
this.value = constraintAnnotation.value();
}
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
List<String> list = Arrays.asList(value.split(","));
final AtomicBoolean flag = new AtomicBoolean(false);
list.forEach(one -> {
if (one.equals(o)) {
flag.set(true);
}
});
return flag.get();
}
}
六.动态改变校验数据
经常某些字段的校验数据是动态改变的(例如手机的号码段扩充,某些操作的操作码增加),所以这些特殊情况我们不能讲校验的数据写死到代码中。下面展示了动态改变校验数据的方法。
自定义校验注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
//指定注解的处理类
@Constraint(validatedBy = DynamicHandler.class)
public @interface Dynamic {
String value() default "";
String message() default "Dynamic 不存在";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
需要校验的实体类,假设 books 内的数据只能在某个范围内。
@Data
public class UserDynamic {
private String name;
@Dynamic
private Set<String> books;
}
注解的处理类。需要注意的点:
1.被 static 关键字修饰的字段是属于整个类的;
2.存储可变字段的 Set 必须是线程安全的,当对容器中元素进行遍历同时增加数据时会抛出 fail-fast 错误。
public class DynamicHandler implements ConstraintValidator<Dynamic, Set<String>> {
// 得用线程安全的容器,当对容器中元素进行遍历同时增加数据时会抛出 fail-fast 错误
private volatile static CopyOnWriteArraySet<String> dynamicSet;
@Override
public boolean isValid(Set<String> set, ConstraintValidatorContext constraintValidatorContext) {
return dynamicSet.containsAll(set);
}
@Override
public void initialize(Dynamic constraintAnnotation) {
// nothing to do
}
public static void setSet(CopyOnWriteArraySet<String> set) {
DynamicHandler.dynamicSet = set;
}
}
使用定时任务来模拟校验数据的改变,每十秒钟改变一下校验的数据。真实环境中应该是从数据库中获取。
@Slf4j
@Component
@EnableScheduling
public class DynamicSchedule {
@Scheduled(fixedDelay = 10000)
public void autoSync() {
CopyOnWriteArraySet<String> dynamicSet = new CopyOnWriteArraySet<>();
Random random = new Random();
for (int i = 0; i < 3; i++) {
dynamicSet.add(random.nextInt(10) + "");
}
DynamicHandler.setSet(dynamicSet);
log.info(dynamicSet.toString());
}
}