2KB项目,专业的源码交易网站 帮助 收藏 每日签到

使用 Upida 验证输入的 JSON 数据

  • 时间:2019-01-23 18:37 编辑:2KB 来源:2KB.COM 阅读:330
  • 扫一扫,手机访问
  • 分享
摘要:
Upida JSON 英文原文:Validating incoming JSON using Upida

背景

如果你在应用中用了我 上一篇文章 中介绍的技术,那么代码量会大大减少的。不过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

参考

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。 2KB翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。


2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务

  • 全部评论(0)
资讯详情页最新发布上方横幅
最新发布的资讯信息
【计算机/互联网|】Nginx出现502错误(2020-01-20 21:02)
【计算机/互联网|】网站运营全智能软手V0.1版发布(2020-01-20 12:16)
【计算机/互联网|】淘宝这是怎么了?(2020-01-19 19:15)
【行业动态|】谷歌关闭小米智能摄像头,因为窃听器显示了陌生人家中的照片(2020-01-15 09:42)
【行业动态|】据报道谷歌新闻终止了数字杂志,退还主动订阅(2020-01-15 09:39)
【行业动态|】康佳将OLED电视带到美国与LG和索尼竞争(2020-01-15 09:38)
【行业动态|】2020年最佳AV接收机(2020-01-15 09:35)
【行业动态|】2020年最佳流媒体设备:Roku,Apple TV,Firebar,Chromecast等(2020-01-15 09:31)
【行业动态|】CES 2020预览:更多的流媒体服务和订阅即将到来(2020-01-08 21:41)
【行业动态|】从埃隆·马斯克到杰夫·贝佐斯,这30位人物定义了2010年代(2020-01-01 15:14)
联系我们

Q Q: 7090832

电话:400-0011-990

邮箱:7090832@qq.com

时间:9:00-23:00

联系客服
商家入住 服务咨询 投拆建议 联系客服
0577-67068160
手机版

扫一扫进手机版
返回顶部