java对象校验(validation)-JSR303规范

小TOT 创建于 2017-02-07

JSR303 规范简介

web开发有一句名言:永远不要相信用户输入,在任何时候,当你要处理一个应用程序的业务逻辑,数据校验是你必须要考虑和面对的事情。应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的。在通常的情况下,应用程序是分层的,不同的层由不同的开发人员来完成。很多时候同样的数据验证逻辑会出现在不同的层,这样就会导致代码冗余和一些管理的问题,比如说语义的一致性等。为了避免这样的情况发生,最好是将验证逻辑与相应的域模型进行绑定。于是就有了JSR303。JSR 303 – Bean Validation 是一个数据验证的规范,2009 年 11 月确定最终方案。2009 年 12 月 Java EE 6 发布,Bean Validation 作为一个重要特性被包含其中。参见官方文档

JSR303一些验证约束

要将验证逻辑绑定到bean上,我们可能最容易显想到的方法是,在bean中写一个isValide(),方法即可完成业务需求。但是大部分的验证可能是相似的,比如限制某个字段不能为空,限制某个数据类型的取值范围,限制某个字段符合某个正则表达式等。因此优雅的做法是在java bean中加上注解用来表明限制,让后使用统一的验证器来对bean进行合法性验证。JSR303与jdbc其规范一样,只制定规范实现由都三方来实现。使用做多最有名的是Hibernate Validator。Hibernate Validator在实现jsr303的基础上,还进行了自己一些扩展。

下面是JSR303一些制定的一些注解:

Constraint(约束) 说明
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(value) 被注释的元素必须符合指定的正则表达式

Hibernate Validator 附加的 constraint

Constraint(约束) 说明
@Email 被注释的元素必须是电子邮箱地址
@Length 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range 被注释的元素必须在合适的范围内

用户自定义(custom)约束

用户也可以编写符合自己业务需求的约束,和验证器。

bean validation简单使用

Bean Validation 规范规定在对 Java Bean 进行约束验证前,目标元素必须满足以下条件:

  • 如果验证的是属性(getter 方法),那么必须遵从 Java Bean 的命名习惯(JavaBeans 规范);
  • 静态的字段和方法不能进行约束验证;
  • 约束适用于接口和基类;
  • 约束注解定义的目标元素可以是字段、属性或者类型等;
  • 可以在类或者接口上使用约束验证,它将对该类或实现该接口的实例进行状态验证;
  • 字段和属性均可以使用约束验证,但是不能将相同的约束重复声明在字段和相关属性(字段的 getter 方法)上。

下面是bean validation 的简单步奏。

1. 导包

本文JSR303的实现的是使用的Hibernate Validator,使用maven进行管理,因此需要在maven 的pom.xml文件中添加如下依赖。本文使用的javax包为1.1版本。实际上1.1是bean validation的新规范,随java EE 7一同推出的。因为hibernate-validator version 5+才支持bean validation1.1,并且1.1支持了el表达式因此同时也添加了el表达式包。使用bean validation1.1(JSR349)并不影响我们对bean validation1.1(JSR303)的验证,因为规范是向下兼容的。

<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.1.0.Final</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-validator -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.2.4.Final</version>
</dependency>

<!-- https://mvnrepository.com/artifact/javax.el/el-api -->
<dependency>
    <groupId>javax.el</groupId>
    <artifactId>el-api</artifactId>
    <version>2.2</version>
</dependency>

2. bean的书写

// User.java
public class User {

  @NotNull(message = "姓名不能为空")
  private String name;
  @Min(value = 1 ,message = "年龄不能小于0")
  @NotNull(message = "age不能为空")
  private Integer age;
  @NotNull(message = "id不能为空")
  private Integer id;
  @Valid
  @NotNull
  private Dog dog;

  //省略get set方法
}
// Dog.java
public class Dog {
  @NotBlank
  private String type;
  @NotBlank
  private String name;
  @NotBlank
  private String gender;
  //省略get set方法
}

3. 验证代码

