简介
JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation。
在任何时候,当你要处理一个应用程序的业务逻辑,数据校验是你必须要考虑和面对的事情。应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的。在通常的情况下,应用程序是分层的,不同的层由不同的开发人员来完成。很多时候同样的数据验证逻辑会出现在不同的层,这样就会导致代码冗余和一些管理的问题,比如说语义的一致性等。为了避免这样的情况发生,最好是将验证逻辑与相应的域模型进行绑定。
Bean Validation 为 JavaBean 验证定义了相应的元数据模型和 API。缺省的元数据是 Java Annotations,通过使用 XML 可以对原有的元数据信息进行覆盖和扩展。在应用程序中,通过使用 Bean Validation 或是你自己定义的 constraint,例如 @NotNull, @Max, @ZipCode, 就可以确保数据模型(JavaBean)的正确性。constraint 可以附加到字段,getter 方法,类或者接口上面。对于一些特定的需求,用户可以很容易的开发定制化的 constraint。Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。
约束注解
约束注解名称 | 约束注解说明 |
@Null | 验证对象是否为空 |
@NotNull | 验证对象是否为非空 |
@AssertTrue | 验证 Boolean对象是否为 false |
@AsserFalse | 验证 Boolean对象是否为 false |
@Min | 验证Number和 String对象是否大等于指定的值 |
@Max | 验证Number和 String对象是否小等于指定的值 |
@DecimalMin | 验证Number 和 String对象是否大等于指定的值,小数存在精度 |
@DecimalMax | 验证Number 和 String对象是否小等于指定的值,小酸存在精度 |
@Size | 验证对象(Array.Collection,Map,String)长度是否在给定的范围之内 |
@Digits | 验证 Number和 String的构成是否合法 |
@Past | 验证 Date和 Calendar对象是否在当前时间之前 |
@Future | 验证 Date和Calendar对象是否在当前时间之后 |
@Pattern | 验证 String对象是否符合正则表达式的规则 |
实例
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
给参数对象添加校验注解
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id;
@NotNull
private String name;
@Pattern(regexp = "^[0-9]{6,18}$",message = "密码必须是8-16位之间")
private String password;
@NotNull
private Integer age;
@Email
private String email;
}
测试如下:
异常的统一处理
参数校验不通过时,会抛出 BingBindException 异常,可以在统一异常处理中,做统一处理,这样就不用在每个需要参数校验的地方都用 BindingResult 获取校验结果了。
@Slf4j
@RestControllerAdvice(basePackages = "com.example.controller")
public class GlobalExceptionControllerAdvice {
//指定异常类型
@ExceptionHandler(value = BindException.class)
public String handleVaildException(BindException e){
log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String,String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach((fieldError)->{
errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
});
return errorMap.toString();
}
//兜底方法
@ExceptionHandler(value = Throwable.class)
public String handleException(Throwable throwable){
log.error("数据校验出现问题{},异常类型:{}",throwable.getMessage(),throwable.getClass());
return throwable.getMessage();
}
}
分组解决校验
新增和修改对于实体的校验规则是不同的,例如id是自增的时,新增时id要为空,修改则必须不为空;新增和修改,若用的恰好又是同一种实体,那就需要用到分组校验。
校验注解都有一个groups属性,可以将校验注解分组,我们看下@NotNull的源码:
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
/**
* Defines several {@link NotNull} annotations on the same element.
*
* @see javax.validation.constraints.NotNull
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
NotNull[] value();
}
}
从源码可以看出 groups 是一个Class<?>类型的数组,那么就可以创建一个Groups.
public class Groups {
public interface Add{}
public interface Update{}
}
给参数对象的校验注解添加分组
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@Null(groups = Groups.Add.class,message = "添加不需要指定id")
@NotNull(groups = Groups.Update.class,message = "修改必须指定id")
private Long id;
@NotNull
private String name;
@Pattern(regexp = "^[0-9]{6,18}$",message = "密码必须是8-16位之间")
private String password;
@NotNull
private Integer age;
@Email
private String email;
}
Controller 中原先的@Valid不能指定分组 ,需要替换成@Validated
@RestController
public class JSR303Controller {
@RequestMapping("/test")
public String test(@Validated(Groups.Add.class) User user){
return user.toString();
}
}
测试如下:
自定义校验注解
虽然JSR303和springboot-validator 已经提供了很多校验注解,但是当面对复杂参数校验时,还是不能满足我们的要求,这时候我们就需要 自定义校验注解。
例如User中的gender,用 1代表男 2代表女,我们自定义一个校验注解@ListValue,指定取值只能1和2。
创建约束规则
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {ListValueConstraintValidator.class })
public @interface ListValue {
String message() default "{javax.validation.constraints.NotNull.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int[] vals() default { };
}
一个标注(annotation) 是通过@interface关键字来定义的. 这个标注中的属性是声明成类似方法的样式的. 根据Bean Validation API 规范的要求:
- message属性, 这个属性被用来定义默认得消息模版, 当这个约束条件被验证失败的时候,通过此属性来输出错误信息。
- groups 属性, 用于指定这个约束条件属于哪(些)个校验组. 这个的默认值必须是Class<?>类型数组。
- payload 属性, Bean Validation API 的使用者可以通过此属性来给约束条件指定严重级别. 这个属性并不被API自身所使用。
除了这三个强制性要求的属性(message, groups 和 payload) 之外, 我们还添加了一个属性用来指定所要求的值. 此属性的名称vals在annotation的定义中比较特殊, 如果只有这个属性被赋值了的话, 那么, 在使用此annotation到时候可以忽略此属性名称.
另外, 我们还给这个annotation标注了一些元标注( meta annotatioin):
- @Target({ METHOD, FIELD, ANNOTATION_TYPE }): 表示此注解可以被用在方法, 字段或者annotation声明上。
- @Retention(RUNTIME): 表示这个标注信息是在运行期通过反射被读取的.
- @Constraint(validatedBy = ListValueConstraintValidator.class): 指明使用哪个校验器(类) 去校验使用了此标注的元素.可以指定多个
- @Documented: 表示在对使用了该注解的类进行javadoc操作到时候, 这个标注会被添加到javadoc当中.
创建约束校验器
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set = new HashSet<>();
//初始化
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
if(vals!=null){
for (int val : vals) {
set.add(val);
}
}
}
/**
* @param value 需要校验的值
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
ListValueConstraintValidator定义了两个泛型参数, 第一个是这个校验器所服务到标注类型(在我们的例子中即ListValue), 第二个这个校验器所支持到被校验元素的类型 (即Integer)。
如果一个约束标注支持多种类型的被校验元素的话, 那么需要为每个所支持的类型定义一个ConstraintValidator,并且注册到约束标注中。
这个验证器的实现就很平常了, initialize() 方法传进来一个所要验证的标注类型的实例, 在本例中, 我们通过此实例来获取其vals属性的值,并将其保存为Set集合中供下一步使用。
isValid()是实现真正的校验逻辑的地方, 判断一个给定的int对于@ListValue这个约束条件来说是否是合法的。
使用自定义注解。
在参数对象中使用@ListValue注解。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@Null(groups = Groups.Add.class,message = "添加不需要指定id")
@NotNull(groups = Groups.Update.class,message = "修改必须指定id")
private Long id;
@NotNull
private String name;
@Pattern(regexp = "^[0-9]{6,18}$",message = "密码必须是8-16位之间")
private String password;
@NotNull
private Integer age;
@Email
private String email;
@ListValue(vals = {1,0},groups = Groups.Add.class,message = "数据类型必须在自动范围内")
private Integer status;
}
测试如下: