Java的业务逻辑验证框架fluent-validator

1 背景

在互联网行业中,基于Java开发的业务类系统,不管是服务端还是客户端,业务逻辑代码的更新往往是非常频繁的,这源于功能的快速迭代特性。在一般公司内部,特别是使用Java web技术构建的平台中,不管是基于模块化还是服务化的,业务逻辑都会相对复杂。

这些系统之间、系统内部往往存在大量的API接口,这些接口一般都需要对入参(输入参数的简称)做校验,以保证: 
1) 核心业务逻辑能够顺利按照预期执行。 
2) 数据能够正常存取。 
3) 数据安全性。包括符合约束以及限制,有访问权限控制以及不出现SQL注入等问题。

开发人员在维护核心业务逻辑的同时,还需要为输入做严格的校验。当输入不合法时,能够给caller一个明确的反馈,最常见的反馈就是返回封装了result的对象或者抛出exception。

一些常见的验证代码片段如下所示:

  1. public Response execute ( Request request ) {
  2. if ( request == null ) {
  3. throw BizException ();
  4. }
  5. List cars = request . getCars ();
  6. if ( CollectionUtils . isEmpty ( cars )) {
  7. throw BizException ();
  8. }
  9. for ( Car car : cars ) {
  10. if ( car . getSeatCount () < 2 ) {
  11. throw BizException ();
  12. }
  13. }
  14. // do core business logic
  15. }

我们不能说这是反模式(anti-pattern),但是从中我们可以发现,它不够优雅而且违反一些范式: 
1)违反 单一职责原则 (Single responsibility)。核心业务逻辑(core business logic)和验证逻辑(validation logic)耦合在一个类中。 
2) 开闭原则 (Open/closed)。我们应该对扩展开放,对修改封闭,验证逻辑不好扩展,而且一旦需要修改需要动整体这个类。 
3) DRY原则 (Don’t repeat yourself)。代码冗余,相同逻辑可能散落多处,长此以往不好收殓。

2 为何要使用FluentValidator

原因很简单,第一为了优雅,出色的程序员都有点洁癖,都希望让验证看起来很舒服;第二,为了尽最大可能符合这些优秀的原则,做clean code。

FluentValidator就是这么一个工具类库,适用于以Java语言开发的程序,让开发人员回归focus到业务逻辑上,使用流式(Fluent Interface)调用风格让验证跑起来很优雅,同时验证器(Validator)可以做到开闭原则,实现最大程度的复用。

3 FluentValidator特点

这里算是Quick learn了解下,也当且看做打广告吧,看了这些特点,希望能给你往下继续阅读的兴趣:)

1) 验证逻辑与业务逻辑不再耦合 
摒弃原来不规范的验证逻辑散落的现象。

2) 校验器各司其职,好维护,可复用,可扩展 
一个校验器(Validator)只负责某个属性或者对象的校验,可以做到职责单一,易于维护,并且可复用。

3) 流式风格(Fluent Interface)调用 
借助 Martin大神提倡的流式API风格 ,使用“ 惰性求值(Lazy evaluation) ”式的链式调用,类似 guava 、 Java8 stream API 的使用体验。

4) 使用注解方式验证 
可以装饰在属性上,减少硬编码量。

5) 支持 JSR 303 – Bean Validation 标准 
或许你已经使用了 Hibernate Validator ,不用抛弃它,FluentValidator可以站在巨人的肩膀上。

6) Spring良好集成 
校验器可以由Spring IoC容器托管。校验入参可以直接使用注解,配置好拦截器,核心业务逻辑完全没有验证逻辑的影子,干净利落。

7) 回调给予你充分的自由度 
验证过程中发生的错误、异常,验证结果的返回,开发人员都可以定制。

4 哪里可以获取到FluentValidator

项目托管在github上,地址点此 https://github.com/neoremind/fluent-validator 。说明文档全英完成,i18n化,同时使用Apache2 License开源。

最新发布的Jar包可以在maven中央仓库找到,地址 点此 

5 上手

5.1 maven引入依赖

