原文链接:https://www.cuba-platform.com/blog/2018-10-09/945

翻译:CUBA China

CUBA-Platform 官网 : https://www.cuba-platform.com

CUBA China 官网 : http://cuba-platform.cn

  我经常看见很多项目没有数据验证的策略和意识。他们的团队在交付日期的重压下,面对不清楚的需求,没有时间去考虑用合适并且统一的方法对数据进行验证。所以在这样的项目中,到处能看见数据验证的代码:在前端JS中,在后端页面控制器中,在业务逻辑的bean中,在数据模型实体中,在数据库的约束和触发器中。这些代码都是一些 if-else 的语句,抛出一些不同的未检查的异常,所以有时会很难找到这些该死的数据到底是在哪里做的验证。因此,一段时间之后,当项目成长到足够大的时候就很难并且需要耗费很多精力来统一这些验证,并且后面的需求也一样模糊不清。

  那有没有做数据验证比较标准、优雅而且还简洁的方法呢?这个方法不会导致代码的不可读,这个方法能帮我们将大部分数据验证的代码维护在统一的地方,而且有没有可能一些流行框架的开发者已经替我们做了大部分的工作呢?

  当然有!

  作为我们CUBA平台的开发者来说,让我们的用户也遵循最佳实践非常重要。我们认为,数据验证的代码应该是:

  1. 可重用但不重复,遵循DRY原则(Don’t Repeat Yourself)。

   2. 用干净和自然的方式表达出来。

  3. 放在开发人员期望看到的地方。

  4. 能对不同数据来源的数据进行检查:用户输入,SOAP或者REST 调用等。

  5. 能处理并发。

  6. 由应用程序隐式统一调用而不需要手动调用这些检查代码。

  7. 能用简洁的弹窗为用户展示清晰,本地语言的消息。

  8. 遵循标准。

  这篇文章里,我将使用基于CUBA平台开发的应用程序来演示所有的例子。由于CUBA是基于Spring和EclipseLink的,所以这些例子对于使用JPA和bean验证的其他Java框架也适用。

数据库约束验证

  也许,最常用最直接的数据验证方法就是使用数据库级别的约束,比如非空,字符串长度,唯一索引等。对于企业级应用来说,这个方法很自然,因为这种类型的软件通常都是以数据为中心。但是,即便是这种情况,开发者也经常出错,在应用程序的各个数据层级分别定义了约束。这个问题主要是由于开发人员的不同责任分工引起的。

  我们看一个几乎大家都会面对的例子,有的人甚至干过这样的事 :)。 假设有个规定要求护照号码字段需要有10个数字,很可能到处都会做这个规则检查:数据库设计者用DDL检查,后台开发人员在相应的实体和REST服务中检查,最后前端工程师在客户端代码中检查。之后这个需求变了,要求护照字段升到15个数字。技术支持人员可能只修改了数据库约束,但是这样对于用户来说等于什么都没改,因为后台和前台的检查还没修改呢。

  大家都知道避免这个问题的方法,验证需要中心化。在CUBA,这种验证的中心点在是实体的JPA注解。基于这个元数据信息,CUBA Studio可以生成正确的DDL脚本并且能在客户端采用相应的验证器。

  此时,如果JPA注解改变的话,CUBA会自动更新DDL脚本以及生成数据库迁移脚本,所以下次部署项目的时候,新的基于JPA的限制将会在UI和DB生效。

  这种方式简单、也能实施到底层数据库级别,因此能完全防破解。但是JPA注解的局限性在于,只能使用在最简单、可以用标准的DDL表述、而不需要引入特定数据库的触发器或者存储过程的情况。所以基于JPA的约束可以用来保证实体字段是唯一的,或者必须的,抑或也能定义varchar字段的最大长度。还有,可以使用 @UniqueConstraint 注解来为一组字段定义唯一性约束。但也就这些了。

  如果在需要更加复杂的验证逻辑的的时候,比如检查某个字段的最大最小值或者对一个字段使用正则表达式进行验证,此时我们就需要使用众所周知的叫做 “bean 验证” 的方法了。

Bean 验证

  我们知道,遵循标准是很好的实践,通常这种方式有更长的生命周期而且有几千个项目实战证明过了。Java 的 Bean验证是早就写在石头上的方案了:在JSR 380, 349 和 303也有些成熟的实现:Hibernate ValidatorApache BVal

