Bean Validation-2.0

为什么会写?

最近项目中用到了校验,原来的校验逻辑都是在代码java里写死的,这样的坏处是代码没法重用,例如需要对下面的name和email做不为空的校验,age做必须在1-200的校验

public class User{
 private String name;
 private String email;
 private int age;
}

旧的逻辑就是在某个校验类里去判断

public class UserValidator{
 public boolean validate(User user){
 	if(user.getName()!=null){
 	  logError("user.name.isNull");
 	  return false;
 	}
 	if(user.getEmail()!=null){
 	  logError("user.email.isNull");
 	  return false;
 	} 	
 	if(user.getAge()<1){
 	  logError("user age less than 1");
 	  return false;
 	}
 	if(user.getAge()>200){
 	 logError("user age larger than 200");
 	  return false;
 	}
 	return true;
 }
}

可以从上面代码里看出!=null的逻辑基本是一样的,都是判读不为空后记录。这个代码不能复用,之前做过的springboot里使用到的对某个bean做validate,只需要在参数里加入
@Validate即可。查找了下springboot里的validate用的是JSR333:Bean Validation specification

对上面校验的改造

用bean Validation来对之前的代码做个简单demo如下

public class User {
   @NotNull(message = "email can not be null")
   private String email;
   
   @NotNull(message = "age can not be null")
   @Min(value = 1,message ="age can not less than 1" )
   @Max(value = 200,message ="age can not more than 200" )
   private  int age;
   
   @NotNull(message = "name can not be null")
   private String name;
}
public class demo {
    public static void main(String[] args) {
        User user=new User();        
        javax.validation.Validator beanValidator = Validation.buildDefaultValidatorFactory()
                .getValidator();
        Set<ConstraintViolation<User>> set=beanValidator.validate(user,Default.class);
       Iterator it=set.iterator();
       while(it.hasNext()){
           ConstraintViolation cv= (ConstraintViolation) it.next();
           System.out.println(cv.getMessage());
       }
    }
}

打印结果

name can not be null
email can not be null
age can not more than 200

以上就是简单的校验,是不是比单纯的写if…else 好多了

内置(build-in)的注解校验

上面的注解比如@NotNull ,@Min, @Max是validation包里已有的一些校验注解,其它的还有如下图:
在这里插入图片描述
比如上面的@Email, 我们就可以直接使用

   @NotNull(message = "email can not be null")
   @Email
   private String email;

校验不通过的话就会打印出

不是一个合法的电子邮件地址

自定义注解

当已经存在的校验注解不满足我们的需求时候,就需要客户化自己的注解

  • 需求1 :假如User 里有个number,这个number是一个以某个值开头,例如CCC开头
Class User{
   //自定义注解
   @UserNumber(prefix="CCC")
   private String number;
   }

实现上面的@UserNumber 主要有2个类:
第一个类是注解即UserNumber ,如下

@Target({ FIELD}) 
@Retention(RUNTIME)
@Documented
//指定一个校验类
@Constraint(validatedBy = UserNumberConstraintValidator.class)
public @interface UserNumber {
    String message() default "{user.number.invalid}";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
    String prefix();
}

注意 message() groups() payload() 这3个 方法必须存在

第2个类是校验的逻辑类,也就是上面指定的
@Constraint(validatedBy = UserNumberConstraintValidator.class)

UserNumberConstraintValidator

public class UserNumberConstraintValidator implements ConstraintValidator<UserNumber,String> {
    private String prefix;
    //真正的逻辑校验
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if(value==null)
            return false;
        return value.startsWith(prefix);
    }
    //初始化,这里可以获取到注解
    public void initialize(UserNumber constraintAnnotation) {
        this.prefix = constraintAnnotation.prefix();
    }
}
public class demo {
    public static void main(String[] args) {
        User user=new SubUser();
        user.setEmail("haha");
        user.setAge(230);
        user.setNumber("AAA");
        javax.validation.Validator beanValidator = Validation.buildDefaultValidatorFactory()
                .getValidator();
        Set<ConstraintViolation<User>> set=beanValidator.validate(user,Default.class);
       Iterator it=set.iterator();
       while(it.hasNext()){
           ConstraintViolation cv= (ConstraintViolation) it.next();
           System.out.println(cv.getMessage());
       }
    }
}

会打印出
{user.number.invalid}

以上就是简单的自定义注解的实现。这里最重要的类就是ConstraintValidator

public interface ConstraintValidator<A extends Annotation, T> {
	default void initialize(A constraintAnnotation) {
	}
	boolean isValid(T value, ConstraintValidatorContext context);
}

注意泛型的使用,A extends Annotation 必须是注解类,例如上面的UserNumber,T 表示的是被该注解标识的类,例如

@UserNumber(prefix="CCC")
   private String number;

T 就是String。如果将注解放到某个类上,那么T 就是那个类。

  • 需求2 :假设增加当user.hasEmail为true的时候,才会去校验email ,user类定义如下
Class User{
   private boolean hasEmail;   
   private String email;
}

对于这种需求一般是写在user类上,比如定一个UserEmailValidator

@Target({ TYPE})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = UserEmailConstraintValidator.class)
public @interface UserEmailValidator {