添加如下依赖到maven的pom.xml文件中:

  1. <dependency>
  2. <groupId> com.baidu.unbiz </groupId>
  3. <artifactId> fluent-validator </artifactId>
  4. <version> 1.0.5 </version>
  5. </dependency>

注:最新release请及时参考 github 

上面这个FluentValidator是个基础核心包,只依赖于slf4j和log4j,如果你使用logback,想去掉log4j,排除掉的方法如下所示:

  1. <dependency>
  2. <groupId> com.baidu.unbiz </groupId>
  3. <artifactId> fluent-validator </artifactId>
  4. <exclusions>
  5. <exclusion>
  6. <groupId> org.slf4j </groupId>
  7. <artifactId> slf4j-log4j12 </artifactId>
  8. </exclusion>
  9. </exclusions>
  10. </dependency>

5.2 开发业务领域模型

从广义角度来说DTO(Data Transfer Object)、VO(Value Object)、BO(Business Object)、POJO等都可以看做是业务表达模型。

我们这里创建一个汽车类(Car)的POJO,里面定义了牌照(license plate)、座椅数(seat count)、生产商(manufacturer)。

  1. public class Car {
  2. private String manufacturer ;
  3. private String licensePlate ;
  4. private int seatCount ;
  5. // getter and setter...
  6. }

5.3 开发一个专职的Validator

实际这里需要开发三个Validator,分别对Car的3个属性进行校验,这里以座椅数为例展示如何开发一个Validator,其他两个省略。

  1. public class CarSeatCountValidator extends ValidatorHandler < Integer > implements Validator < Integer > {
  2. @Override
  3. public boolean validate ( ValidatorContext context , Integer t ) {
  4. if ( t < 2 ) {
  5. context . addErrorMsg ( String . format ( "Seat count is not valid, invalid value=%s" , t ));
  6. return false ;
  7. }
  8. return true ;
  9. }
  10. }

很简单,实现Validator接口,泛型T规范这个校验器待验证的对象的类型,继承ValidatorHandler可以避免实现一些默认的方法,例如accept(),后面会提到,validate()方法第一个参数是整个校验过程的上下文,第二个参数是待验证对象,也就是座椅数。

验证逻辑很简单,座椅数必须大于1,否则通过context放入错误消息并且返回false,成功返回true。

5.4 开始验证吧

二话不说,直接上代码:

  1. Car car = getCar ();
  2. Result ret = FluentValidator . checkAll ()
  3. . on ( car . getLicensePlate (), new CarLicensePlateValidator ())
  4. . on ( car . getManufacturer (), new CarManufacturerValidator ())
  5. . on ( car . getSeatCount (), new CarSeatCountValidator ())
  6. . doValidate ()
  7. . result ( toSimple ());
  8. System . out . println ( ret );

我想不用多说,如果你会英文,你就能知道这段代码是如何工作的。这就是流式风格(Fluent Interface)调用的优雅之处,让代码更可读、更好理解。

还是稍微说明下,首先我们通过FluentValidator.checkAll()获取了一个FluentValidator实例,紧接着调用了failFast()表示有错了立即返回,它的反义词是failOver,然后,一连串on()操作表示在Car的3个属性上依次使用3个校验器进行校验(这个过程叫做applying constraints),截止到此,真正的校验还并没有做,这就是所谓的“惰性求值(Lazy valuation)”,有点像Java8 Stream API中的filter()、map()方法,直到doValidate()验证才真正执行了,最后我们需要收殓出来一个结果供caller获取打印,直接使用默认提供的静态方法toSimple()来做一个回调函数传入result()方法,最终返回Result类,如果座椅数不合法,那么控制台打印结果如下:

Result{isSuccess=false, errors=[Seat count is not valid, invalid value=99]}

6 深入实践

6.1 Validator详解