很多开发者都熟悉这个方法,但是这个方法的好处却总是被低估。用这个方法甚至可以很容易在遗留项目中添加数据验证,并且还能以清晰、直接、可靠最贴近业务逻辑的方式表达需要做的验证。

  使用Bean验证能为项目带来很多好处:

  l  验证逻辑集中在数据模型附近:使用最自然的方法定义针对值、方法和bean的约束,因此可以将OOP推进到下一个级别(验证也可以OOP)。

  l Bean验证的标准提供了几十种 开箱即用的验证注解比如 @NotNull, @Size, @Min, @Max, @Pattern, @Email, @Past, 不太标准的比如 @URL, @Length,强大的 @ScriptAssert,另外还有很多其他的。

  l 不会受限于仅使用预定义的约束,还可以自定义约束注解。可以定义一个注解来将其他几个注解绑定到一起,或者定义一个全新的注解,然后定义一个相应的Java类作为验证器。比如,之前那个例子中,可以定义一个类级别的注解 @ValidPassportNumber 用来检查护照号码是否符合正确的格式,号码也许还依赖 country 字段的值。

  l 不止可以在类和字段上加约束,也可以添加到方法和方法参数上。这个叫做“合同验证”,后面会介绍。

  CUBA平台(以及一些其他平台)会在用户提交数据的时候自动调用这些验证,所以一旦验证失败用户会马上看到错误消息,不需要考虑手动执行这些bean验证。

  我们一起再看看护照号码验证的例子,但是这次我们还需要在实体添加几个其他的验证:

  l 人物姓名(例子中是用的英文名)至少有2个单词或者可以更多,必须是格式化很好的姓名。检查的正则表达式很复杂,比如 Charles Ogier de Batz de Castelmore Comte d'Artagnan 能通过检查,但是 R2D2 却不能通过。

  l 人物身高的区间:0< height <=300厘米。

  l 邮件地址需要是正确的邮件地址格式。

  因此,带有所有这些检查,Person类看起来是这样:

  那些标准的注解,比如 @NotNull, @DecimalMin, @Length, @Pattern 还有其他几个都是非常清楚的不需要过多解释。主要看看自定义的 @ValidPassportNumber 是怎么实现的。

  我们全新的 @ValidPassportNumber 会检查 Person#passportNumber 是否符合针对每个国家(Person#country)定义的正则表达式。

  首先,按照文档(CUBAHibernate文档是很好的参考)的描述,我们需要使用新的注解来标记实体类,以及将约束分组传递给这个注解。CUBA文档有说,UiCrossFieldChecks.class 应当在所有单独的字段检查完之后,才执行跨字段的检查,Default.class 能将约束添加到默认的验证组。

  注解的定义是这样的:

  @Target(ElementType.TYPE) 定义了注解在运行时生效的对象是一个类,@Constraint(validatedBy = … ) 声明注解的实现在 ValidPassportNumberValidator 类中,此类需要实现 ConstraintValidator<...> 接口,在isValid(...) 方法中添加验证代码,方法也很直接:

  好了,足够了。使用CUBA平台不需要多写任何代码来保证这个验证的运行,也不需要添加代码在用户输入错误的时候给用户发送消息通知。很简单吧?

  现在,我们看看这些东西都是怎么工作的,CUBA还做了一些额外的事情:不但给用户展示错误消息,而且还将有问题的表单字段高亮出来,这些漂亮的描红字段没有通过单一字段的bean验证:

  是不是很简洁?在用户的浏览器显示漂亮的错误提醒,只需要在实体中添加几个简单的注解就好了。

  作为本章节的总结,我们再简单列举一下实体的Bean验证有什么好处:

  1. 清晰可读
  2. 可以直接在实体模型中定义值的约束
  3. 可扩展、可定制化
  4. 跟很多流行的ORM集成,检查都是在实体保存在数据库之前自动调用的
  5. 有些框架也能在用户从UI提交数据的时候自动运行bean验证(但是如果不支持的话,很难手动调用 Validator 接口)
  6. Bean验证是众所周知的标准,网上能找到很多相关文档

  但是如果我们需要将验证放到方法、构造器上或者放到某个REST终端来验证从外部来的数据呢?或者我们想用声明式的方法验证方法参数而不是在每个方法内写很多if-else这种枯燥的检查参数的方法?

  答案很简单,bean验证也可以作用在方法上!