// Main.java
public class Main {
    public static void main(String[] args) {
        User user = new User();
        user.setDog(new Dog());
        validate(user);
    }

    static void validate(Object o){
        ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
        Validator validator = vf.getValidator();
        Set<ConstraintViolation<Object>> set = validator.validate(o);
        for (ConstraintViolation<Object> constraintViolation : set) {
            System.out.println(constraintViolation.getPropertyPath()+":"+constraintViolation.getMessage());
        }
    }

}

4. 示例说明:

运行商代码应该不出意外应该可以得到如下验证信息:

    name:姓名不能为空
    id:id不能为空
    phoneNumber:电话号码格式不正确
    age:age不能为空
    dog.name:不能为空
    dog.type:不能为空
    dog.gender:不能为空

以上注解,基本都比较好懂。注解里面的message属性是可选的,不填则用默认的提示。默认的提示配置会根据位置信息给出友好的提示,在cn地区则会以如上因为进行提示,国际化配置文件位于包hibernate-validator-5.2.4.Final.jar下/org/hibernate/validator/ValidationMessages.properties还需要注意的,当校验的对象还依赖其他对象时,验证依赖对象需要用到JSR303级联特性,要验证依赖对象需要加@Valid注解。

自定义约束&约束验证器

想要定义自己的规则,需要完成约束注解的定义,对应的验证器的定义。下面来看看官方的实现。

典型的约束定义

// length约束,带参数
@Documented
@Constraint(
    validatedBy = {}//这里没有指定相应的验证器,实际上在org.hibernate.validator.internal.metadata.core.ConstraintHelper初始化的时候,已经加上。
) 

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Length {
    int min() default 0;

    int max() default 2147483647;

    String message() default "{org.hibernate.validator.constraints.Length.message}";

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

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

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        Length[] value();
    }
}

// notBlank约束,不带参数
@Documented
@Constraint(
    validatedBy = {}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@ReportAsSingleViolation
@NotNull
public @interface NotBlank {
    String message() default "{org.hibernate.validator.constraints.NotBlank.message}";

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

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

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        NotBlank[] value();
    }
}

对应的校验器

// length的约束验证器
public class LengthValidator implements ConstraintValidator<Length, CharSequence> {
    private static final Log log = LoggerFactory.make();
    private int min;
    private int max;

    public LengthValidator() {
    }

    public void initialize(Length parameters) {
        this.min = parameters.min();
        this.max = parameters.max();
        this.validateParameters();
    }

    public boolean isValid(CharSequence value, ConstraintValidatorContext constraintValidatorContext) {
        if(value == null) {
            return true;
        } else {
            int length = value.length();
            return length >= this.min && length <= this.max;
        }
    }

    private void validateParameters() {
        if(this.min < 0) {
            throw log.getMinCannotBeNegativeException();
        } else if(this.max < 0) {
            throw log.getMaxCannotBeNegativeException();
        } else if(this.max < this.min) {
            throw log.getLengthCannotBeNegativeException();
        }
    }
}

// notBlank的约束验证器

public class NotBlankValidator implements ConstraintValidator<NotBlank, CharSequence> {
    public NotBlankValidator() {
    }

    public void initialize(NotBlank annotation) {
    }

    public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
        return charSequence == null?true:charSequence.toString().trim().length() > 0;
    }
}

一个符合规范的约束注解至少应该包含message,group,payLoad等3个字段,message用于校验信息提示,group用于分组校验,payLoad常用来将一些元数据信息与该约束注解相关联,常用的一种情况是用负载表示验证结果的严重程度

     @Target({ })   // 约束注解应用的目标元素类型
     @Retention()   // 约束注解应用的时机
     @Constraint(validatedBy ={})  // 与约束注解关联的验证器
     public @interface ConstraintName{ 
     String message() default " ";   // 约束注解验证时的输出消息
     Class<?>[] groups() default { };  // 约束注解在验证时所属的组别
     Class<? extends Payload>[] payload() default { }; // 约束注解的有效负载
     }

一个验证器需要实现ConstraintValidator接口,如下。