Validator接口定义如下:

  1. public interface Validator < T > {
  2. /**
  3. * 判断在该对象上是否接受或者需要验证
  4. * <p/>
  5. * 如果返回true,那么则调用{@link #validate(ValidatorContext, Object)},否则跳过该验证器
  6. *
  7. * @param context 验证上下文
  8. * @param t 待验证对象
  9. *
  10. * @return 是否接受验证
  11. */
  12. boolean accept ( ValidatorContext context , T t );
  13. /**
  14. * 执行验证
  15. * <p/>
  16. * 如果发生错误内部需要调用{@link ValidatorContext#addErrorMsg(String)}方法,也即<code>context.addErrorMsg(String)
  17. * </code>来添加错误,该错误会被添加到结果存根{@link Result}的错误消息列表中。
  18. *
  19. * @param context 验证上下文
  20. * @param t 待验证对象
  21. *
  22. * @return 是否验证通过
  23. */
  24. boolean validate ( ValidatorContext context , T t );
  25. /**
  26. * 异常回调
  27. * <p/>
  28. * 当执行{@link #accept(ValidatorContext, Object)}或者{@link #validate(ValidatorContext, Object)}发生异常时的如何处理
  29. *
  30. * @param e 异常
  31. * @param context 验证上下文
  32. * @param t 待验证对象
  33. */
  34. void onException ( Exception e , ValidatorContext context , T t );
  35. }

ValidatorHandler是实现Validator接口的一个模板类,如果你自己实现的Validator不想覆盖上面3个方法,可以继承这个ValidatorHandler。

  1. public class ValidatorHandler < T > implements Validator < T > {
  2. @Override
  3. public boolean accept ( ValidatorContext context , T t ) {
  4. return true ;
  5. }
  6. @Override
  7. public boolean validate ( ValidatorContext context , T t ) {
  8. return true ;
  9. }
  10. @Override
  11. public void onException ( Exception e , ValidatorContext context , T t ) {
  12. }
  13. }

内部校验逻辑发生错误时候,有两个处理办法,

第一,简单处理,直接放入错误消息。

  1. context . addErrorMsg ( "Something is wrong about the car seat count!" );
  2. return false ;

第二,需要详细的信息,包括错误消息,错误属性/字段,错误值,错误码,都可以自己定义,放入错误的方法如下,create()方法传入消息(必填),setErrorCode()方法设置错误码(选填),setField()设置错误字段(选填),setInvalidValue()设置错误值(选填)。当然这些信息需要result(toComplex())才可以获取到,详见6.7小节。

  1. context . addError ( ValidationError . create ( "Something is wrong about the car seat count!" ). setErrorCode ( 100 ). setField ( "seatCount" ). setInvalidValue ( t ));
  2. return false ;

6.2 ValidatorChain

on()的一连串调用实际就是构建调用链,因此理所当然可以传入一个调用链。

  1. ValidatorChain chain = new ValidatorChain ();
  2. List < Validator > validators = new ArrayList < Validator >();
  3. validators . add ( new CarValidator ());
  4. chain . setValidators ( validators );
  5. Result ret = FluentValidator . checkAll (). on ( car , chain ). doValidate (). result ( toSimple ());

6.3 onEach

如果要验证的是一个集合(Collection)或者数组,那么可以使用onEach,FluentValidator会自动为你遍历,依次apply constraints。

  1. FluentValidator . checkAll ()
  2. . onEach ( Lists . newArrayList ( new Car (), new Car ()), new CarValidator ());
  3. FluentValidator . checkAll ()
  4. . onEach ( new Car []{}, new CarValidator ());

6.4 fail fast or fail over

当出现校验失败时,也就是Validator的validate()方法返回了false,那么是继续还是直接退出呢?默认为使用failFast()方法,直接退出,如果你想继续完成所有校验,使用failOver()来skip掉。

  1. FluentValidator . checkAll (). failFast ()
  2. . on ( car . getManufacturer (), new CarManufacturerValidator ());
  3. FluentValidator . checkAll (). failOver ()
  4. . on ( car . getManufacturer (), new CarManufacturerValidator ());

6.5 when

on()后面可以紧跟一个when(),当when满足expression表达式on才启用验证,否则skip调用。

  1. FluentValidator . checkAll ()
  2. . on ( car . getManufacturer (), new CarManufacturerValidator ()). when ( a == b )

6.6 验证回调callback