合同验证

  有时候,我们需要前进一步,不只是做到应用的数据模型验证。如果能做到参数和返回值自动验证,那么写方法的时候就会容易很多。这个需求可能不只是用在检查REST或者SOAP接入的数据,也会用在针对方法的输入参数和返回值上。用来做所谓的前置条件和后置条件检查,确保在方法体执行前对输入参数的检查,以及在方法执行后对返回值范围的检查,或者只是希望能声明式的用在参数上限定参数的范围以达到代码更好的可读性。

  使用合同验证,就可以在任何Java类型的方法、构造器的参数和返回值上使用验证。相对传统的检查参数和返回值的办法,这个方案的优点是:

  l 不需要以极端的方式执行检查(比如,抛出类似 illegalArgumentException 这样的异常)。我们会更愿意使用声明式的约束,这样会形成可读性表达性更强的代码。

  l 约束都是可重用、可配置、可定制化的:不需要每次都写验证代码,更少的代码意味着更少的bug。

  l 如果类、方法的返回值或参数使用了 @Validated 注解,平台会在每个方法调用的时候自动执行约束检查。

  l 如果一个可执行程序使用了 @Documented 注解,那么它的前置条件和后置条件会自动包含在生成的JavaDoc中。

  因此,使用合同验证方案,会有清晰、相对少的代码,更易于维护和理解。

  我们看看在CUBA应用的REST控制器中,使用合同验证的代码大概是什么样的。通过 PersonApiService 接口的 getPerson() 方法可以从数据库获取用户的列表,使用 addNewPerson(…) 方法可以添加新用户。需要注意的是,bean验证是可以继承的!也就是说,如果用验证的注解标记了某些类,字段或者方法,那么这个类的后代或者接口的实现类都会受到这些验证的影响。

  这个代码片段看起来怎样,是不是非常清晰,可读性也不错?(除了 @RequiredView(“_local”)  注解,这个是CUBA平台的专有注解,确保返回的Person对象会有 PASSPORTNUMBER_PERSON 表的所有字段)。

  @Valid 注解指定 getPerson() 方法的返回列表中的每个对象需要使用 Person 类的验证进行检查。

  CUBA会自动生成下列路径用来执行这些API:

  l /app/rest/v2/services/passportnumber_PersonApiService/getPersons

  l /app/rest/v2/services/passportnumber_PersonApiService/addNewPerson

  我们打开Postman试试这些验证是否都好用:

  你可能会注意到,上面的例子没有验证护照号码。这是因为这个需要在 addNewPerson 做跨参数验证,passportNumber 的验证正则表达式依赖 country 的值。这种跨参数的验证跟实体类级别约束是一样的。

  JSR 349 和 380 支持跨参数验证, 可以查阅 hibernate 文档了解如何为类/接口方法实施自定义的跨参数验证。

超越Bean验证

  世上没有什么是完美的,bean验证也有局限性:

  l 有时候需要在保存更改之前检查复杂的对象关系图的状态。比如,可能需要检查客户在你电商网站的订单中购买的所有东西是否能装到一个快递箱子中。这是个比较繁重的检查,因此每次客户订单的商品变更的时候都做这个检查不合适。所以这个检查应该只需要在Order对象和它的OrderItem对象保存到数据库之前做一次。

  l 有些检查需要在数据库的事务中做。比如,电商系统需要在订单保存到数据库之前检查是否有足够的库存。这些检查只能在事务级别,因为系统是并发的,库存的数量是实时变化的。

  CUBA平台提供了两个在数据提交之前做验证的机制:实体监听器事务监听器我们仔细看看。

实体监听器

  CUBA的实体监听器跟JPA提供的 PreInsertEvent, PreUpdateEvent 和 PredDeleteEvent 监听器非常相似。这两种机制都都可以在实体对象持久化到数据库之前或者之后做检查。

  在CUBA中定义和组织实体监听器不难,只需要两步:

  1. 创建实现了实体监听器接口的托管bean。作为数据验证方面的考虑,其中三个接口比较重要:BeforeDeleteEntityListener,BeforeInsertEntityListener以及BeforeUpdateEntityListener。
  2. 在需要做验证的实体用 @Listeners 注解标记

  可以了。

  跟JPA标准(JSR 338 3.5)不一样,CUBA的监听器接口是带数据类型的,所以不需要在方法内做类型转换,可以直接使用实体。CUBA平台还提供了跟当前实体关联的实体以及通过EntityManager去加载或者更改其他任何实体的机制。这些改动也会调用相应的实体监听器。

  另外,CUBA平台支持“软删除(soft deletion)”,实体在数据库只是标记为删除,但是不会真正删除数据库记录。所以对于软删除,CUBA平台会调用 BeforeDeleteEntityListener / AfterDeleteEntityListener 而标准的实现则会调用 PreUpdate / PostUpdate。

  看看下面的例子吧。事件监听器的bean跟实体类连接,只需要一行注解:@Listeners,注解使用的参数是监听器类的名称。

  实体监听器的实现是这样的:

  实体监听器有时候很有用:

  l 在实体持久化到数据库之前需要在事务内做检查

  l 需要在验证的过程中访问数据库信息,比如在保存订单之前先检查库存的数量

  l 需要遍历实体关联或者组合的实体,比如Order里面的OrderItem实体

  l 需要跟踪某些实体的增/删/改操作,比如希望跟踪Order和OrderItem的变化情况