    String message() default "{user.email.is.empty}";

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

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

逻辑校验类

public class UserEmailConstraintValidator implements ConstraintValidator<UserEmailValidator,User> {
    public boolean isValid(User user, ConstraintValidatorContext context) {
        if (user.isHasEmail()&& user.getEmail()==null) {
            return false;
        }
        return true;
    }
    public void initialize(UserEmailValidator constraintAnnotation) {
    }
}

写完后将注解放到类上。注解放到User上后,T就是User

@UserEmailValidator
public class User{}
public class demo {
    public static void main(String[] args) {
        User user=new User();
        user.setHasEmail(true);   //设成true而没有给email的话就会报错    
        javax.validation.Validator beanValidator = Validation.buildDefaultValidatorFactory()
                .getValidator();
        Set<ConstraintViolation<User>> set=beanValidator.validate(user,Default.class);
       Iterator it=set.iterator();
       while(it.hasNext()){
           ConstraintViolation cv= (ConstraintViolation) it.next();
           System.out.println(cv.getMessage());
       }
    }
}

结果会报错{user.email.is.empty}

注解放到类上的不足

上面的注解是为了校验email这个字段,这样的注释放到类上总觉的不是专门的地方,能否直接放到email 这个字段上,例如

  @UserEmailValidator
   private String email;

由于ConstraintValidator的方法isValid中的参数ConstraintValidatorContext 里拿不到email对应的类(User)

public boolean isValid(String value, ConstraintValidatorContext context) {
}

所以要想从context里获取到email,可以通过改源码来实现,主要是修改2个类

  • org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorContextImpl
    这个类是ConstraintValidatorContext的实现类,在这个类中,增加了Object rootbean,以及getter和setter方法,如下:
public class ConstraintValidatorContextImpl implements HibernateConstraintValidatorContext {
 private Object rootbean;
  public Object getRootbean() {
        return rootbean;
    }
    public void setRootbean(Object rootbean) {
        this.rootbean = rootbean;
    }
 }
  • org.hibernate.validator.internal.engine.constraintvalidation.SimpleConstraintTree
    这个类是构造出ConstraintValidatorContextImpl的地方,需要将rootbean塞值。
    注意下面//!!!的标注
package org.hibernate.validator.internal.engine.constraintvalidation;
class SimpleConstraintTree<B extends Annotation> extends ConstraintTree<B> {

    private static final Log LOG = LoggerFactory.make( MethodHandles.lookup() );

    public SimpleConstraintTree(ConstraintDescriptorImpl<B> descriptor, Type validatedValueType) {
        super( descriptor, validatedValueType );
    }

    @Override
    protected <T> void validateConstraints(ValidationContext<T> validationContext,
                                           ValueContext<?, ?> valueContext,
                                           Set<ConstraintViolation<T>> constraintViolations) {

        if ( LOG.isTraceEnabled() ) {
            LOG.tracef(
                    "Validating value %s against constraint defined by %s.",
                    valueContext.getCurrentValidatedValue(),
                    descriptor
            );
        }

        // find the right constraint validator
        ConstraintValidator<B, ?> validator = getInitializedConstraintValidator( validationContext, valueContext );

        // create a constraint validator context
        ConstraintValidatorContextImpl constraintValidatorContext = new ConstraintValidatorContextImpl(
                validationContext.getParameterNames(),
                validationContext.getClockProvider(),
                valueContext.getPropertyPath(),
                descriptor,
                validationContext.getConstraintValidatorPayload()
        );
        
//!!! 就是这行了,将valueContext.getCurrentBean()设置到context中
        constraintValidatorContext.setRootbean(valueContext.getCurrentBean());
        // validate
        constraintViolations.addAll(
                validateSingleConstraint(
                        validationContext,
                        valueContext,
                        constraintValidatorContext,
                        validator
                )
        );
    }
}

这样UserEmailConstraintValidator 就可以改成如下

public class UserEmailConstraintValidator implements ConstraintValidator<UserEmailValidator,String> {
       public boolean isValid(String  email, ConstraintValidatorContext context) {
        Object root=((ConstraintValidatorContextImpl)context).getRootbean();
        if(root instanceof User){
            User user=(User)root;
            if (user.isHasEmail()&& user.getEmail()==null) {
                return false;
            }else{
                return true;
            }
        }
        return false;
    }

    public void initialize(UserEmailValidator constraintAnnotation) {
    }
}
spring validator

对于某些校验只会有一次使用,并不需要复用。例如对于上面提要的email的依赖hasEmail为true的时候才进行校验。基本上不会是复用,对于这类的校验还需要去写个annotaion和validation的类就显得不太合适。因为我们写annotation的目的就是为了复用。对于这样的,还有一种就是使用spring提供的接口类:org.springframework.validation.Validator注意区别于javax.validation.Validator

使用的时候放在controller里的 @InitBinder

@Controller
public class UserController{

    @RequestMapping("/user/save")
    public String save(@Valid @ModelAttribute("user") User user){
            return "user_detail";
    }

    
  @InitBinder
    protected void initBinder(WebDataBinder binder) {
        //binder里已经含有全局的BeanValidator提供的validator
        //这里可以再加如一个本地的validator
        binder.addValidators(new UserValidator());
    }

    static class UserValidator implements org.springframework.validation.Validator{
        public boolean supports(Class<?> clazz) {
            return User.class.isAssignableFrom(clazz);
        }

        public void validate(Object target, Errors errors) {
            User  user = (User) target;
            if (user.isHasEmail() && StringUtils.isEmpty(user.getMail())) {
                errors.reject("field.email.empty",
                        "the email can not empty");
            }
            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "field.required","password can not be empty");
        }
    }
 }

User 类

public class User{
 private boolean hasEmail;
 private String email;
 private String password;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值