doValidate()方法接受一个ValidateCallback接口,接口定义如下:

  1. public interface ValidateCallback {
  2. /**
  3. * 所有验证完成并且成功后
  4. *
  5. * @param validatorElementList 验证器list
  6. */
  7. void onSuccess ( ValidatorElementList validatorElementList );
  8. /**
  9. * 所有验证步骤结束,发现验证存在失败后
  10. *
  11. * @param validatorElementList 验证器list
  12. * @param errors 验证过程中发生的错误
  13. */
  14. void onFail ( ValidatorElementList validatorElementList , List < ValidationError > errors );
  15. /**
  16. * 执行验证过程中发生了异常后
  17. *
  18. * @param validator 验证器
  19. * @param e 异常
  20. * @param target 正在验证的对象
  21. *
  22. * @throws Exception
  23. */
  24. void onUncaughtException ( Validator validator , Exception e , Object target ) throws Exception ;
  25. }

默认的,使用不含参数的doValidate()方法,FluentValidator使用DefaultValidateCallback,其实现如下,可以看出出错了,成功了什么也不做,有不可控异常的时候直接抛出。

  1. public class DefaultValidateCallback implements ValidateCallback {
  2. @Override
  3. public void onSuccess ( ValidatorElementList validatorElementList ) {
  4. }
  5. @Override
  6. public void onFail ( ValidatorElementList validatorElementList , List < ValidationError > errors ) {
  7. }
  8. @Override
  9. public void onUncaughtException ( Validator validator , Exception e , Object target ) throws Exception {
  10. throw e ;
  11. }
  12. }

如果你不满意这种方式,例如成功的时候打印一些消息,一种实现方式如下:

  1. Result ret = FluentValidator . checkAll ()
  2. . on ( car . getSeatCount (), new CarSeatCountValidator ())
  3. . doValidate ( new DefaulValidateCallback () {
  4. @Override
  5. public void onSuccess ( ValidatorElementList validatorElementList ) {
  6. LOG . info ( "all ok!" );
  7. }
  8. }). result ( toSimple ());

6.7 获取结果

result()接受一个ResultCollector接口,如上面所示,toSimple()实际是个静态方法,这种方式在 Java8 Stream API 中很常见,默认可以使用FluentValidator自带的简单结果Result,如果需要可以使用复杂ComplexResult,内含错误消息,错误属性/字段,错误值,错误码,如下所示:

  1. ComplexResult ret = FluentValidator . checkAll (). failOver ()
  2. . on ( company , new CompanyCustomValidator ())
  3. . doValidate (). result ( toComplex ());

当然,如果你想自己实现一个结果类型,完全可以定制,实现ResultCollector接口即可。

  1. public interface ResultCollector < T > {
  2. /**
  3. * 转换为对外结果
  4. *
  5. * @param result 框架内部验证结果
  6. *
  7. * @return 对外验证结果对象
  8. */
  9. T toResult ( ValidationResult result );
  10. }

toSimple() 和 toComplex()方法是通过如下方式static import进来的:

import static com.baidu.unbiz.fluentvalidator.ResultCollectors.toSimple;

import static com.baidu.unbiz.fluentvalidator.ResultCollectors. toComplex;

6.8 关于RuntimeValidateException

如果验证中发生了一些不可控异常,例如数据库调用失败,RPC连接失效等,会抛出一些异常,如果Validator没有try-catch处理,FluentValidator会将这些异常封装在RuntimeValidateException,然后再re-throw出去,这个情况你应该知晓并作出你认为最正确的处理。

6.9 context上下文共享

通过putAttribute2Context()方法,可以往FluentValidator注入一些键值对,在所有Validator中共享,有时候这相当有用。

例如下面展示了在caller添加一个ignoreManufacturer属性,然后再Validator中拿到这个值的过程。

  1. FluentValidator . checkAll ()
  2. . putAttribute2Context ( "ignoreManufacturer" , true )
  3. . on ( car . getManufacturer (), new CarManufacturerValidator ())
  4. . doValidate (). result ( toSimple ());
  5. public class CarManufacturerValidator extends ValidatorHandler < String > implements Validator < String > {
  6. @Override
  7. public boolean validate ( ValidatorContext context , String t ) {
  8. Boolean ignoreManufacturer = context . getAttribute ( "ignoreManufacturer" , Boolean . class );
  9. if ( ignoreManufacturer != null && ignoreManufacturer ) {
  10. return true ;
  11. }
  12. // ...
  13. }
  14. }