public class ValidatorName implements ConstraintValidator<ConstraintAnnotation,TargetValiValue> {



    public void initialize(ConstraintAnnotation constraintAnnotation) {

        //初始化验证器,参数为注解对象,可以将注解里面属性值注入到该对象供校验时使用。例如Length约束注解的max,min注解。
    }

    public boolean isValid(TargetValiValue targetValue, ConstraintValidatorContext constraintValidatorContext) {
       //校验逻辑,校验通过,返回true,校验不同过返回false,参数为校验目标的值,以及校验器的上下文,通过上下午可以获取验证更多内容
    }

}

自定义校验示例

目标:定义一个校验手机号的的验证器。

1. 定义注解:

@Target({ElementType.FIELD,ElementType.METHOD,ElementType.PARAMETER})//申明注解的作用位置
@Retention(RetentionPolicy.RUNTIME)//运行时机
@Constraint(validatedBy={MobilePhoneValidator.class})//定义对应的校验器,自定义注解必须指定
public @interface MobileNumber {
    String message() default "电话号码格式不正确";//错误提示信息默认值,可以使用el表达式。

    Class<?>[] groups() default {};//约束注解在验证时所属的组别

    Class<? extends Payload>[] payload() default {};//约束注解的有效负载

}

2. 定义验证器

public class MobilePhoneValidator implements ConstraintValidator<MobileNumber,String> {

    private final Pattern mobilePhonePattern = Pattern.compile("1([\\d]{10})");

    public void initialize(MobileNumber mobileNumber) {
        //do nothing
    }

    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if(s == null){
            return false;
        }
        return mobilePhonePattern.matcher(s).matches();
    }

}

3. 使用

public class User {

    @NotNull(message = "姓名不能为空")
    private String name;
    @Min(value = 1 ,message = "年龄不能小于0")
    @NotNull(message = "age不能为空")

    private Integer age;
    @NotNull(message = "id不能为空")
    private Integer id;
    @MobileNumber(payload = MobilePayLoad.class)
    private String phoneNumber;
    //省略get set方法
}

//验证代码

public static void main(String[] args) {
    User user = new User();
    user.setPhoneNumber("136896972");
    validate(user);
}

static void validate(Object o){
    ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
    Validator validator = vf.getValidator();
    Set<ConstraintViolation<Object>> set = validator.validate(o);
    for (ConstraintViolation<Object> constraintViolation : set) {
        System.out.println(constraintViolation.getPropertyPath()+":"+constraintViolation.getMessage());
    }
}

JSR303常用特性介绍

1. 级联校验

上面有提到,当一个对象依赖另外一个对象在校验对象时需要校验管理对象就需要用到规范的级联特性。这个具体实现由bean validation来进行,我们只需要在关联字段上添加。@Vali注解即可。

2. 分组验证

试想这样的场景,我们定义了一个公共的类,在不同的业务方法里,我们我们需要对其进行不同的校验,比如在业务a,需要a约束,在业务b需要b约束。我们很自然的想到编写两套bean不就可以了,若这样做我们的bean类就不能重用了,好在JSR303已经支持分组验证了,我们在签名的约束注解定义中发现,有一个字段Class<?>[] groups() default {},该字段即用来做分组验证的。下面介绍如何使用。

public interface Group {
    interface A{}
    interface B{}
}

public class Person {
    // Group.A.class为小孩约束
    // Group.B.class为成人约束
    @NotEmpty(groups = {Group.A.class,Group.B.class})
    private String name;

    @Min(value = 20,groups = Group.B.class)
    @Max(value = 14,groups = Group.A.class)
    private Integer age;

    //省略get set方法