事务监听器

  CUBA 事务监听器也在事务的上下文环境中工作,但是跟实体监听器不一样的是,事务监听器是在事务级别被调用的。

  因此,事务监听器是终极大杀器,能监管到所有的数据库交互,但是这样也带来了弱点:

  l 不是很好编码

  l 如果做太多检查会显著的降低性能

  l 编码需要很小心,一个bug可能会导致整个应用都启动不了

  所以事务监听器在需要用同一算法检查很多不同类型的实体的时候是个好办法。比如需要给支持所有业务的“欺诈侦探器”填充数据的时候。

  我们看看下面这个例子,检查是否有实体带有 @FraudDetectionFlag 注解,如果有的话,调用欺诈侦探器来检查一下。注意,这个方法会在每次数据库提交的事务都调用,所以代码需要尽可能少的检查数据对象,并且越快越好。

  只需要实现 BeforeCommitTransactionListener 接口的 beforeCommit 方法,托管bean就会变成事务监听器。事务监听器会在应用启动的时候自动装载。CUBA会将所有实现了 BeforeCommitTransactionListener 或者 AfterCompleteTransactionListener 接口的类注册为事务监听器。

结论

  Bean 验证(JPA 303 349 980)基本能满足企业级应用中 95% 的数据验证的情况。这个方案最大的优点是,大部分验证的逻辑都集中到了数据模型类中。因此很容易找到代码,可读性强还容易维护。Spring,CUBA以及很多类库都能知道这些标准并且在UI输入值的时候,调用方法的时候或者做ORM持久化的时候自动调用验证代码,从开发者角度来说,这些验证就像是小魔法。

  有些软件工程师认为,在数据模型层面做的验证复杂且带有侵入性,觉得在UI层做验证就够了。但是,我个人觉得,在UI或者UI控制器中写很多验证点是很容易出问题的。另外,我们这里讨论的验证方法在跟平台集成的时候,并不是侵入性的代码,因为平台会感知这些验证器、监听器然后将它们自动集成到客户端层。

  最后,我们制定一个经验规则来选择最佳的验证方法:

  l JPA验证:功能有限,但是在实体类上做最简单的约束是最好的选择。要求这些约束能映射成DDL

  l Bean验证:灵活、简洁、声明式、可重用而且易读。基本上能覆盖模型中需要的所有验证,如果不需要在事务中进行验证的话,这是最好的选择

  l 合同验证:也是一种bean验证,不过是应用在方法上。如果需要检查输入和输出参数,比如REST调用,可以使用这个方法

  l 实体监听器:尽管不像bean验证那样是使用全部声明式的方式,但是可以在数据库事务中对比较复杂的对象关系图做验证。比如需要从数据库加载一些信息来做决定。Hibernate也有类似的监听器

  l 事务监听器:危险但是这是事务级别的终极武器。如果需要在运行时对实体进行验证或者需要对很多不同类型的实体使用同一种验证方法的时候可以选用

  希望这篇文章能刷新你对于Java企业级应用中验证方法的记忆,也希望在提升项目架构方面提供一点点参考。

