我经常看到的项目几乎没有任何有意识的数据验证策略。 他们的团队在截止日期,明确要求的巨大压力下工作,只是没有足够的时间以适当且一致的方式进行验证。 因此,数据验证代码随处可见:JavaScript片段,Java屏幕控制器,业务逻辑bean,域模型实体,数据库约束和触发器。 这段代码充满了if-else语句,引发了各种未经检查的异常,很难找到可以验证该死数据的正确位置……因此,过了一段时间,当项目长大到足够长的时候,它就变得非常困难保持此验证的一致性和后续要求非常昂贵,正如我所说,这些要求通常很模糊。
有没有办法以一种优雅,标准和简洁的方式进行数据验证? 不会成为无法阅读的罪过的方法,是帮助我们将大多数数据验证逻辑保持在一起的方法,以及流行的Java框架的开发人员已经为我们完成了大部分代码的方法?
就在这里。
对于我们CUBA Platform的开发人员而言,让我们的用户遵循最佳实践非常重要。 我们认为验证代码应为:
- 可重复使用并遵循DRY原则;
- 表达清晰自然
- 放置在开发人员希望看到的地方;
- 能够检查来自不同数据源的数据:用户输入,SOAP或REST调用等。
- 意识到并发性;
- 由应用程序隐式调用,而无需手动调用检查;
- 使用简洁的设计对话框向用户显示清晰,本地化的消息;
- 遵循标准。
在本文中,我将针对所有示例使用基于CUBA平台的应用程序。 但是,由于CUBA基于Spring和EclipseLink,因此大多数示例都可用于支持JPA和bean验证标准的任何其他Java框架。
数据库约束验证
也许,最常见,最直接的数据验证方法是使用数据库级别的约束,例如必需的标志(“非空”字段),字符串长度,唯一索引等。 对于企业应用程序来说,这种方式非常自然,因为此类软件通常以数据为中心。 但是,即使在这里,开发人员也经常犯错,分别为应用程序的每一层定义约束。 此问题通常是由开发人员之间的职责分散引起的。
让我们举一个例子,你们大多数人面对甚至参与:)。 如果某个规范说护照字段的数字应该有10位数字,则很可能会在所有地方对其进行检查:由DDL中的DB架构师,由相应Entity和REST服务中的后端开发人员,最后由UI开发人员直接在客户端源中进行检查-码。 后来,此要求发生了变化,并且字段的大小最多增长了15位。 技术支持更改了数据库约束,但是对于用户而言,这没有任何意义,因为无论如何都不会通过客户端检查……
每个人都知道避免这种问题的方法,验证必须集中! 在CUBA中,这种验证的中心点是实体上的JPA批注。 基于此元信息,CUBA Studio生成正确的DDL脚本并在客户端应用相应的验证器。
如果更改了JPA批注,则CUBA将更新DDL脚本并生成迁移脚本,因此,下次您部署项目时,基于JPA的新限制将应用于应用程序的UI和数据库。
尽管涉及到DB级别的简单性和实现方式是完全防弹的,但是JPA注释受到可以以DDL标准表示的最简单情况的限制,而无需涉及特定于DB的触发器或存储过程。 因此,基于JPA的约束可以确保实体字段是唯一的或必填的,或者可以为varchar列定义最大长度。 另外,您可以使用@UniqueConstraint批注为列的组合定义唯一约束。 但这差不多。
但是,在需要更复杂的验证逻辑的情况下,例如检查字段的最大值和最小值,使用表达式进行验证或对您的应用程序进行特定的自定义检查,我们需要使用众所周知的方法“ Bean验证” 。
Bean验证
我们所知道的,遵循标准是一种好习惯,标准通常具有较长的生命周期,并且在数千个项目中得到了实践证明。 Java Bean验证是一种在JSR 380、349 和303及其实现中固定的方法: Hibernate Validator和Apache BVal 。
尽管许多开发人员都熟悉这种方法,但是它的好处常常被低估了。 即使对于旧项目,这也是添加数据验证的简便方法,它使您能够以清晰,直接和可靠的方式表达验证,并尽可能接近业务逻辑。
使用Bean验证方法可以为您的项目带来很多好处:
- 验证逻辑集中在您的域模型附近:定义值,方法,bean约束以一种自然的方式完成,可以将OOP方法提升到一个新的水平。
- Bean验证标准为您提供了数十种开箱即用的验证批注 ,例如:@ NotNull,@ Size,@ Min,@ Max,@ Pattern,@ Email,@ Past,次标准,例如@ URL,@ Length,强大的@ScriptAssert和很多其他的。
- 您不受预定义约束的限制,可以定义自己的约束注释。 您还可以通过结合其他方式进行新的注释,或者创建一个全新的注释并定义将用作验证器的Java类。
- 例如,在前面的示例中,我们可以定义一个类级别的批注@ValidPassportNumber,以检查护照号码是否遵循正确的格式,该格式取决于国家/地区字段的值。
- 您不仅可以对字段和类施加约束,还可以对方法和方法参数施加约束。 这称为“合同验证”,是下一节的主题。
CUBA平台(和其他框架一样)在用户提交数据时自动调用这些Bean验证,因此,如果验证失败,用户将立即收到错误消息,而您不必担心手动运行这些Bean验证器。
让我们再次看一下护照号码示例,但是这次我们要在实体上添加几个附加约束:
- 人名的长度应为2个或更多,并且是格式正确的名称。 Regexp非常复杂,但是Charles Ogier de Batz de Castelmore Comte d'Artagnan通过了检查,R2D2没有通过:);
- 人的身高应在以下范围内:0 <身高<= 300厘米;
- 电子邮件字符串应为格式正确的电子邮件地址。
因此,通过所有这些检查,Person类如下所示:
我认为使用@ NotNull,@ DecimalMin,@ Length,@ Pattern等标准注释非常清楚,不需要太多注释。 让我们看看如何实现自定义@ValidPassportNumber批注。
我们全新的@ValidPassportNumber会检查Person#passportNumber是否匹配特定于Person#country定义的每个国家的正则表达式模式。
首先,遵循文档( CUBA或Hibernate文档是很好的参考),我们需要使用此新注释标记我们的实体类,并向其传递groups参数,其中UiCrossFieldChecks.class表示应在检查了跨字段检查阶段和Default.class将约束保留在默认验证组中。
批注定义如下所示:
@Target(ElementType.TYPE)定义此运行时批注的目标为一个类,@Constraint(validatedBy =…)声明批注实现在ValidPassportNumberValidator类中,该类实现ConstraintValidator <…>接口,并在isValid( …)方法,该代码以非常简单的方式进行实际检查:
而已。 使用CUBA平台,我们无需编写任何代码即可使我们的自定义验证工作并在用户输入错误时向用户发送消息。 没什么复杂的,你同意吗?
现在,让我们检查一下所有这些东西是如何工作的。 CUBA还有一些额外的好处:它不仅向用户显示错误消息,而且还用红色线条突出显示尚未通过单字段Bean验证的表单字段:
这不是一件好事吗? 在将几个Java批注添加到域模型实体之后,您在用户浏览器中获得了不错的错误UI反馈。
在本节结束时,让我们再次简要列出实体的bean验证的优点:
- 清晰易读;
- 它允许在域类中定义值约束;
- 它是可扩展和可定制的;
- 它与许多流行的ORM集成在一起,并且在将更改保存到数据库之前会自动调用检查。
- 当用户在UI中提交数据时,某些框架还自动运行Bean验证(但如果不是,则手动调用Validator接口并不难);
- Bean验证是众所周知的标准,因此Internet上有很多关于它的文档。
但是,如果我们需要对方法,构造函数或某些REST端点设置约束以验证来自外部系统的数据,该怎么办? 或者,如果我们想以声明性的方式检查方法参数的值,而不用在每个方法中编写充满if-els的乏味代码,我们需要进行这种检查吗?
答案很简单:bean验证也可以应用于方法!
合同确认
有时,我们需要采取进一步的措施,而不仅仅是应用程序数据模型状态验证。 许多方法可能会受益于自动参数和返回值验证。 这可能不仅在我们需要检查到达REST或SOAP端点的数据时需要,而且在我们要表达方法调用的前提条件和后置条件以确保在执行方法主体之前已检查输入数据或返回值时可能需要处于预期范围内,或者我们只想声明性地表达参数边界以提高可读性。
使用bean验证,可以将约束条件应用于任何Java类型的方法或构造函数的参数和返回值,以检查其调用的前提条件和后置条件。 与传统的检查参数和返回值正确性的方法相比,此方法具有多个优点:
- 不需要强制性地执行检查(例如,通过抛出IllegalArgumentException或类似方法)。 我们宁愿声明性地指定约束,因此我们拥有更具可读性和表达力的代码;
- 约束是可重用,可配置和可自定义的:我们不需要每次都需要进行检查时编写验证代码。 更少的代码–更少的错误。
- 如果用@Validated注释标记了类或方法的返回值或方法参数,则约束检查将由框架在每次方法调用时自动完成。
- 如果可执行文件标有@Documented批注,则其前提条件和后置条件将包含在生成的JavaDoc中。
作为“合同确认”方法的结果,我们拥有清晰的代码,数量更少,更易于支持和理解。
让我们看一下CUBA应用程序中REST控制器界面的外观。 PersonApiService接口允许使用getPersons()方法从数据库中获取人员列表,并使用addNewPerson(…)调用将新人员添加到数据库中。 请记住:bean验证是可继承的! 换句话说,如果用约束为某个类,字段或方法添加注释,则所有扩展或实现该类或接口的后代都将受到相同的约束检查的影响。
这个代码片段对您来说看起来很清楚并且可读吗? (除了@RequiredView(“ _ local”)批注,该批注专用于CUBA平台,并检查返回的Person对象是否具有从PASSPORTNUMBER_PERSON表中加载的所有字段)。
@Valid批注指定getPersons()方法返回的集合中的每个对象也必须针对Person类约束进行验证。
CUBA使这些方法可用于以下端点:
- / app / rest / v2 / services / passportnumber_PersonApiService / getPersons
- / app / rest / v2 / services / passportnumber_PersonApiService / addNewPerson
让我们打开Postman应用程序并确保验证按预期进行:
您可能已经注意到,上面的示例未验证护照号码。 这是因为它需要对addNewPerson方法进行跨参数验证,因为passportNumber验证正则表达式模式取决于国家/地区值。 这样的交叉参数检查直接等效于实体的类级别约束!
JSR 349和380支持交叉参数验证,您可以查阅hibernate文档,以了解如何为类/接口方法实现自定义交叉参数验证器。
超越Bean验证
世界上没有什么是完美的,并且bean验证也有一些局限性:
- 有时,您只想在将更改保存到数据库之前验证复杂的对象图状态。 例如,您可能需要确保来自电子商务系统客户的订单中的所有项目都可以放入您拥有的其中一个运输箱中。 每次用户向他们的订单中添加新项目时,进行这种检查都是相当繁重的操作,并不是最好的主意。 因此,可能需要在Order对象及其OrderItem对象保存到数据库之前仅调用一次此检查。
- 必须在事务内部进行一些检查。 例如,电子商务系统应在将订单提交到数据库之前检查库存是否有足够的商品来履行订单。 这种检查只能在交易内部进行,因为系统是并发的,并且库存数量可以随时更改。
CUBA平台提供了两种在提交之前验证数据的机制,称为实体侦听器和事务侦听器 。 让我们更仔细地看看它们。
实体侦听器
CUBA中的实体侦听 器 与 JPA提供给开发人员的PreInsertEvent,PreUpdateEvent 和PredDeleteEvent侦听器非常相似。 两种机制都允许在实体对象持久化到数据库之前或之后检查实体对象。
在CUBA中定义和连接实体侦听器并不难,我们需要做两件事:
- 创建一个实现实体侦听器接口之一的托管Bean。 为了验证,这些接口中的3个很重要:
BeforeDeleteEntityListener,BeforeInsertEntityListener和BeforeUpdateEntityListener
- 使用@Listeners注释对计划跟踪的实体对象进行注释。
而已。
与JPA标准(JSR 338,第3.5章)相比,CUBA平台的侦听器接口是有类型的,因此您无需强制转换Object参数即可开始使用实体。 CUBA平台增加了与当前实体关联的实体或调用EntityManager加载和更改任何其他实体的可能性。 所有这些更改也会调用适当的实体侦听器调用。
CUBA平台还支持软删除 ,这是将数据库中的实体标记为已删除而不从数据库中删除其记录时的功能。 因此,对于软删除,CUBA平台将调用BeforeDeleteEntityListener / AfterDeleteEntityListener侦听器,而标准实现将调用PreUpdate / PostUpdate侦听器。
让我们来看一个例子。 事件侦听器bean仅使用以下一行代码连接到Entity类:注释@Listeners接受实体侦听器类的名称:
实体侦听器的实现可能如下所示:
当您执行以下操作时,实体侦听器是不错的选择:
- 需要在实体对象持久化到数据库之前在事务内部进行数据检查;
- 在验证过程中需要检查数据库中的数据,例如,检查我们是否有足够的库存货物可以接受订单;
- 不仅需要遍历给定的实体对象(例如Order),还需要遍历与该实体关联或组成的对象,例如Order实体的OrderItems对象;
- 只想跟踪某些实体类的插入/更新/删除操作,例如,您只想跟踪Order和OrderItem实体的此类事件,而无需在交易期间验证其他实体类中的更改。
交易听众
CUBA事务侦听器也可以在事务上下文中工作,但是与实体侦听器相比,它们针对每个数据库事务都被调用。
这赋予了他们最终的力量:
- 没有什么可以吸引他们的注意力,但同样也会给他们带来弱点:
- 他们很难写,
- 如果执行过多不必要的检查,他们会大大降低性能,
- 需要更加谨慎地编写它们:事务侦听器中的错误甚至可能阻止应用程序引导。
因此,当您需要使用相同的算法检查许多不同类型的实体时,例如将数据馈送到为您的所有业务对象服务的自定义欺诈检测器中,事务侦听器是一个很好的解决方案。
让我们看一下检查实体是否使用@FraudDetectionFlag注释进行注释的示例,如果是,请运行欺诈检测器对其进行验证。 再次提醒您,此方法在每个数据库事务提交到系统之前被调用,因此代码必须尝试尽可能快地检查最少的对象。
要成为事务侦听器,受管bean应该只实现BeforeCommitTransactionListener接口并实现beforeCommit方法。 应用程序启动时,事务监听器会自动连接。 CUBA将所有实现BeforeCommitTransactionListener或AfterCompleteTransactionListener的类注册为事务侦听器。
结论
Bean验证(JPA 303、349和980)是可以作为企业项目中发生的95%数据验证案例的具体基础的一种方法。 这种方法的最大优点是,大多数验证逻辑都集中在域模型类中。 因此,它易于发现,易于阅读和支持。 Spring,CUBA和许多库都了解这些标准,并会在UI输入,经过验证的方法调用或ORM持久性过程中自动调用验证检查,因此从开发人员的角度来看,验证就像是一种魅力。
一些软件工程师将影响应用程序域模型的验证视为具有一定侵入性和复杂性,他们说在UI级别进行数据检查已足够好。 但是,我认为在UI控件和控制器中具有多个验证点是很成问题的方法。 此外,当我们将此处讨论的验证方法与可识别Bean验证器,侦听器并将其自动集成到客户端级别的框架集成在一起时,它们不会被视为具有侵入性。
最后,让我们制定一个经验法则来选择最佳的验证方法:
- JPA验证的功能有限,但是如果可以将此类约束映射到DDL,则它是对实体类的最简单约束的理想选择。
- Bean验证是一种灵活,简洁,声明性,可重用和易读的方式,可以涵盖您可能在域模型类中进行的大多数检查。 在大多数情况下,无需在事务内运行验证时,这是最佳选择。
- 合同验证是一个bean验证,但用于方法调用。 当您需要检查方法的输入和输出参数时,例如在REST调用处理程序中,请使用它。
- 实体侦听器:尽管它们不像Bean验证注释那样声明性,但它们是检查大对象图或进行需要在数据库事务内完成的检查的好地方。 例如,当您需要从数据库读取一些数据来做出决定时。 Hibernate具有此类侦听器的类似物。
- 事务侦听器是危险的,但却是在事务上下文中起作用的终极武器。 当您需要在运行时决定必须验证哪些对象或何时需要根据同一验证算法检查许多不同类型的实体时,请使用它。
我希望本文能使您对有关Java企业应用程序中可用的不同验证方法的记忆重新焕发,并为您提供一些有关如何改善正在处理的项目的体系结构的想法。
翻译自: https://www.javacodegeeks.com/2018/10/validation-java-applications.html