为什么会写?
最近项目中用到了校验,原来的校验逻辑都是在代码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;
}