Hibernate Validator实战篇

在写程序的时候经常需要进行数据校验,比如服务端对http请求参数校验,数据入库时对字段长度进行校验,接口参数校验,可以说数据校验遍布应用程序代码中,就像下图所示:


application-layers.png

为了减少代码重复率,开发人员将数据校验的逻辑直接加载数据模型中。JSR380定义了一套bean校验的元数据模型,将数据的约束定义在数据模型中。

Paste_Image.png

而hibernate validator则实现了这样一套规范。
下面我们来看看如何使用hibernate进行bean校验已经方法参数校验。

准备

下载hibernate validator的依赖包,这里全部使用maven管理依赖。

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.1.Final</version>
</dependency>
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.el</artifactId>
    <version>3.0.1-b08</version>
</dependency>

校验bean的属性

  1. 先定义一个需要校验的类,这里采用hibernate validator官方的例子
public class Car {
   @NotNull @Size(min = 2, max = 14)
   private String manufacturer;
   public Car(String manufacturer) {
      this.manufacturer = manufacturer;
   }
   public void drive(@Max(50) int speedInMph) {
     // ...
   }
  // getter setter
}
  1. 获取Validator
  ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
                           .configure()
                           .buildValidatorFactory();
   Validator validator = factory.getValidator();
  1. 使用Validator校验Car
  Car car = new Car(null);
  Set<ConstraintViolation<Car>> violations = validator.validate(car);
  assertEquals(1, violations.size());

对于bean的校验首先需要确定要校验哪些类,在这些类的属性添加各种约束(比如@NotNull),通过java.validation.Validation获取Validator,通过validator校验对象,获取校验的结果,其校验结果都是返回一个包含ConstraintViolation对象的集合。

校验方法中的参数

比如要校验Car中drive方法中的参数。

  1. 首先获取ExecutableValidator
ExecutableValidator executableValidator = validator.forExecutables();

通过之前获取的Validator得到ExecutableValidater.

  1. 通过ExecutableValidator校验drive方法中的参数
Car object = new Car("Morris");
Method method = RacingCar.class.getMethod( "drive", int.class );
Object[] parameterValues = { 90 };
Set<ConstraintViolation> violations = executableValidator.validateParameters(
                object,
                method,
                parameterValues
        );
// assertEquals(1, violations.size() );

通过这个例子能够最bean的属性和方法进行简单的校验,但是我们经常遇到需要校验的场景有:

  1. 对象的级联校验;
    比如Car中引用一个对象Driver,在校验Car的同时也需要校验Driver中的属性。
class Car {
  @NotNull
   private String manufacturer;
   private Driver driver;
}
  1. 对象中关联参数联合校验;
    比如Car有两个属性,座位数和乘客数,要求乘客数量不能大于座位数。
class Car {
  @Max(20)
  private int seatCount;
  // passengers.size() <= seatCount
  private List<Passenger> passengers;
}
  1. 方法中参数关联校验;
    比如Car对象的方法buildCar的签名如下:
public Car buildCar(int seatCount, List<Passenger> passengers) {
  // ...
  return null;
}

要求乘客数量小于座位数。

  1. 默认情况下,validator会对被校验对象的所有属性进行校验,能否只校验一部分?
  2. 如何自定义约束呢?
  3. 如何自定义校验结果中的message呢?
    下面一起来回答前面提到的几个问题。

级联校验

通过在属性上添加@Valid注解就可以进行级联校验了。如下:

class Car {
  @NotNull
  private String manufacturer;
  @Valid
  private Driver driver;

  // getter setter
}

这样在校验Car的时候,也同时会校验Driver中的属性。

自定义约束

1.首先定义一个约束,约束是一个注解形式,相当于定义个注解,我们需要确定这个注解是用于属性、类还是方法上等,其次约束注解需要提供几个固定的方法,最后确定这个约束需要的自定义方法。例如要验证汽车的载客人数不能超过座位数的约束。

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
//必须添加下面这个注解,实际校验的时候将使用指定的Validator进行校验
@Constraint(validateBy = {ValidPassengerCount.Validator.class}) 
public @interface ValidPassengerCount {
// 固定需要添加的方法
  String message() default 'validatePassengerCount.message';
// 固定需要添加的方法
Class<?>[] groups() default {};
// 固定需要添加的方法
Class<? extends Payload> payload() default {};

class Validator implements ConstraintValidator<ValidPassengerCount, Car> {

        @Override
        public void initialize(ValidPassengerCount constraintAnnotation) {

        }

        @Override
        public boolean isValid(Car value, ConstraintValidatorContext context) {
            if (value.getPassengers().size() > value.getPassengers().size()) {
                return false;
            }
            return true;
        }
    }

上面自定义的约束没有添加自定义的业务属性,但可以添加任何自定义的方法,然后在Validator的initialize方法中,通过ValidatorPassengerCount获取自定义方法放回的结果保存在Validator中,然后在isValid方法使用;
对于isValid方法的返回类型是boolean型,校验通过返回true,校验失败返回false。
2.在需要约束的类定义中添加自定义的约束,已Car为例,如下:

@ValidPassengerCount
class Car {
  @Min(4)
  private int seatCount;
  @NotNull
  private List<Passenger> passengers;
  // getter setter
}

约束分组(GROUP)

约束分组用来实现部分校验的功能,例如我们在Car的fields上添加了较多约束,但是在有些场景中我们只需要验证car的部分属性,虽然这种场景的使用应不多,但我们如何实现这种功能呢?
通过前面的例子我们可以看到,在每一个约束中都包含一个groups的属性,返回class数组,Validator的validate方法也提供一个输入groups的参数,我想大家都明白groups是怎么用的了,对,我们就是可以使用groups实现之校验该跟分组的约束。示例如下:

public class Driver  {

@NotNull
public String name;