6.10 闭包

通过putClosure2Context()方法,可以往FluentValidator注入一个闭包,这个闭包的作用是在Validator内部可以调用,并且缓存结果到Closure中,这样caller在上层可以获取这个结果。

典型的应用场景是,当需要频繁调用一个RPC的时候,往往该执行线程内部一次调用就够了,多次调用会影响性能,我们就可以缓存住这个结果,在所有Validator间和caller中共享。

下面展示了在caller处存在一个manufacturerService,它假如需要RPC才能获取所有生产商,显然是很耗时的,可以在validator中调用,然后validator内部共享的同时,caller可以利用闭包拿到结果,用于后续的业务逻辑。

  1. Closure < List < String >> closure = new ClosureHandler < List < String >>() {
  2. private List < String > allManufacturers ;
  3. @Override
  4. public List < String > getResult () {
  5. return allManufacturers ;
  6. }
  7. @Override
  8. public void doExecute ( Object ... input ) {
  9. allManufacturers = manufacturerService . getAllManufacturers ();
  10. }
  11. };
  12. Result ret = FluentValidator . checkAll ()
  13. . putClosure2Context ( "manufacturerClosure" , closure )
  14. . on ( car , validator )
  15. . doValidate (). result ( toSimple ());
  16. public class CarValidator extends ValidatorHandler < Car > implements Validator < Car > {
  17. @Override
  18. public boolean validate ( ValidatorContext context , Car car ) {
  19. Closure < List < String >> closure = context . getClosure ( "manufacturerClosure" );
  20. List < String > manufacturers = closure . executeAndGetResult ();
  21. if (! manufacturers . contains ( car . getManufacturer ())) {
  22. context . addErrorMsg ( String . format ( CarError . MANUFACTURER_ERROR . msg (), car . getManufacturer ()));
  23. return false ;
  24. }
  25. return true ;
  26. }
  27. }

7 高级玩法

7.1 与JSR303规范最佳实现Hibernate Validator集成

Hibernate Validator 是 JSR 303 – Bean Validation 规范的一个最佳的实现类库,他仅仅是jboss家族的一员,和大名鼎鼎的Hibernate ORM是系出同门,属于远房亲戚关系。很多框架都会天然集成这个优秀类库,例如Spring MVC的@Valid注解可以为Controller方法上的参数做校验。

FluentValidator当然不会重复早轮子,这么好的类库,一定要使用站在巨人肩膀上的策略,将它集成进来。

想要了解更多Hibernate Validator用法,参考 这个链接 

下面的例子展示了@NotEmpty, @Pattern, @NotNull, @Size, @Length和@Valid这些注解装饰一个公司类(Company)以及其成员变量(Department)。

  1. public class Company {
  2. @NotEmpty
  3. @Pattern ( regexp = "[0-9a-zA-Z\4e00-\u9fa5]+" )
  4. private String name ;
  5. @NotNull ( message = "The establishTime must not be null" )
  6. private Date establishTime ;
  7. @NotNull
  8. @Size ( min = 0 , max = 10 )
  9. @Valid
  10. private List < Department > departmentList ;
  11. // getter and setter...
  12. }
  13. public class Department {
  14. @NotNull
  15. private Integer id ;
  16. @Length ( max = 30 )
  17. private String name ;
  18. // getter and setter...
  19. }

我们要在这个Company类上做校验,首先引入如下的jar包到pom.xml中。

  1. <dependency>
  2. <groupId> com.baidu.unbiz </groupId>
  3. <artifactId> fluent-validator-jsr303 </artifactId>
  4. <version> 1.0.5 </version>
  5. </dependency>

默认依赖于Hibernate Validator 5.2.1.Final版本。

