如果你在应用中用了我 上一篇文章 中介绍的技术,那么代码量会大大减少的。不过Upida 还有另外一个非常重要并且有用的功能--验证。
实现非常简单。首先,必须指定需要验证的类,通常是一些领域类。其次,必须指定每一个类的验证组--举个例子,Client 类有两个组-保存前进行验证和更新前进行验证。这就意味着同样的Client类需要两种不同方式的验证--保存和更新。有时你可能需要不同的验证组--举个例子,分配或合并或其它。并且最后一步是为每一个组实现一个验证器类。举个例子,Client类必须有两个验证器--ClientSaveValidator和ClientUpdateValiator。
让我们为Client类创建这些验证器。每个验证器(或类型检查器)都是从abstractConstraintValidator类派生而来的。它包含15个用于检查不同限制的方法。例如 MustBeNull, MustRegexpr等等。
***注意,有一些以Is*开头的方法 (IsNull, IsAssigned, IsEqualTo)。这些方法可以简单地对一种限制进行检查,并返回true或false。另一些方法以Must*开头 (MustBeNull, MustBeAssigned, MustEqualTo)。这些方法才真的是在检验流程中负责检查和错误标记。
所有的这些限制检查方法都接受错误提示信息参数。为了实现类型检查类,必须实现抽象方法Validate()。在其中必须访问每个属性并调用相应的检查方法。首先要调用callField()方法,来获取field的值和名字,然后调用带有错误提示信息的检查方法。检查方法会对当前field进行检查,之后再次调用Field()方法来检查另一个field,所有相关的检查方法也都会作用于下一个field。
首先我们将错误消息定义为静态类。
public class Errors { public static final string MUST_BE_EMPTY = "must be empty"; public static final string REQUIRED = "is required"; public static final string LENGTH_3_20 = "must be between 3 and 20 characters"; public static final string GREATER_ZERO = "must be greater than zero"; public static final string MUST_BE_NUMBER = "invalid format"; public static final string NUMBER_OF_LOGINS = "must be at least one login"; }
正如之前提到的,类 ConstraintValidator 非常简单,包含了基本的验证例程。通常,在我们的应用程序中,有着共同的约束,为了满足这个需求,我要创建一个自己的基本验证抽象类,它是应用程序中所有验证器的基类。
public class HandyValidator: ConstraintValidator{ public boolean isAssignedAndNotNull() { return this.isAssigned() && !this.isNull(); } public void required() { this.mustBeAssigned(Errors.REQUIRED); this.mustBeNotNull(Errors.REQUIRED); this.stop(); } public void required(String wrongFormatMessage) { this.mustBeAssigned(Errors.REQUIRED); this.mustBeValidFormat(wrongFormatMessage); this.mustBeNotNull(Errors.REQUIRED); self.stop(); } public void requiredIfAssigned(String msg){ if(this.isAssignedAndNotNull()) { this.required(msg); } } public void mustBeEmail(String msg) { this.mustRegexpr("^[_a-z0-9-]+(.[_a-z0-9-]+)*@[a-z0-9-]+(.[a-z0-9-]+)*(.[a-z]{2,4})$", msg); } public void missing(String field, Object value) { this.field(field, value); this.setSeverity(Severity.Fatal); this.mustBeNotAssigned(Errors.MUST_BE_EMPTY); } }
类 HandyValidatorclass 包含了一些复杂例程,我将在自己的特殊验证器中复用它们。让我们简单讲讲上面的类。
required()程序看起来比较复杂,它通过mustBeAssigned()方法来检查一个字段是否由JSON formatter来设值,通过mustBeValidFormat()方法来检查字段是否被正确的解析(这对于判断数值型字段是否解析失败很有用),最后一个约束是非空检查。stop()方法表示如果当前字段的验证已经失败,剩余的约束将被忽略。
missing()方法简单些 - 它对当前字段进行设定,确保字段的值不是由JSON formatter来设置。
用HandyValidator类作为基类,我们可以极大的简化类型校验。让我们看一下ClientSaveValidator是什么样子:
@Component @Scope(value="prototype") public class ClientSaveValidator extends HandyValidator{ @Override public void validate(Object state) { Client target = this.getTarget(); this.missing("id", target.getId()); // Id field must be missing in JSON this.field("name", target.getName()); this.required(); // 必须以JSON格式表示且不能为NULL this.mustHaveLengthBetween(3, 20, Errors.LENGTH_3_AND_20); // 长度介于3和20之间. this.field("lastname", target.getLastname()); this.required(); // 必须是JSON格式且不能是NULL this.mustHaveLengthBetween(3, 20, Errors.LENGTH_3_AND_20); // 长度 this.field("age", target.getAge()); this.required(Errors.MUST_BE_NUMBER); // 必须是JSON格式且不能是NULL,并检验数字 self.mustBeGreaterThan(0, Errors.GREATER_THAN_ZERO); this.field("logins", target.getLogins()); this.required(); // 必须是JSON格式且不能为NULL this.mustHaveCountBetween(1, 5, Errors.WRONG_SIZE); //collection大小 1-5 this.stop(); //如果collection为null或大小有误 - 则不再对该字段做更多检查 this.nestedList(Login.class, Groups.SAVE.class, null); // 校验每个登录对象是否属于SAVE用户组 } }
Client 类的 ClientUpdateValidator 与 ClientSaveValidator 有细微的区别。
@Component @Scope(value="prototype") public class ClientUpdateValidator extends HandyValidator{ @Override public void validate(object state) { Client target = this.getTarget(); this.field("id", target .Id); this.required(Errors.MUST_BE_NUMBER); // Id field must be present on Update this.field("name", target.getName()); this.required(); // must be present in JSON and must be not NULL this.mustHaveLengthBetween(3, 20, Errors.LENGTH_3_20); // Length between 3 and 20. this.field("lastname", target.getLastname()); this.required(); // Must be present in JSON and not NULL this.mustHaveLengthBetween(3, 20, Errors.LENGTH_3_20); // length this.field("age", target.getAge()); this.required(Errors.MUST_BE_NUMBER); // Must be present in JSON and not NULL, and valid number this.field("logins", target.getLogins()); this.required(); this.mustHaveCountBetween(1, 5, Errors.WRONG_SIZE); this.stop(); this.nestedList(Login.class, Groups.MERGE.class, null); // Validate every login object against MERGE group } }
其中的 Groups 集合类型是 Upida 的一部分,它看起来差不多是这样:
public Enum Groups { DEFAULT, SAVE, UPDATE, MERGE, ASSIGN, .....
Upida.Net 并不依赖于 Groups 类型,你完全可以定义自己的 group 。下面我们来为 Login 编写验证器。当我们保存 Client 对象时,所有从属的 login 都要保存,这意味着我们必须写一个 LoginSaveValidator 。当我们更新 Client 对象时,所有从属的 login 要么保存,要么更新——这意味着我要写一个 LoginMergeValidator 。
@Component @Scope(value="prototype") public class LoginSaveValidator extends HandyValidator{ @Override public void validate(object state) { Login target = this.getTarget(); self.missing("id", target.getId()); self.field("name", target.getName()); self.required(); self.mustHaveLengthBetween(3, 20, Errors.LENGTH_3_AND_20); self.field("password", target.getPassword()); self.required(); self.mustHaveLengthBetween(3, 20, Errors.LENGTH_3_AND_20); self.field("enabled", target.getEnabled()); self.required(); self.missing("client", target.getClient()); } } public class LoginMergeValidator extends HandyValidator{ @Override public void validate(object state) { Login target = this.getTarget(); // if ID is present - check it as a Required field // if ID is missing - it is valid self.field("id", target.getId()); self.requiredIfAssigned(Errors.MUST_BE_NUMBER); self.field("name", target.getName()); self.required(); self.mustHaveLengthBetween(3, 20, Errors.LENGTH_3_AND_20); self.field("password", target.getPassword()); self.required(); self.mustHaveLengthBetween(3, 20, Errors.LENGTH_3_AND_20); self.field("enabled", target.getEnabled()); self.required(); self.missing("client", target.getClient()); } }
好的,我已经创建了针对我所有的域类所进行的类型校验。现在我想在我的工作流里包含这些验证。你可以根据你的需要,无论在控制器或者是业务中进行验证。我想把它放在业务上,因为我可能在将来会写一个和数据库交互的校验,因此我想有一个开源的数据库会话。我必须把Upida.Validator类注入到我的业务层去。
@Service public class ClientService implements IClientService { private IValidationContext validator; private IClientDao clientDao; private IMapper mapper; public ClientBusiness(IValidationContext validator, IMapper mapper, IClientDao clientDao) { this.validator = validator; this.mapper = mapper; this.clientDao = clientDao; } .... @Override public void save(Client item) { this.validator.assertValid(item, Client.class, Groups.SAVE.class); this.mapper.map(item, Client.class); this.clientDao.save(item); } @Override public void update(Client item) { this.validator.assertValid(item, Client.class, Groups.UPDATE.class); Client existing = this.clientDao.load(item.getId()); this.mapper.mapTo(item, existing, Client.class); this.clientDao.merge(existing); } }
正如你所见,我调用目标对象和被请求组的AsserValid()方法。这个校验器会根据Group来识别使用哪个类型的校验类。最后一步是来定义多对的类型校验和群组。你仅仅只需添加ValidateWith属性到你的域类中即可。
@ValidateWith.List([ @ValidateWith(typeof(ClientSaveValidator), Groups.SAVE), @ValidateWith(typeof(ClientUpdateValidator), Groups.UPDATE)]) public class Client extends Dtobase { private Integer id; private String name; ..... @ValidateWith.List([ @ValidateWith(typeof(LoginSaveValidator), Groups.SAVE), @ValidateWith(typeof(LoginMergeValidator), Groups.MERGE)]) public class Login extends Dtobase { private Integer id; private String name; .....
如上所示,现在校验器便能工作了。如果从业务层调用assertValid()方法,那么它将根据所提供的组来识别使用哪个类型校验器。然后,它会调用抽象方法Validate()的实现类。如果校验成功,将没有任何反应。如果校验失败,会抛出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(); if(Severity.Fatal == fail.getFailures().getSeverity()) { fail.setMain("You are trying to break validation !!!"); } } else { fail = new FailResponse(ex.getMessage()); } return fail; }
我用我的JSON队列(属性路径 - 错误信息)对应 - FailResponse类来替代HTTP的响应内容。
现在,无论ValidationException什么时候抛出,都会被ErrorHandlerAttribute处理器处理,一系列的属性路径和错误信息也会以JSON的形式在HTTP响应中返回。
最后一步是把这些信息在HTML中显示出来。你可以自由选择JavaScript框架,用 AngularJS 还是 KnockoutJS,还是其他。 Upida.Net 自带两个小的JavaScript库——一个 AngularJS 版和一个 KnockoutJS 版。这些库让显示错误信息更加简单。例如,如果你用angular,HTML代码应该类似这样:
<label>Name:</label> <input type="text" ng-model="name" /> <span class="error" errorkey="name"></span>
错误信息将会填在 span 元素的 errorkey 中,绑定在属性 name 上。errorkey 也可以更复杂——如 "Create Client" 形式。
你可以在我的下一篇文章中学到如何用 AngularJS 创建一个单页面应用:AngularJS 单页面应用与 Upida.Net。
文章: Java Spring Mvc 用 Upida 创建单页面应用 (前端/AngularJS)
2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务