    public static void main(String[] args) {
        ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
        Validator validator = vf.getValidator();
        Person child = new Person();
        child.setName("小明同学");
        child.setAge(20);//20岁的小孩不合法,待会我们使用Group.A.class这一组约束进行验证
        System.out.println("下面验证小明同学是不是是不是孩子");
        Set<ConstraintViolation<Person>> set = validator.validate(child, Group.A.class);
        for (ConstraintViolation<Person> constraintViolation : set) {
            System.out.println(constraintViolation.getPropertyPath()+":"+constraintViolation.getMessage());
        }
        System.out.println("下面验证怪叔叔是不是成人");
        Person adult = new Person();
        adult.setName("怪叔叔");
        adult.setAge(18);//18岁不能叫叔叔,我们待会使用Group.B.class这一组进行验证
        Set<ConstraintViolation<Person>> set02 = validator.validate(adult, Group.B.class);
        for (ConstraintViolation<Person> constraintViolation : set02) {
            System.out.println(constraintViolation.getPropertyPath()+":"+constraintViolation.getMessage());
        }

    }
}

输出结果:
下面验证小明同学是不是是不是孩子
age:最大不能超过14
下面验证怪叔叔是不是成人
age:最小不能小于20

3.组序列验证

默认情况下,不同组别的约束验证是无序的,然而在某些情况下,约束验证的顺序却很重要,如下面两个例子:(1)第二个组中的约束验证依赖于一个稳定状态来运行,而这个稳定状态是由第一个组来进行验证的。(2)某个组的验证比较耗时,CPU 和内存的使用率相对比较大,最优的选择是将其放在最后进行验证。因此,在进行组验证的时候尚需提供一种有序的验证方式,这就提出了组序列的概念。一个组可以定义为其他组的序列,使用它进行验证的时候必须符合该序列规定的顺序。在使用组序列验证的时候,如果序列前边的组验证失败,则后面的组将不再给予验证。

@GroupSequence({Group.Default.class,Group.A.class,Group.B.class})
public interface Group {
    interface Default{}
    interface A{}
    interface B{}
}

public class Name {
    @NotBlank(groups = Group.Default.class)
    private String firstName;
    @NotBlank(groups = Group.A.class)
    private String middleName;
    @NotBlank(groups = Group.B.class)
    private String lastName;
    @NotBlank //没有指定组,当使用组验证时,忽略该字段
    private String belongCompanyName;
    //省略get set方法

    public static void main(String[] args) {
        ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
        Validator validator = vf.getValidator();

        Name name01 = new Name();
        System.out.println("校验name01==========>");
        Set<ConstraintViolation<Name>> set01 = validator.validate(name01, Group.class);
        for (ConstraintViolation<Name> constraintViolation : set01) {
            System.out.println(constraintViolation.getPropertyPath()+":"+constraintViolation.getMessage());
        }

        Name name02 = new Name();
        name02.setFirstName("firstName");
        name02.setMiddleName("middleName");
        System.out.println("校验name02===========>");
        Set<ConstraintViolation<Name>> set02 = validator.validate(name02, Group.class);
        for (ConstraintViolation<Name> constraintViolation : set02) {
            System.out.println(constraintViolation.getPropertyPath()+":"+constraintViolation.getMessage());
        }


    }
}


得到如下结果:
校验name01==========>
firstName:不能为空
校验name02===========>
lastName:不能为空

name01的firstName验证不通过,中断验证,后面验证被取消。
name02的firstName,middleName验证通过,lastName验证没通过

4. 组合约束

Bean Validation 规范允许将不同的约束进行组合来创建级别较高且功能较多的约束,从而避免原子级别约束的重复使用。 查看hibernate 中@NotEmpty约束

    @Documented
    @Constraint(
        validatedBy = {}
    )
    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @ReportAsSingleViolation
    @NotNull
    @Size(
        min = 1
    )
    public @interface NotEmpty {
        String message() default "{org.hibernate.validator.constraints.NotEmpty.message}";

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

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

        @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        public @interface List {
            NotEmpty[] value();
        }
    }

我们试图去找对应的NotEmptyValidator时,竟然找不到!再看看NotEmpty约束注解内容,发现有两个我们熟悉的注解@NotNull,@Size(min=1)。以上两个组合起来不正是NotEmpty的含义么?看到这里相信你已经组合约束是怎么回事了。实际使用中 @NotEmpty2 约束注解可以得到与 @NotEmpty 约束注解同样的验证结果。

the END .....