然后我们使用HibernateSupportedValidator这个验证器来在company上做校验,它和普通的Validator没任何区别。

  1. Result ret = FluentValidator . checkAll ()
  2. . on ( company , new HibernateSupportedValidator < Company >(). setValidator ( validator ))
  3. . on ( company , new CompanyCustomValidator ())
  4. . doValidate (). result ( toSimple ());
  5. System . out . println ( ret );

注意这里的HibernateSupportedValidator依赖于javax.validation.Validator的实现,也就是Hibernate Validator,否则它将不会正常工作。

下面是Hibernate Validator官方提供的初始化javax.validation.Validator实现的方法,供参考使用。

  1. Locale . setDefault ( Locale . ENGLISH ); // speicify language
  2. ValidatorFactory factory = Validation . buildDefaultValidatorFactory ();
  3. javax . validation . Validator validator = factory . getValidator ();

7.2 注解验证

一直以来介绍的方式都是通过显示的API调用来进行验证,FluentValidator同样提供简洁的基于注解配置的方式来达到同样的效果。

@FluentValidate可以装饰在属性上,内部接收一个Class[]数组参数,这些个classes必须是Validator的子类,这叫表明在某个属性上依次用这些Validator做验证。举例如下,我们改造下Car这个类:

  1. public class Car {
  2. @FluentValidate ({ CarManufacturerValidator . class })
  3. private String manufacturer ;
  4. @FluentValidate ({ CarLicensePlateValidator . class })
  5. private String licensePlate ;
  6. @FluentValidate ({ CarSeatCountValidator . class })
  7. private int seatCount ;
  8. // getter and setter ...
  9. }

然后还是利用on()或者onEach()方法来校验,这里只不过不用传入Validator或者ValidatorChain了。

  1. Car car = getCar ();
  2. Result ret = FluentValidator . checkAll (). configure ( new SimpleRegistry ())
  3. . on ( car )
  4. . doValidate ()
  5. . result ( toSimple ());

那么问题来了?不要问我挖掘机(Validator)哪家强,先来说挖掘(Validator)从哪来。

默认的,FluentValidator使用SimpleRegistry,它会尝试从当前的class loader中调用Class.newInstance()方法来新建一个Validator,具体使用示例如下,默认的你不需要使用configure(new SimpleRegistry())。

一般情况下,SimpleRegistry都够用了。除非你的验证器需要Spring IoC容器管理的bean注入,那么你干脆可以把Validator也用Spring托管,使用@Service或者@Component注解在Validator类上就可以做到,如下所示:

  1. @Component
  2. public class CarSeatCountValidator extends ValidatorHandler < Integer > implements Validator < Integer > {
  3. // ...
  4. }

这时候,需要使用SpringApplicationContextRegistry,它的作用就是去Spring的容器中寻找Validator。想要使用Spring增强功能,将下面的maven依赖加入到pom.xml中。

  1. <dependency>
  2. <groupId> com.baidu.unbiz </groupId>
  3. <artifactId> fluent-validator-spring </artifactId>
  4. <version> 1.0.5 </version>
  5. </dependency>

依赖树如下,请自觉排除或者更换不想要的版本。

  1. [ INFO ] +- org . springframework : spring - context - support : jar : 4.1 . 6.RELEASE : compile
  2. [ INFO ] | +- org . springframework : spring - beans : jar : 4.1 . 6.RELEASE : compile
  3. [ INFO ] | +- org . springframework : spring - context : jar : 4.1 . 6.RELEASE : compile
  4. [ INFO ] | | +- org . springframework : spring - aop : jar : 4.1 . 6.RELEASE : compile
  5. [ INFO ] | | | \- aopalliance : aopalliance : jar : 1.0 : compile
  6. [ INFO ] | | \- org . springframework : spring - expression : jar : 4.1 . 6.RELEASE : compile
  7. [ INFO ] | \- org . springframework : spring - core : jar : 4.1 . 6.RELEASE : compile
  8. [ INFO ] +- org . slf4j : slf4j - api : jar : 1.7 . 7 : compile
  9. [ INFO ] +- org . slf4j : slf4j - log4j12 : jar : 1.7 . 7 : compile
  10. [ INFO ] | \- log4j : log4j : jar : 1.2 . 17 : compile