Java中的数据验证的更多相关文章

  1. Struts2数据验证与使用Java代码进行数据验证

    Struts2数据验证 使用Java代码进行数据验证 重写ActionSupport的validate()方法 对Action类的中所有请求处理方法都会进行验证! 对Action类的数据属性进行检查, ...

  2. Java 设置Excel数据验证

    数据验证是Excel 2013版本中,数据功能组下面的一个功能,在Excel2013之前的版本,包含Excel2010 Excel2007称为数据有效性.通过在excel表格中设置数据验证可有效规范数 ...

  3. WPF中的数据验证

    数据验证 WPF的Binding使得数据能够在数据源和目标之间流通,在数据流通的中间,便能够对数据做一些处理. 数据转换和数据验证便是在数据从源到目标 or 从目标到源 的时候对数据的验证和转换. V ...

  4. Java中静态数据的初始化顺序

    Java的类中的数据成员中包含有静态成员(static)时,静态数据成员的初始化顺序是怎样的呢? [程序实例1] import java.util.*; import java.lang.*; imp ...

  5. Java中浮点型数据Float和Double进行精确计算的问题

    Java中浮点型数据Float和Double进行精确计算的问题 来源  https://www.cnblogs.com/banxian/p/3781130.html 一.浮点计算中发生精度丢失     ...

  6. Java中XML数据

    Java中XML数据 XML解析——Java中XML的四种解析方式 XML是一种通用的数据交换格式,它的平台无关性.语言无关性.系统无关性.给数据集成与交互带来了极大的方便.XML在不同的语言环境中解 ...

  7. 在kettle中实现数据验证和检查

    在kettle中实现数据验证和检查 在ETL项目,输入数据通常不能保证一致性.在kettle中有一些步骤能够实现数据验证或检查.验证步骤能够在一些计算的基础上验证行货字段:过滤步骤实现数据过滤:jav ...

  8. Java中的参数验证(非Spring版)

    1. Java中的参数验证(非Spring版) 1.1. 前言 为什么我总遇到这种非正常问题,我们知道很多时候我们的参数校验都是放在controller层的传入参数进行校验,我们常用的校验方式就是引入 ...

  9. JAVA中的数据存储空间简述

    在 JAVA 中,有六个不同的地方可以存储数据: 1. 寄存器( register ): 最快的存储区,因为它位于不同于其他存储区——处理器内部.但是寄存器的数量极其有限,所以寄存器由编译器根据需求进 ...

随机推荐

  1. asp.net—单例模式

    一.单例模式是什么? 定义:确保一个类仅仅能产生一个实例,并且提供一个全局访问点来获取该实例. 二.单例模式怎么用? class SingleCase { public string Name{get ...

  2. NetCore入门篇:(六)Net Core项目使用Controller之一

    一.简介 1.当前最流行的开发模式是前后端分离,Controller作为后端的核心输出,是开发人员使用最多的技术点. 2.个人所在的团队已经选择完全抛弃传统mvc模式,使用html + webapi模 ...

  3. C#构造方法(函数)

    一.概括 1.通常创建一个对象的方法如图: 通过  Student tom = new Student(); 创建tom对象,这种创建实例的形式被称为构造方法. 简述:用来初始化对象的,为类的成员赋值 ...

  4. js form 表单 重置 清空

    清空 和 重置的差异是 清空是彻底清空input内容即便初始值value有值,重置是将input内容重置为value初始状态 很简单记录下 方便之后使用 //重置 //document.getElem ...

  5. [Xamarin]我的Xamarin填坑之旅(一)

    一想到明天是星期五,不对,是今天,心里就很激动,毕竟明天没课.激动之余,来写一篇博客,记录一下最近踏坑Xamarin开发校园助手APP的一些事儿.也许更像是一篇流水账. 在扯Xamarin之前,有必要 ...

  6. Android 标题栏(1)

    本文来自网易云社区 作者:孙有军 标题栏在每个应用中都有,有各种各样的标题栏,今天我们就主要来说说标题栏怎么做,主要内容涉及到自定义标题,ActionBar,Toolbar等知识. 自定义标题 几年前 ...

  7. Day 33 Socket编程.

    套接字 (socket)处使用 基于TCP 协议的套接字 TCP 是基于链接的 ,服务器端和客户端启动没有顺序. 服务器端设置: import socket sk =socket.socket() # ...

  8. Java基础学习篇---------继承

    一.覆写(重写) 1.含义:子类的定义方法.属性和父类的定义方法.属性相同时候 方法名称相同,参数相同以及参数的个数也相同,此时为覆写(重写) 扩充知识点: 覆盖:只有属性名字和方法名字相同,类型.个 ...

  9. (1)RGB-D点云生成

    bin文件夹下为生成的可执行文件generate_cloud,执行时和data文件放在同一文件夹下. 图像数据来自小觅相机. src下的源码,包括generatePointCloud.cpp和CMak ...

  10. 微信小程序一些总结

    1.体验版和线上是啥区别,啥关系 在微信开发者工具里提交代码后进入体验版,在微信后台里点击版本管理,就可以看到线上版本,和开发体验版,描述里有提交备注. 在体验版里发布审核之后会进入到线上.他们两个可 ...