    @Min(
            value = 18,
            message = "You have to be 18 to drive a car",
            groups = DriverChecks.class
    )
    public int age;

    @AssertTrue(
            message = "You first have to pass the driving test",
            groups = DriverChecks.class
    )
    public boolean hasDrivingLicense;

    public Driver(String name) {
       this.name = name;
    }

    public void passedDrivingTest(boolean b) {
        hasDrivingLicense = b;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
public interface DriverChecks {
}

上面的示例代码中使用了两种group,DEFAULT和DriverChecks,Driver.name属于DEFAULT 组,Driver.age和Driver.hasDrivingLicense属于DriverChecks分组,如果只想校验Driver.name,只需要参照如下示例:

validator.validate(driver, DEFAULT.class);

如果只想校验Driver.age和Driver.hasDrivingLicense,参考如下示例:

validator.validate(driver, DriverChecks.class);

如果想同时Driver.name、Driver.age和Driver.hasDrivingLicense,参考如下示例:

validator.validate(driver, DEFAULT.class, DriverChecks.class);

校验的顺序与group的先后顺序一致。

自定义message

自定义message可以通过多种方式来实现。

  1. 在属性或者方法参数上添加约束注解时,可以在约束的message属性上设置自定义的message。如下:
class Car {
  @NotNull("car.menufacturer can't be null")
  private String manufacturer;
}
  1. 通过在构建Validator实例的过程中,配置ValidatorFactory时,设置一个MessageInterpolater,如下:
validator = Validation.byProvider(HibernateValidator.class)
                .configure()
                .messageInterpolator(new ResourceBundleMessageInterpolator(new PlatformResourceBundleLocator("message/validate_message")))
                .buildValidatorFactory()
                .getValidator();

然后在classpath的message目录下添加validate_message.properties文件,将要翻译的信息添加到该文件中:

image.png

这个是hibernate-validator jar中ValidationMessages.properties文件中的内容,默认key的规则都是以定义的约束的class的全限量名加".message"组成。

  1. 自定义约束设置message
    自定义约束时,在约束的message属性上设置default值,如下:
String message() default "{javax.validation.constraints.NotNull.message}";
  1. 在指定约束的校验器中,覆盖默认message,如下:
constraintContext.disableDefaultConstraintViolation();           
constraintContext.buildConstraintViolationWithTemplate( "{com.mycompany.constraints.CheckCase.message}"  ).addConstraintViolation();       
 }

hibernate-validator还包含一些其他的特性,就不细说了。下面我们看看如何将hibernate-validator应用到实际的开发工作中去。

应用例子

对于使用springMvc框架的,要在Controller中校验方法参数只需要在参数上注解@Valid和一些约束注解就可以了。
在使用一些rpc通讯框架时,一般这些rpc框架都不会集成一些参数校验的组件,需要我们自己写,这个时候我们就可以采用hibernate-validator组件了,这个相比自己去写校验组件真是快多了。
一般采用rpc做服务实现时,在服务实现的第一层,我们通过配置aop的方式对服务实现类进行代理,在代理中添加校验的逻辑。如下:

  1. 写一个通过hibernate-validator进行校验的类,如下:
public class HibernateValidateService implements ValidateService {

    private static HibernateValidateService INSTANCE;

    private static ExecutableValidator validator;

    static {
        validator = Validation.byProvider(HibernateValidator.class)
                .configure()
                .failFast(true)
                .ignoreXmlConfiguration()
                .parameterNameProvider(new ParanamerParameterNameProvider())
                .messageInterpolator(new ResourceBundleMessageInterpolator(new PlatformResourceBundleLocator("message/validate_message")))
                .buildValidatorFactory()
                .getValidator()
                .forExecutables();
    }


    public <T> void validate(T obj, Method method, Object[] parameters) throws OspException {
        Set<ConstraintViolation<T>> violations = validator.validateParameters(obj, method, parameters);
        if (!violations.isEmpty()) {
            ConstraintViolation<T> violation = violations.iterator().next();
            throw new IllegalArgumentException(buildErrorMsg(violation));
        }
    }

    private <T> String buildErrorMsg(ConstraintViolation<T> violation) {
        Iterator<Path.Node> propertyNodes = violation.getPropertyPath().iterator();
        // skip method name
        propertyNodes.next();

        StringBuilder sb = new StringBuilder();
        while (propertyNodes.hasNext()) {
            sb.append(propertyNodes.next().getName());
            if (propertyNodes.hasNext()) {
                sb.append(".");
            } else {
                sb.append(" ");
            }
        }
        return sb.append(violation.getMessage()).toString();
    }

    public static ValidateService getInstance() {
        if (INSTANCE == null) {
            synchronized (HibernateValidateService.class) {
                if (INSTANCE == null) {
                    INSTANCE = new HibernateValidateService();
                }
            }
        }
        return INSTANCE;
    }

}
  1. 编写aop advice
public class ValidateBeforeAdvice implements MethodBeforeAdvice {
    private ValidateService validateService = HibernateValidateService.getInstance();
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        validateService.validate(target, method, args);
    }
}
  1. 配置服务实现层(api层)代理
 <bean id="validateBeforeAdvice" class="advice.ValidateBeforeAdvice" />
<aop:config proxy-target-class="false">
        <aop:pointcut id="pointcut" expression="execution(* api.validate..*(..))" />
        <aop:advisor advice-ref="validateBeforeAdvice" pointcut-ref="pointcut" />
    </aop:config>

这里有个注意点就是api层抛出了什么类型的异常,在写校验的时候也应该抛出什么类型的异常。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容