让Spring容器去管理SpringApplicationContextRegistry,由于这个类需要实现ApplicationContextAware接口,所以Spring会帮助我们把context注入到里面,我们也就可以随意找bean了。

调用的例子省略,原来怎么校验现在还怎么来。

7.3 分组

当使用注解验证时候,会遇到这样的情况,某些时候例如添加操作,我们会验证A/B/C三个属性,而修改操作,我们需要验证B/C/D/E 4个属性,显示调用FluentValidator API的情况下,我们可以做到“想验证什么,就验证什么”。

@FluentValidate注解另外一个接受的参数是groups,里面也是Class[]数组,只不过这个Class可以是开发人员随意写的一个简单的类,不含有任何属性方法都可以,例如:

  1. public class Add {
  2. }
  3. public class Car {
  4. @FluentValidate ( value = { CarManufacturerValidator . class }, groups = { Add . class })
  5. private String manufacturer ;
  6. }

那么验证的时候,只需要在checkAll()方法中传入想要验证的group,就只会做选择性的分组验证,例如下面例子,只有生产商会被验证。

  1. Result ret = FluentValidator . checkAll ( new Class <?>[] { Add . class })
  2. . on ( car )
  3. . doValidate ()
  4. . result ( toSimple ());

这种groups的方法同样适用于hibernate validator,想了解更多请参考官方文档。

7.4 级联对象图

级联验证(cascade validation),也叫做对象图(object graphs),指一个类嵌套另外一个类的时候做的验证。

如下例所示,我们在车库(Garage)类中含有一个汽车列表(carList),可以在这个汽车列表属性上使用@FluentValid注解,表示需要级联到内部Car做onEach验证。

  1. public class Garage {
  2. @FluentValidate ({ CarNotExceedLimitValidator . class })
  3. @FluentValid
  4. private List < Car > carList ;
  5. }

注意,@FluentValid和@FluentValidate两个注解不互相冲突,如下所示,调用链会先验证carList上的CarNotExceedLimitValidator,然后再遍历carList,对每个car做内部的生产商、座椅数、牌照验证。

7.5 Spring AOP集成

读到这里,你会发现,只介绍了优雅,说好的业务逻辑和验证逻辑解耦合呢?这需要借助Spring AOP技术实现,如下示例,addCar()方法内部上来就是真正的核心业务逻辑,注意参数上的car前面装饰有@FluentValid注解,这表示需要Spring利用切面拦截方法,对参数利用FluentValidator做校验。

  1. @Service
  2. public class CarServiceImpl implements CarService {
  3. @Override
  4. public Car addCar ( @FluentValid Car car ) {
  5. System . out . println ( "Come on! " + car );
  6. return car ;
  7. }
  8. }

Spring XML配置如下,首先定义了ValidateCallback表示该如何处理成功,失败,抛出不可控异常三种情况,因此,即使你拿不到Result结果,也可以做自己的操作,例如下面例子,如果发生错误,抛出CarException,并且消息是第一条错误信息,如果成功则打印"Everything works fine",如果失败,把实际的异常e放入运行时的CarException抛出。

  1. public class ValidateCarCallback extends DefaulValidateCallback implements ValidateCallback {
  2. @Override
  3. public void onFail ( ValidatorElementList validatorElementList , List < ValidationError > errors ) {
  4. throw new CarException ( errors . get ( 0 ). getErrorMsg ());
  5. }
  6. @Override
  7. public void onSuccess ( ValidatorElementList validatorElementList ) {
  8. System . out . println ( "Everything works fine!" );
  9. }
  10. @Override
  11. public void onUncaughtException ( Validator validator , Exception e , Object target ) throws Exception {
  12. throw new CarException ( e );
  13. }
  14. }

