一、关于 Bean Validation
在任何时候,当你要处理一个应用程序的业务逻辑,数据校验是你必须要考虑和面对的事情。应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的。在通常的情况下,应用程序是分层的,不同的层由不同的开发人员来完成。很多时候同样的数据验证逻辑会出现在不同的层,这样就会导致代码冗余和一些管理的问题,比如说语义的一致性等。为了避免这样的情况发生,最好是将验证逻辑与相应的域模型进行绑定。
Bean Validation 为 JavaBean 验证定义了相应的元数据模型和 API。缺省的元数据是 Java Annotations,通过使用 XML 可以对原有的元数据信息进行覆盖和扩展。在应用程序中,通过使用 Bean Validation 或是你自己定义的 constraint,例如 @NotNull, @Max, @ZipCode, 就可以确保数据模型(JavaBean)的正确性。constraint 可以附加到字段,getter 方法,类或者接口上面。对于一些特定的需求,用户可以很容易的开发定制化的 constraint。Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。
Bean Validation1.1 规范 https://beanvalidation.org/1.1/spec/(最新的已经是2.0了)
Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303、JSR 349 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。如果想了解更多有关 Hibernate Validator 的信息,请查看http://hibernate.org/validator/
引用 https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/ 超哥略有修改
二、约束的定义
Constraints are defined by the combination of a constraint annotation and a list of constraint validation implementations. The constraint annotation is applied on types, fields, methods, constructors, parameters or other constraint annotations in case of composition.
https://beanvalidation.org/1.1/spec/
翻译:约束由约束注释和约束验证实现列表的组合定义。在组合的情况下,约束注释应用于类型、字段、方法、构造函数、参数或其他约束注释。
看下约束的源码:
@Documented
@Target({ ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface Constraint {
/**
* {@link ConstraintValidator} classes implementing the constraint. The given classes
* must reference distinct target types for a given {@link ValidationTarget}. If two
* {@code ConstraintValidator}s refer to the same type, an exception will occur.
*
* At most one {@code ConstraintValidator} targeting the array of parameters of
* methods or constructors (aka cross-parameter) is accepted. If two or more
* are present, an exception will occur.
*
* @return array of {@code ConstraintValidator} classes implementing the constraint
*/
Class extends ConstraintValidator, ?>>[] validatedBy();
}
三、约束的属性
先看下JDK中NotNull约束的定义
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
//validatedBy 没有指定ConstraintValidator,必须由SPI的实现类默认支持
@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();
}
}
- message
错误消息,如果需要支持国际化可以设置消息的键值 - groups
对约束进行分组,可以用于分组校验或是顺序校验 - payload
标注负载信息,由用户自己自定义使用规则(比如可以设置错误的严重级别之类的) - validationAppliesTo
用于约束声明时阐明约束的目标(即带注释的元素、方法返回值或方法参数)。 - Constraint specific parameter(由各类约束本身定义)
约束注释定义可以定义其他元素来参数化约束。例如,验证字符串长度的约束可以使用名为length的注释元素指定约束声明时的最大长度。
TALK IS CHEAP , SHOW ME THE CODE!
四、应用举例
示例代码使用了Lombok中的@Data、@Accessors,不了解的同学可以baidu下
1、无分组的例子
@Data
@Accessors(chain = true)
public class User {
@NotNull
private String name;
@Min(20)
private int age;
}
public static void main(String[] args) {
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
//不指定分组用的是默认分组
Set> constraintViolations = validator.validate(new User());
//如果有错误信息,constraintViolations集合一定有内容
for(ConstraintViolation constraintViolation : constraintViolations){
System.out.println(constraintViolation.getMessage());
}
}
//运行输出
不能为null
最小不能小于20
2、按分组校验
//定义年龄分组
public interface ValidateAge {
}
//定义名称分组
public interface ValidateName {
}
@Data
@Accessors(chain = true)
public class User {
@NotNull(groups = ValidateName.class) //指定分组
private String name;
@Min(value = 20,groups = ValidateAge.class) //指定分组
private int age;
}
public static void main(String[] args) {
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
//指定检查的分组
Set> constraintViolations =
validator.validate(new User(),ValidateName.class);
System.out.println("ValidateName group :");
for(ConstraintViolation constraintViolation : constraintViolations){
System.out.println(constraintViolation.getMessage());
}
constraintViolations = validator.validate(new User(),ValidateAge.class);
System.out.println("ValidateAge group :");
for(ConstraintViolation constraintViolation : constraintViolations){
System.out.println(constraintViolation.getMessage());
}
}
//运行输出
ValidateName group :
不能为null
ValidateAge group :
最小不能小于20
3、按分组指定顺序校验
@Data
@Accessors(chain = true)
public class User {
@NotNull(groups = ValidateName.class)
private String name;
@Min(value = 20,groups = ValidateAge.class)
private int age;
@GroupSequence({ValidateAge.class,ValidateName.class})
public interface ValidateGroupSequence{
}
}
public static void main(String[] args) {
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
Set> constraintViolations =
validator.validate(new User(), User.ValidateGroupSequence.class);
for(ConstraintViolation constraintViolation : constraintViolations){
System.out.println(constraintViolation.getMessage());
}
}
//运行输出:
最小不能小于20
@GroupSequence({ValidateAge.class,ValidateName.class})顺序的含义是:
ValidateAge.class完成validate通过才会检查ValidateName.class并不是一起validate。可以理解为ValidateAge.class是ValidateName.class的前置校验,如果ValidateAge都通不过检查的化ValidateName也就没有检查的必要了。
4、指定PayLoad
@Data
@Accessors(chain = true)
public class User {
//如果name为空我们认为是fatal
@NotNull(payload = Fatal.class)
private String name;
@Min(value = 20)
private int age;
public interface Fatal extends Payload{
}
}
public static void main(String[] args) {
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
Set> constraintViolations =
validator.validate(new User());
for(ConstraintViolation constraintViolation : constraintViolations){
for(Object payload: constraintViolation.getConstraintDescriptor().getPayload()){
if(User.Fatal.class.isAssignableFrom((Class)payload)){
System.out.println("严重错误");
System.out.println(constraintViolation.getMessage());
}
}
}
}
// 运行输出
严重错误
不能为null
5、自定义ConstraintValidator
设计一个定长length约束
@Target({FIELD })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {ConstraintValidator4Length6Impl.class })
public @interface Length6 {
String message() default "长度必须是6个字符长度";
Class>[] groups() default { };
Class extends Payload>[] payload() default { };
}
定长约束检查实现
public class ConstraintValidator4Length6Impl implements ConstraintValidator {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if(!StringUtils.isEmpty(value) && value.length() == 6){
return true;
}
return false;
}
}
使用Length6
@Data
@Accessors(chain = true)
public class User {
//如果name为空我们认为是fatal
@NotNull(payload = Fatal.class)
@Length6
private String name;
@Min(value = 20)
private int age;
public interface Fatal extends Payload{
}
}
public static void main(String[] args) {
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
User user = new User();
user.setName("cg");
Set> constraintViolations =
validator.validate(user);
for(ConstraintViolation constraintViolation : constraintViolations){
System.out.println(constraintViolation.getMessage());
}
}
//运行输出:
长度必须是6个字符长度
最小不能小于20
6、其他可执行模块的检查
以方法检查方法返回值为例
public class ValidationApplication {
@Length6
public String getUser() {
return "cg";
}
public static void main(String[] args) throws NoSuchMethodException {
ValidationApplication application = new ValidationApplication();
Method method = ValidationApplication.class.getMethod("getUser");
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
Set> constraintViolations =
validator
.forExecutables()
.validateReturnValue(application, method, application.getUser());
for(ConstraintViolation constraintViolation : constraintViolations){
System.out.println(constraintViolation.getMessage());
}
}
}
//运行输出
长度必须是6个字符长度
ExecutableValidator接口定义如下:
关于validationApplies与 Constraint specific parameter应用举例参看: https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#example-dual-cross-parameter-constraint
五、spring对Validation的支持增强
1、随意注入ValidatorFactory与Validator
Spring提供了对Bean验证API的完全支持。这包括方便地支持将JSR-303或JSR-349 Bean验证提供者引导为Spring Bean。这允许您注入javax.validation.ValidatorFactory或javax.validation.Validator 。在应用程序中需要验证的地方使用验证器。
示例
@Configuration
public class ValidationApplication {
@Bean
public LocalValidatorFactoryBean validator(){
return new LocalValidatorFactoryBean();
}
@Length6
public String getUser() {
return "cg";
}
public static void main(String[] args) throws NoSuchMethodException {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(ValidationApplication.class);
ValidationApplication application = applicationContext.getBean(ValidationApplication.class);
Validator validator = applicationContext.getBean(Validator.class);
Method method = application.getClass().getMethod("getUser");
Set> constraintViolations = validator
.forExecutables()
.validateReturnValue(application, method, application.getUser());
for(ConstraintViolation constraintViolation : constraintViolations){
System.out.println(constraintViolation.getMessage());
}
}
}
LocalValidatorFactoryBean 实现了javax.validation.ValidatorFactory与javax.validation.Validator,所以我们可以方便的注入ValidatorFactory、Validator。
2、自定义ConstraintValidator允许注入spring bean
定义@Dependency
@Target({FIELD,METHOD })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {DependencyValidatorImpl.class })
public @interface Dependency {
String message() default "依赖为空";
Class>[] groups() default { };
Class extends Payload>[] payload() default { };
}
实现DependencyValidatorImpl
public class DependencyValidatorImpl implements ConstraintValidator {
@Autowired
private ValidationApplication application;
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
//这里仅仅校验application不为空
return application != null;
}
}
测试主函数
@Configuration
public class ValidationApplication {
@Bean
public LocalValidatorFactoryBean validator(){
return new LocalValidatorFactoryBean();
}
@Length6
@Dependency
public String getUser() {
return "cg";
}
public static void main(String[] args) throws NoSuchMethodException {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(ValidationApplication.class);
ValidationApplication application = applicationContext.getBean(ValidationApplication.class);
Validator validator = applicationContext.getBean(Validator.class);
Method method = application.getClass().getMethod("getUser");
Set> constraintViolations = validator
.forExecutables()
.validateReturnValue(application, method, application.getUser());
for(ConstraintViolation constraintViolation : constraintViolations){
System.out.println(constraintViolation.getMessage());
}
}
}
//输出:
长度必须是6个字符长度
//如果去掉DependencyValidatorImpl中的@Autowired,还会输出:依赖为空
3、Spring驱动的方法级校验
示例代码
@Validated
class UserService{
public void createUser(@Length6 String name){
}
}
@Configuration
public class ValidationApplication {
@Bean
public LocalValidatorFactoryBean validator(){
return new LocalValidatorFactoryBean();
}
//代理UserService类,相当于与简化版的validator.forExecutables()调用
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor(){
return new MethodValidationPostProcessor();
}
@Bean
public UserService userService(){
return new UserService();
}
public static void main(String[] args) throws NoSuchMethodException {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(ValidationApplication.class);
UserService userService = applicationContext.getBean(UserService.class);
userService.createUser("cg");
}
}
输出结果
Exception in thread "main" javax.validation.ConstraintViolationException: createUser.name: 长度必须是6个字符长度
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
at com.demo.UserService$$EnhancerBySpringCGLIB$$4cda4e82.createUser()
at com.demo.ValidationApplication.main(ValidationApplication.java:49)
更多内容,关注《超哥spring源码解析》