使用 Upida/Jeneva 验证传入的 JSON





5.00/5 (6投票s)
使用 JSON 进行 Web 开发很简单。
引言
在上一篇文章中,我描述了 Jeneva 如何帮助解决在实现基于 Spring Mvc 的 Web 应用程序时遇到的常见问题。本文将展示 Jeneva 如何也能极大地简化您的验证流程。
*注意,本文假设您已经阅读过我之前的文章。
背景
如果您在 Web 应用程序中应用我上一篇文章中的技术,可以大大减少自定义编码的数量。但 Jeneva 还有一个非常重要且有用的功能——验证。
实现非常简单。首先,您必须识别需要验证的类,通常这些是领域类。其次,您必须为每个类确定验证方法——例如,Client
类有两个方法——保存前验证和更新前验证。这意味着同一个类——Client
——可以用两种不同的方式进行验证——针对保存和针对更新。有时,您可能需要不同的验证方法——例如,Assign
或 Merge
或其他任何方法。最后一步是实际实现这些验证方法。例如,Client
类必须有两个验证方法——validateForSave()
和 validateForUpdate()
。
实施
让我们为 Client
类创建这些验证方法。为了遵循所有的 SOLID 原则,我将创建一个单独的类——ClientValidator
——它将包含这些验证方法。基于 Jeneva 的验证的主要思想如下:每次需要验证某些内容时,您都必须创建一个新的 JenevaValidationContext
类的实例。每次发现错误时,您都必须使用其方法在上下文实例中注册它。使用上下文实例可以确保每个错误消息都与相应的属性路径绑定。顺便说一下,上下文类已经包含了一些简单的验证例程,例如,它可以检查特定字段是否为 null
,或者它是否存在于 JSON 中,它可以检查文本长度或集合大小,还可以检查正则表达式等。您必须知道,Jeneva 管理数据反序列化,并存储有关每个 JSON 字段的信息,因此,之后您可以验证一个字段是否存在于 JSON 中,是否为 null
,或者在反序列化期间是否被正确解析。这些信息可以通过 JenevaValidationContext
类的方法访问。
JenevaValidationContext
类的主要目标之一是跟踪属性路径。例如,当您验证一个对象然后验证其子对象时,上下文类确保所有错误消息都与相应的属性路径相关联。验证的结果是一个失败列表,其中每个失败项由属性路径文本和消息组成。这个失败结构被序列化为 JSON 并发送回浏览器,在浏览器中它被解析并正确地显示在 HTML 中的正确位置。
最佳实践是继承 JenevaValidationContext
类,在其中扩展一些额外的特定于应用程序的验证例程,然后在验证方法中使用该子类。下面是如何扩展上下文类的示例。
public class ValidationContext extends JenevaValidationContext implements IValidationContext {
public ValidationContext(IJenevaContext context) {
super(context);
}
@Override
public void required() {
this.assigned("is required");
if (this.isFieldValid() && this.isValidFormat())
{
this.notNull("is required");
}
}
@Override
public void missing() {
this.notAssigned("must be empty");
}
@Override
public void number() {
this.validFormat("must be valid number");
}
@Override
public void date() {
this.validFormat("must be valid date");
}
@Override
public void floating() {
this.validFormat("must be valid floating point number");
}
@Override
public void email() {
this.regexp("\\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}\\b", "must be valid Email");
}
@Override
public void text() {
this.stringLengthBetween(3, 20, "must be between 3 and 20 characters");
}
@Override
public void trueFalse() {
this.validFormat("must be 'yes' or 'no'");
}
}
上述逻辑非常有帮助,并将使验证方法极其简单和可读。例如,我将不再需要为长度在 3 到 20 个字符之间的字符串
编写多余的错误消息,我将使用 text()
。日期、双精度浮点数、数字、必填字段等也是如此。
在下面的代码片段中,您可以看到用于保存
的验证方法 validateForSave()
是如何实现的。
@Service
public class ClientValidator implements IClientValidator {
@Autowired
public ILoginValidator loginValidator;
@Override
public void validateForSave(Client target, IValidationContext context) {
context.setField("id", target.getId()); // validate id property now
context.missing(); // it must be missing - not present in json
context.setField("name", target.getName()); // validate name property now
context.required(); // it is required - present in json and not null
context.text(); // it also must be between 3 and 20 characters length
context.setField("lastname", target.getLastname());
context.required();
context.text();
context.setField("age", target.getAge());
context.required();
context.number();
context.greaterThan(0, "must be greater than zero");
context.setField("logins", target.getLogins());
context.required();
context.countBetween(1, 5, "must be at least one login");
context.addNested(); // let us validate child properties - logins
int index = 0;
for (Login login : target.getLogins())
{
context.setIndex(index++); // the logins - is indexed
// property - property path will have index - logins[4].name
context.setTarget(login); // validate login class
this.loginValidator.validateForSave(login, context);
}
context.removeNested(); // switch back - up in property path - back to
// Client's properties
}
}
setField()
方法告诉上下文当前正在验证哪个字段,即,如果您在上下文中注册一个失败,它将具有当前字段的属性路径。验证例程必须在 setField()
方法调用之后进行。JenevaValidationContext
类包含许多验证例程。这些例程通常执行两个操作:首先,它们检查一个条件(例如,检查字段值是否为 null
),其次,如果条件不为真,它们会注册一个失败(使用 fail()
方法)。例如,assigned()
- 检查字段是否已赋值(是否存在于 JSON 中),如果为 false
- 它会使用当前属性路径注册一个失败;notNull()
, isNull()
是自解释的;validFormat()
- 如果字段值未正确解析(integer
或 double
),则注册一个失败。
正如您所注意到的,JenevaValidationContext
类除了验证例程方法外,还包含其他重要方法:setTarget()
设置当前被验证的对象,这个方法很重要,应总是在任何验证例程之前调用;addNested()
- 这个方法将当前属性作为嵌套对象传播到属性路径中,所有后续对 setField()
的调用都将导致属性名与嵌套对象名连接;removeNested()
方法执行相反的操作;setIndex()
方法也向当前属性路径添加索引 - "[" + index + "]"
。
在这里,您可以看到 Login
类的验证方法。
@Service
public class LoginValidator implements ILoginValidator {
@Override
public void validateForSave(Login target, IValidationContext context) {
context.setField("id", target.getId());
context.missing();
context.setField("name", target.getName());
context.required();
context.text();
context.setField("password", target.getPassword());
context.required();
context.text();
context.setField("enabled", target.getEnabled());
context.trueFalse();
context.setField("client", target.getClient());
context.missing();
}
}
当所有领域和 DTO 类的验证器都完成后,我们就可以自由地定义一个 facade
类,它将被注入到我们的服务或控制器中,并用于触发验证。这是一个 validation facade
的示例。
@Service
public class ValidationFacade implements IValidationFacade {
@Autowired
public IValidationContextFactory contextFactory;
@Autowired
public IClientValidator clientValidator;
// @Autowired
// other validators
@Override
public void assertClientForSave(Client target) {
IValidationContext context = this.contextFactory.getNew();
context.setTarget(target);
this.clientValidator.validateForSave(target, context);
context.assertValid();
}
@Override
public void assertClientForUpdate(Client target) {
IValidationContext context = this.contextFactory.getNew();
context.setTarget(target);
this.clientValidator.validateForUpdate(target, context);
context.assertValid();
}
}
这个类看起来非常简单,这里我使用简单的工厂方法来获取验证上下文的新实例。最重要的是每个方法的最后一行——assertValid()
——如果在上下文中至少注册了一个错误,这个方法会抛出 ValidationException
,否则它什么也不做。
在这里,您可以看到 facade 是如何被注入并在服务层中使用的。
@Service
public class ClientService implements IClientService {
@Autowired
public IMapper mapper;
@Autowired
public IValidationFacade validator;
@Autowired
public IClientDao clientDao;
@Override
public void save(Client item) {
this.validator.assertClientForSave(item);
this.mapper.map(item, Client.class);
this.clientDao.save(item);
}
@Override
public void update(Client item) {
this.validator.assertClientForUpdate(item);
Client existing = this.clientDao.load(item.getId());
this.mapper.mapTo(item, existing, Client.class);
this.clientDao.merge(existing);
}
}
顺便说一下,验证上下文类非常方便,易于扩展和以不同方式使用。您总是可以尝试它的方法并获得不同的行为。在示例代码中,您将看到它如何在不同情况下用于验证——例如,在删除
之前进行验证。请参阅 ValidationFacade
类中的 assertClientExists()
和 assertMoreThanOneClient()
方法。
@Override
public void assertClientExists(Client item) {
if (item == null)
{
IValidationContext context = this.contextFactory.getNew();
context.fail("Client does not exist");
context.assertValid();
}
}
@Override
public void assertMoreThanOneClient(long count) {
if (count == 1)
{
IValidationContext context = this.contextFactory.getNew();
context.fail("Cannot delete the only client");
context.assertValid();
}
}
在这些方法中,您不依赖于目标验证对象。您只需要断言某个条件已满足。在这种情况下,会抛出一个 ValidationException
,其中只有一个失败,并且属性路径为空。
就是这样。现在验证应该可以工作了。如果验证成功,什么也不会发生。如果验证失败,会抛出 ValidationException
。ValidationException
包含一个名称-值对列表——属性路径和错误消息。为了在 Spring MVC 中正确处理此异常,我将在我的控制器中创建一个方法,并用 @ExceptionHandler
注解标记它。这种技术在 Spring MVC 中处理异常时很常见。这是该方法的实现。
@ExceptionHandler
@ResponseBody
public FailResponse handleError(Exception ex, HttpServletResponse response) {
FailResponse fail = null;
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
if(ex instanceof ValidationException) {
fail = ((ValidationException) ex).buildFailResponse();
}
else {
fail = new FailResponse(ex.getMessage());
}
return fail;
}
我用我的(属性路径 - 错误消息)对的 JSON 数组替换 HTTP 响应内容 - FailResponse
类。
现在,每当抛出 ValidationException
时,它都将由 handleError()
方法处理,并且属性路径和错误消息的列表将作为 JSON 在 HTTP 响应中发回。
前端
最后一步是在 HTML 的正确位置显示这些消息。您可以自由地为 AngularJS 或 KnockoutJS 或其他任何东西编写自定义 JavaScript。Jeneva 带有一个小型的 AngularJS JavaScript 库,这使得显示这些错误变得更简单。例如,如果您使用 angular,您的 HTML 必须看起来像这样:
<label>Name:</label>
<input name="name" type="text" ng-model="name" jv-path="name" />
<span class="error" ng-if="form.name.$error.jvpath"
ng-repeat="msg in form.name.$jvlist">{{msg}}</span>
jVpath
指令的工作方式与任何 AngularJS 验证指令相同,即,它表明 textbox
的名称负责 "name
" 属性路径。下面的 span
将重复显示分配给 "name
" 属性路径的验证消息。
您可以在我的下一篇文章中了解如何使用 AngularJS 创建单页应用程序 (SPA):AngularJS 单页应用与 Jeneva。