将ValidateCarCallback注入到FluentValidateInterceptor中。然后利用BeanNameAutoProxyCreator在*ServiceImpl上用fluentValidateInterceptor拦截做验证,这有点类似Spring事务处理拦截器的使用方式。更多AOP的用法请参考 Spring官方文档 

  1. <bean id = "validateCarCallback" class = "com.baidu.unbiz.fluentvalidator.interceptor.ValidateCarCallback" />
  2. <bean id = "fluentValidateInterceptor"
  3. class = "com.baidu.unbiz.fluentvalidator.interceptor.FluentValidateInterceptor" >
  4. <property name = "callback" ref = "validateCarCallback" />
  5. <property name = "locale" value = "zh_CN" />
  6. <property name = "hibernateDefaultErrorCode" value = "10000" />
  7. </bean>
  8. <bean class = "org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator" >
  9. <property name = "beanNames" >
  10. <list>
  11. <value> *ServiceImpl </value>
  12. </list>
  13. </property>
  14. <property name = "interceptorNames" >
  15. <list>
  16. <value> fluentValidateInterceptor </value>
  17. </list>
  18. </property>
  19. </bean>

测试代码如下:

  1. @Test
  2. public void testAddCarNegative () {
  3. try {
  4. Car car = getValidCar ();
  5. car . setLicensePlate ( "BEIJING123" );
  6. carService . addCar ( car );
  7. } catch ( CarException e ) {
  8. assertThat ( e . getMessage (), Matchers . is ( "License is not valid, invalid value=BEIJING123" ));
  9. return ;
  10. }
  11. fail ();
  12. }

7.6 国际化支持

也就是常说的i18n,使用Spring提供的MessageSource来支持,只需要再Spring的XML配置中加入如下配置:

  1. <bean id = "messageSource" class = "org.springframework.context.support.ResourceBundleMessageSource" >
  2. <property name = "basenames" >
  3. <list>
  4. <value> error-message </value>
  5. </list>
  6. </property>
  7. </bean>

同时编辑两个国际化文件,路径放在classpath中即可,通常放到resource下面,文件名分别叫做error-message.properties和error-message_zh_CN.properties表示默认(英文)和中文-简体中文消息。

car.size.exceed=Car number exceeds limit, max available num is {0}
car.size.exceed=\u6c7d\u8f66\u6570\u91cf\u8d85\u8fc7\u9650\u5236\uff0c\u6700\u591a\u5141\u8bb8{0}\u4e2a

注意,其中{0}代表和运行时填充的参数。

在Validator使用时,示例如下,只用MessageSupport.getText(..)来获取国际化信息。

  1. public class GarageCarNotExceedLimitValidator extends ValidatorHandler < Garage > implements Validator < Garage > {
  2. public static final int MAX_CAR_NUM = 50 ;
  3. @Override
  4. public boolean validate ( ValidatorContext context , Garage t ) {
  5. if (! CollectionUtil . isEmpty ( t . getCarList ())) {
  6. if ( t . getCarList (). size () > MAX_CAR_NUM ) {
  7. context . addError (
  8. ValidationError . create ( MessageSupport . getText ( GarageError . CAR_NUM_EXCEED_LIMIT . msg (),
  9. MAX_CAR_NUM ))
  10. . setErrorCode ( GarageError . CAR_NUM_EXCEED_LIMIT . code ())
  11. . setField ( "garage.cars" )
  12. . setInvalidValue ( t . getCarList (). size ()));
  13. return false ;
  14. }
  15. }
  16. return true ;
  17. }
  18. }

这里需要注意下,如果使用HibernateValidator打的注解,例如@NotNull,@Length等,需要将错误信息放到ValidationMessages.properties和ValidationMessages_zh_CN.properties中,否则找不到哦。

8 总结

上面对FluentValidator做了一个全面的介绍,从显示的流式风格(Fluent Interface)API调用,以及各种丰富多样的链操作方法,再到对JSR303 – Bean Validation规范的集成,最后介绍了高级点的注解方式验证、支持级联对象图和Spring AOP的集成,我想这完成了对FluentValidator任务的诠释,更好的帮助开发人员做业务逻辑验证,希望能给你的项目一点帮助,哪怕不是用这个框架,而只是汲取一些思想,然后产生自己的思考,让我们的clean code更加“环保”。

如果有任何疑问,欢迎致电骚扰这个对代码有终身质保承诺的我。

更多实例,请参考 github 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值