开发者社区 > 博文 > Jakarta Bean Validation在安卓端的应用探索
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

Jakarta Bean Validation在安卓端的应用探索

  • 京东零售技术
  • 2022-01-04
  • IP归属:北京
  • 32440浏览
    背景




    安卓1.jpg


    在日常开发中,前端运行、页面展示所需要的数据基本都来源于接口,拿到接口数据后如何确定数据是准确、合法且不影响应用程序正常运行展示的呢?这就离不开数据校验功能,通常情况下我们会对重要的字段在真正使用前做各种判断,这样当然也能达到数据校验的目的,但是免不了出现大段的if-else判断逻辑,如此编写的代码可读性差,容易出错,而且编写的代码基本没有可复用性,每个接口都得写一遍,重复造轮子,实在是费时费力。


    安卓2.jpg



    Jakarta Bean Validation 标准




    关于数据校验需求Java标准委员会经制定了一套完善的标准规范,通过注解的方式进行数据校验,该规范单纯抽象的定义了一套标准接口,调用方可基于该接口自行实现校验逻辑,目前主流开源的实现方案是Hibernate Validator框架。目前为止,该标准已迭代了三个版本,具体版本信息如下:


    安卓3.jpg


    JSR是Java Specifcation Requests的缩写,意思是Java 规范提案,通过该标准我们也能看到数据校验功能的必要性,都已经形成一套标准来执行了,足见数据校验的重要。此外,Jakarta Bean Validation 原名Java Bean Validation,属于Java EE标准的一项子标准,在Oracle收购Sun公司后将Java EE标准捐献给Eclipse基金会,但不允许继续使用Java名字,因此改名为Jakarta Bean Validation。



    适用场景




    那么数据校验功能具体适用于哪些场景呢?


    •  接口被拦截篡改后的校验


    很多人认为数据校验是服务端的事,只要服务端处理了,客户端无需校验,其实不然,比如在应用的接口请求被攻击者拦截的情形下,攻击者随意篡改返回数据,返回给客户端,如果客户端没有做数据校验,那么程序的页面展示必定错乱,程序的运行也会受到影响,甚至出现崩溃的问题,导致用户体验差。


    •  接入三方服务后对接口的监控


    试想一下,在商业合作项目中,如果我们的客户端组件单独提供给客户使用,客户有自己的服务端接口,那么如何才能快速反映出接口数据存在的问题呢,数据校验功能是最佳的选择。


    • 组件化开发中,本地Bean的校验


    现在主流的开发模式都是组件化开发或插件化开发,大家分工合作,每个人负责不同的模块组件,最后集成各个模块,那么在组件交互的过程中避免不了数据传值交互,此时我们也可以通过数据校验功能来处理其他组件传来的数据,保证组件的健壮性。


    •  线上问题排查处理


    在app线上运行过程中,总会遇到一些因数据异常导致的偶发问题,难以排查定位,通过日志排查又因为日志太多或者日志被定时清理,导致无法快速定位问题,此时我们可以通过接口数据校验的功能来确定数据异常,然后将数据异常情况的接口信息(request、header、cookie、response)上传监控平台,对于严重影响app运行流程的数据异常情况甚至可以接入实时报警机制,联系相关人员迅速处理,避免因处理不及时造成损失。



    Java 注解




    上面提到Jakarta Bean Validation是通过注解来实现的数据校验功能,那么注解到底是什么呢?有什么好处呢?


    •  注解原理


    Java注解也叫元数据,是通过对类、方法、属性、参数等通过特有标记进行标注说明或逻辑处理的一项功能。Java注解自带标准注解和元注解,标准注解可以直接使用,也可以基于元注解根据需要自定义注解。


    安卓4.jpg


    •  注解优点


    通过注解编写的代码优雅、可读性强,易维护,数据校验代码与业务逻辑分离,不会相互影响,可复用在多个接口中,只需在对应的字段属性标注即可,注解与域模型(接口数据模型)强绑定,这样其他人只要一看注解就知道具体该属性的约束,无需编写大段的注释。



    Jakarta Bean Validation 框架使用




    Bean Validation包含两部分,即标准接口和具体实现,api-validation(标准接口Api),具体的验证实现框架主要有三个,Spring-Validatior主要用于服务端使用,和Spring MVC无缝集成;Hibernate-Validator,相当于api-validator的官方标准实现;Apache-Validator,目前使用的人已经较少,可以不讨论,今天我们使用的是Hibernate-Validator,其它两个不做解释,感兴趣的请自行研究,整个方案流程如下:


    安卓5.jpg


    •  依赖集成


    因为hibernate-validator框架对异常信息(message)的处理用到了el表达式,因此我们添加依赖库的时候还需要添加el表达式的依赖,具体的validator和el表达式对应版本可通过源码分支对应的集成文档查询。






    //hibernate-validatorapi 'org.hibernate:hibernate-validator:5.0.+' //el表达式标准接口implementation 'javax.el:javax.el-api:2.2.4'//el表达式实现implementation 'org.glassfish.web:javax.el:2.2.4'


    以上是validation-api-1.1标准规范下的版本引入,validation-api-1.0版本已经基本没人使用,可忽略。




    //hibernate-validatorapi group: 'org.hibernate.validator', name: 'hibernate-validator', version: '6.0.+' //el表达式实现api 'org.glassfish:javax.el:3.0.+'


    以上是validation-api-2.0标准规范下的版本引入,这里需要注意,el表达式的依赖除了版本变化,依赖库名也发生了变化。


    • 初始化hibernate-validator配置












    HibernateValidatorConfiguration configuration = Validation.byProvider(HibernateValidator.class).configure()//开启快速失败模式.failFast(true)//消息插值器.messageInterpolator(null)//忽略xml配置.ignoreXmlConfiguration();ValidatorFactory factory = configuration.buildValidatorFactory();


    Hibernate-Validator的初始化调用非常简单,创建配置对象后可进行多种设置,该设置支持全局设置,生成配 置后根据该配置生成ValidatorFactory对象,通过该对象获取校验器对象(valiadtor),常用的配置项有:


    •  开启/关闭快速失败模式


    所谓快速失败模式是指,如果一个bean中含有多个字段属性的校验,开启快速失败模式后,只要有一个属性的校验不通过,那么本次校验即结束并返回不通过的结果,如未开启快速失败,则对整个bean的所有校验都完成后再结束本次校验,快速失败模式默认关闭。


    •  自定义消息插值器


    支持设置自定义消息插值器,主要处理错误提示消息,安卓端可以通过自定义消息插值器实现错误消息toast提示。


    • 忽略xml配置解析


    Hibernate-Validator 支持通过xml形式下发校验规则,如果不需要动态下发校验规则的时候可以配置忽略。还有其他一些配置项,可根据文档查询使用。


    •  异常结果处理







    public void validate(Object bean, HttpSetting httpSetting, HttpResponse httpRespons //返回异常结果Set<ConstraintViolation<Object>> result = factory.getValidator().validate(bean) if (result != null) {//校验未通过,有异常,需上报BLogReport.logEndReportAuto("order", "OrderListPage", httpSetting, httpResp}}


    通过factory.getValidator()获取到校验器对象,然后调用该对象的validate方法去执行真正的校验,传入的参数就是要校验的bean,validate方法执行返回结果是一个ConstraintViolation类型的Set集合,如果校验通过,则该集合为空,如果校验未通过切且启了快速失败模式,则该集合有一个元素,校验未通过且未开启快速失败模式,该集合可能有多个元素。本次探索对生成的异常数据接口鹰眼系统,校验时报后将接口相关信息上传到鹰眼监控平台,方便问题排查。



    简单使用




    •  常用验证注解


    安卓6.jpg


    基本类型的数据校验注解使用较简单,直接在属性上添加对应注解即可






































    public class ValidationBean {

    @AssertFalse(message = "属性值必须为false")public boolean assertFalse;
    @AssertTrue(message = "属性值必须为true")public boolean assertTrue;
    @DecimalMax(value = "55", message = "属性值不能大于55")public int decimalMax;
    @DecimalMin(value = "55", message = "属性值不能小于55")public int decimalMin;
    @Future(message = "必须大于当前时间")public Date futureDate;
    @Past(message = "必须小于当前时间")public Date pastDate;
    @Max(5)public int max;
    @Min(5)public int min;
    @NotNull(message = "该对象不可为null")public Object objNotNull;
    @Null(message = "该对象必须为null")public Object objNull;
    @Size(max = 5, min = 1, message = "该字符串的长度必须在1-5之间")public String strLength;
    }


    我们构造一下测试数据验证一下








    ValidationBean validationBean = new ValidationBean(); validationBean.assertFalse = true; validationBean.assertTrue = false; validationBean.decimalMax = 88; validationBean.decimalMin = 11;validationBean.futureDate = new Date(System.currentTimeMillis() - 10000000l);validationBean.pastDate = new Date(System.currentTimeMillis() + 10000000l);validationBean.max = 55;validationBean.min = 55;validationBean.objNull = new Object();validationBean.strLength = "strLengthstrLength";


    输出如下结果


    安卓7.jpg


    以上操作属于基本类型属性的校验,相对简单,实际开发中,还会有复合类型属性的应用,即级联属性校验:


    •  级联验证


    要实现级联校验需要用到@Valid注解,该注解用于List、Object等复合属性,代表除去校验该属性本身的校验规则外还需校验属性内部的校验规则。























    // 1. 新建SubValiadtionBean测试类,指定两个不可为空的属性 public static class SubValiadtionBean {
    @NotNull(message = "名字属性不可为空")public String name;@NotNull(message = "id属性不可为空")public String id;}
    //2. ValidationBean 类中增加两个属性,@Validpublic SubValiadtionBean subValiadtionBean;@Valid@NotNull(message="列表不可为空")@Size(min = 1, max = 5, message="列表长度在1-5之间")public List<SubValiadtionBean> beanList;
    //3. 构造测试数据ValidationBean validationBean = new ValidationBean(); validationBean.subValiadtionBean = new SubValiadtionBean(); List<SubValiadtionBean> beanList = new ArrayList<>(); validationBean.beanList = beanList;
    //4.执行校验factory.getValiadtor.validate(validationBean);


    输出结果如下


    安卓8.jpg



    进阶使用




    上面提到的单个属性字段是比较基础的,只需要在相应属性上增加对应的注解即可,但是在实际开发中还会遇到一些复杂的业务场景,比如多个属性字段按照顺序校验、多个字段联合校验等复杂情况,这就需要用到Hibernate-Validation的高级用法。


    • 顺序校验 GroupSequence


    首先创建Bean类,然后声明顺序标记接口OrderFirst、OrderSecond、OrderThird,在新建属性,并添加注解,通过注解的groups值设置该属性的校验顺序,validOrder1、validOrder2、validOrder3按顺序校验,如果有一个失败,后面的就不再执行。



















    public class GroupSquenceValidate {//顺序1public interface OrderFirst {}//顺序2public interface OrderSecond {}//顺序3public interface OrderThird {}
    @NotNull(message = "第一顺序属性不可为空", groups = OrderFirst.class) public String validOrder1;@NotNull(message = "第二顺序属性不可为空", groups = OrderSecond.class) public String validOrder2;@NotNull(message = "第三顺序属性不可为空", groups = OrderThird.class) public String validOrder3;@GroupSequence({OrderFirst.class, OrderSecond.class, OrderThird.class}) public interface Group {
    }}


    构建测试数据并执行校验





    //创建对象,所有属性值为默认值GroupSquenceValidate groupSquenceValidate = new GroupSquenceValidate();//指定校验顺序接口并执行校验Validator.validate(groupSquenceValidate,GroupSquenceValidate.Group.class);


    输出如下结果


    安卓10.jpg





    //创建对象,为validOrder1、validOrder2赋值,validOrder3为默认值 GroupSquenceValidate groupSquenceValidate = new GroupSquenceValidate(); groupSquenceValidate.validOrder1 = "11"; groupSquenceValidate.validOrder2 = "22";//指定校验顺序接口并执行校验Validator.validate(groupSquenceValidate,GroupSquenceValidate.Group.class);


    输出如下结果


    安卓11.jpg


    两个结果比较可知我们定义的顺序校验生效


    • 多字段联合校验 GroupSequenceProvider


    对于多字段联合校验,框架给我们提供了GroupSequenceProvider注解实现,但和其他注解不同的是需要我们根据自己的业务自定义验证解析器,我们通过一个例子来说明,试想有这样一种情形,统计不同年龄段的年收入,单位万元,规则如下:


    • 20-30岁的人群,年收入10-30万元

    • 30-40岁的人群,年收入30-50万元


    实现解析接口并创建数据模型Bean








































    public class RevenueStaticsProvider implements DefaultGroupSequenceProvider<RevenueStatics> {
    @Overridepublic List<Class<?>> getValidationGroups(RevenueStatics revenueStatics) { //实现具体的解析逻辑List<Class<?>> groupSequence = new ArrayList<>(); groupSequence.add(RevenueStatics.class);if (revenueStatics != null) {Integer age = revenueStatics.ageRange;if (age >= 20 && age < 30) {groupSequence.add(RevenueStatics.AgeRangeFrom20To30.class);else if (age >= 30 && age < 40) {groupSequence.add(RevenueStatics.AgeRangeFrom30To40.class);}}return groupSequence;}}
    @GroupSequenceProvider(RevenueStaticsProvider.class)public class RevenueStatics {/*** 年龄范围*/
    @Range(min = 10, max = 40)public int ageRange;/*** 输入范围*/
    @Range.List({//通过容器实现重复注解
    @Range(min = 10, max = 30 , groups = AgeRangeFrom20To30.class)@Range(min = 30, max = 50 , groups = AgeRangeFrom30To40.class)})public int revenueRange;public interface AgeRangeFrom20To30{}public interface AgeRangeFrom30To40{}}


    构造测试数据








    RevenueStatics revenueStatics = new RevenueStatics(); revenueStatics.ageRange = 20; revenueStatics.revenueRange = 60; Validator.validate(revenueStatics); revenueStatics.ageRange = 35;  revenueStatics.revenueRange = 5;  Validator.validate(revenueStatics);


    输出结果可验证符合我们预期校验逻辑


    监控平台收到数据


    安卓12.jpg



    总结




    •  探索成果


    通过对Jakarta Bean Validation的研究使用,确定其上手快,使用简单,功能强大,值得引入。


    •  未来的计划


    探索动态下发规则,设想mpass可支持xml格式配置文件,通过mpass平台动态下发规则,做到实时控制规则配置。


    •  问题


    •  新版本存在兼容性问题,如果想使用新版特性,需要二次开发改造,具体情形为目前安卓端比较适合Bean-Validation1.1(JSR-349标准)版本的实现,即Hibenate-validator-5.x版本,因为Bean-Validation2.0(JSR-380标准)版本基于Jdk1.8开发,引入了很多1.8的新特性,但是安卓系统并非所有版本都完全支持JDK1.8特性,比如对MethodHandle的支持最低是Android O版本,不过根据实际情况Hibenate-validator-5.x版本也已经可以支持绝大多数数情况的校验了。如果必须用到新版特性,在有资源和能力情况下建议基于Hibernate-Validator主分支拉取新分支,开发适配Android端的套件。

    •  维护成本略高,因不能完全使用官方版本,如有无法兼容的问题需要二次开发。



    参考阅读


    Bean-Validation官网:

    https://beanvalidation.org/

    JSR-380官网:

    https://www.jcp.org/en/egc/view?id=380

    Hibernate-validator官网:

    https://hibernate.org/validator/

    Hibernate-validator源码:

    https://github.com/hibernate/hibernate-validator




    作者:技数中心田子龙