65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Upida/Jeneva 验证传入的 JSON

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2014 年 3 月 3 日

CPOL

7分钟阅读

viewsIcon

30099

downloadIcon

217

使用 JSON 进行 Web 开发很简单。

引言

在上一篇文章中,我描述了 Jeneva 如何帮助解决在实现基于 Spring Mvc 的 Web 应用程序时遇到的常见问题。本文将展示 Jeneva 如何也能极大地简化您的验证流程。

*注意,本文假设您已经阅读过我之前的文章

背景

如果您在 Web 应用程序中应用我上一篇文章中的技术,可以大大减少自定义编码的数量。但 Jeneva 还有一个非常重要且有用的功能——验证。

实现非常简单。首先,您必须识别需要验证的类,通常这些是领域类。其次,您必须为每个类确定验证方法——例如,Client 类有两个方法——保存前验证和更新前验证。这意味着同一个类——Client——可以用两种不同的方式进行验证——针对保存和针对更新。有时,您可能需要不同的验证方法——例如,AssignMerge 或其他任何方法。最后一步是实际实现这些验证方法。例如,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() - 如果字段值未正确解析(integerdouble),则注册一个失败。

正如您所注意到的,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,其中只有一个失败,并且属性路径为空。

就是这样。现在验证应该可以工作了。如果验证成功,什么也不会发生。如果验证失败,会抛出 ValidationExceptionValidationException 包含一个名称-值对列表——属性路径和错误消息。为了在 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 的正确位置显示这些消息。您可以自由地为 AngularJSKnockoutJS 或其他任何东西编写自定义 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

参考文献

© . All rights reserved.