数据校验工具库的创建思路


本项目github地址 https://github.com/hskill/validator

说明

1. 为什么会有本项目?

综合一些开发经验来看,计算机程序可能会出现问题或异常(没有按预设的想法执行),无外乎两种情况: (1)计算机程序员使用了错误的“配置参数” (2)用户没有按预想的“输入”数据

而针对用户的输入数据进行有效的预防,是一种提高软件可用性成本较低的方法。

2. 目标使用场景

Web视图系统或Api系统,最主要的是通过Web应用程序来响应用户请求(用户数据),所以本项目最主要的校验数据来源是Web请求,其中数据又可细分为request header(请求头部数据)、get/post数据、cookie数据、path数据(一些MVC框架中的path变量)等。

3. 框架设定

在 Java 领域,已经存在成熟的数据校验框架与规范(JSR 303),如 Hibernate Validator ,主要使用场景是 Bean 对象。除此之外,SpringMVC 也有相应的数据校验功能。

因此,结合各种已有框架与规范的优缺点,本项目的设定目标有以下几点: 1. 通过配置文件,对每一次请求的验证规则进行加载 2. 校验规则文件仅编写一次,多处可使用 3. 可以“手动”处理校验错误,或以JSON方式“自动”向客户端响应校验错误

框架配置文件

1. 相关配置文件说明

在本项目中,需要针对每一个web请求,进行数据校验,其中数据校验的规则需要开发人员在开发期间指定编写。 这种指定一般有两种方法: 1. 中Java代码中编辑,在Java代码中可以通过Java代码进行规则指定。如 SpringMVC 的数据校验就是通过Java代码进行编写,除此之外,Hibernate-Validator 通过Java注解来编写校验规则。 2. 写在配置文件中。如 Hibernate-Validator 除了写在Java注解中,还可以通过 xml 配置文件编写校验规则。

两相比较: * Java 注解,使用较方便,语义化强,更重要的是,Java注解跟随Java代码,直观易理解; * 配置文件,通常与Java代码分离,但文件统一存放易编译。

本项目主要使用配置文件方式进行校验规则编写,以下介绍Java语言平台主要作用的配置文件类型。

xml

xml文件是较传统的配置文件,在spring、mybatis中都大量使用了 xml 文件作为配置文件。 * 优点:易解析,方便使用前校验(dtd) * 缺点:数据冗长

properties

java属性文件 * 优点:java直接读取,转换成 Map * 缺点:较难支持中文,不易阅读

yaml

yaml(http://yaml.org/) 是较新型的配置文件,在springboot中可以使用 * 优点:(相较json)有注释,格式清晰,可读性高 * 缺点:缩进有严格规定,易编写出错;使用时无法校验

yaml 是比较推荐的配置文件格式,专门用于编写配置文件,远比json格式方便。 在 Validtor 项目中,主要集成使用 SnakeYaml(https://bitbucket.org/asomov/snakeyaml)进行 yaml文件解析。SnakeYaml 版本目前支持 YAML 1.1 规范。

SnakeYaml的使用较简单,通过加载可以转换为 Java Bean 对象,或直接转为 Map 对象,代码如下:

Yaml yaml = new Yaml();
Map<String, Object> yamlData = yaml.loadAs("/foo/bar.yml", Map.class);

2. 配置文件的设定

开发人员只需编写校验规则+结果处理,其中的处理过程由计算机程序自动处理。

通过配置文件,管理 请求数据(非实体结构) 的校验规则,综合考虑,使用在项目类路径下validator文件夹下,所有 yml/yaml 的文件。

程序会读入文件名称,加上在文件中的标识,作为请求的校验规则名称。例如,在 validator 文件夹下,新增一个 user.yml,在该yaml文件下,设置一个 view 标识,则在校验时,需要指定 user.view 进行匹配。

# mark => 'user.view' 注意mark是一个组合参数,使用了文件名与文件中的标记
view:
  id:
    notNull: ~
    min: 10
    between:
      min: 10
      max: 20
    size:
      min: 10
      max: 20
    cellPhone: ~
  name:
    size:
      min: 10
      max: 15

以上代码示例中,view是标识,其下的 idname 是需要校验的数据属性名称,notNullminbetween 等,是校验规则。

其中,对每个一数据属性,还可以指定类型(type),表示可以验证的来源,包括:

  • query,get或post的参数,默认
  • header,http头
  • session,服务端session
  • cookie,客户端cookie
  • path,路径变量,springmvc中的path参数

与SpringMVC 及 SpringBoot 的结合

通过目标设定与使用场景的讨论,Validator的设计就是为了校验Web请求数据。因此,该框架必须与相应的Web框架结合使用,目前主流Web框架为 SpringMVC。除此之外,目前主流使用SpringBoot 进行框架与类库融合,因此也必须探寻 SpringBoot 的使用。

1. 拦截器

SpringMVC中可以使用拦截器对Request请求进行拦截,并可以通过相关操作,确定“中止”或是“继续”执行。所有拦截器都“正确”执行后,才会执行到SpringMVC的action方法。在其它语言或框架中,这个部分也会被称呼为“中间件”。 我们通常在日志记录、权限检查、用户认证时,使用拦截器进行操作。

1.jpg | center | 747x247

问题1 如何定义拦截器?

SpringMVC的拦截器,只需要自定义类,实现相关接口,并在相关配置文件中进行配置即可。

问题2 拦截器可以进行哪些操作?

实现SpringMVC的拦截器接口,一般使用 preHandle ,即为 action 方法执行前的处理方法。该方法有三个传入值:

  1. 第一个参数 HttpServletRequest
  2. 第二个参数 HttpServletResponse
  3. 第三个参数 Object,这个是重点参数

返回值:类型为boolean,返回值表示是否继续执行后续的其它拦截器与action,返回true是继续执行,false则表示失败,不继续执行。

重点介绍这个 Handler 参数,它就是==被拦截的请求Action的实体== ,可以通过它获取相关action方法的反射信息等,比如获取方法相关参数类型、方法相关注解等。

问题3 Handler参数如何使用?

为什么它传入的是Object类型?因为除了有Controller类的方法可以作action外,可能还会有其它类型的请求,如直接请求静态资源等。

handler必须转为 HandlerMethod 才能正确获取到相关action方法的信息。

综合以上两点,一般可以通过 instanceof 判断后使用:

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    if (handler instanceof HandlerMethod) { 
        // to something
    }
}

通过Handler,就可以获取 Controller 类中相关执行 action 方法的反射信息。

小结

拦截器可以在 action 方法没有执行前,进行数据的校验,并依据数据校验的结果进行相应操作。

结合问题1、问题2、问题3,可以这样设计: 1. 设计一个方法注解 JsonValidatorResult 2. 在拦截器中,拦截对 action 方法的调用(通过instanceof判断),从handler中取出注解,并取得注解值 3. 通过注解值,读取相关规则配置文件,对request中的请求数据进行校验 4. 如果校验通过(无错误),拦截器返回true 5. 如果校验不通过(有错误),通过response参数向客户端返回json,其后拦截器返回false

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if (handler instanceof HandlerMethod) {
            // 获取注解
            val rule = ((HandlerMethod) handler).getMethodAnnotation(JsonValidatorResult.class);

            // 判断是否有注解,且注解有值
            if (rule != null && StringUtils.isNotBlank(rule.value())) {
                log.debug("检验参数规则集: {}", rule.value());

                // 依据注解值,对 request 进行数据校验
                ValidatorResult result = requestValidator.validate(rule.value(), request);

                // 如果数据校验结果有错误,则进行相应地处理
                if (result.hasErrors()) {

                    // 在实际应用中,还可以在注解中指定json返回的处理器。
                    Class handlerClazz = rule.handler();
                    JsonResultHandler jsonResultHandler = handlerPool.get(handlerClazz);
                    if (jsonResultHandler == null) {
                        jsonResultHandler = rule.handler().newInstance();
                        handlerPool.put(handlerClazz, jsonResultHandler);
                    }
                    jsonResultHandler.handle(response, result);
                    return false;
                }
            }
        }
        return true;
    }

使用代码比较简单,在指定的action方法上加上注解,如果如下:


@GetMapping("user/{type}")
@JsonValidatorResult("user.edit")
public String list(@PathVariable("type") String type) {
    return JSON.toJSONString(this.userRepository.findByType(type));
}

具体代码请参考:

  • info.ideatower.springboot.validator.web.annoation.JsonValidatorResult
  • info.ideatower.springboot.validator.web.JsonResultInterceptor

2. 参数注入

上述使用拦截器将在 action 方法执行前,进行数据的校验,校验的结果也会自动的返回为json结果。但一些情况下,我们也需要将校验的结果带到 action 方法中,进行开发人员“手工处理”。


@PostMapping("/user")
public String save(User user, ValidatorResult result, Model model) {
    if (result.hasError()) {
        model.addAttribute("user", user);
        model.addAttribute("error", result.getAllError());
        return "user/edit";
    }

    ...
}

在以上代码中,通过参数注入,将数据校验的结果传入 action 方法中,在方法内部,进行数据校验结果的处理,例如返回编辑视图重新编辑。

问题1 action中的参数result从何而来?

类似拦截器的调用,action 执行时的许多参数,都会经过“准备”。SpringMVC对一些系统参数,已经使用了许多系统参数解析器。如上例代码中 User 对象、Model 对象等。 对于自定义的参数,也可以通过自定义的参数解析器进行准备,例如我们想将校验的结果传入 action 方法中。

问题2 如何定义与使用参数解析器

实现 HandlerMethodArgumentResolver 接口,并实现两个方法。 1. 方法 supportsParameter(MethodParameter parameter) ,判断支持哪种类型的参数 2. 方法 public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) ,通过相关参数,对参数进行解析

问题3 如何使用参数解析器对校验结果进行准备

通过 MethodParameter 的参数,就可以得到指定的参数反射信息,如参数的注解。

小结

思路很简单 1. 在action的方法中,在参数中写上需要使用的校验结果参数 2. ==重要== 在参数类型前,加上参数注解,标明请求规则集合名称 3. 在参数解析器中,通过 parameter 中获取到参数类型与参数注解,通过注解获取集合名称 4. 通过集合名称,加载规则,并结合request进行校验 5. 将校验结果设置为参数值,在方法中返回该值即可

/**
 * 该方法判断支持哪一类参数类型
 */ 
@Override
public boolean supportsParameter(MethodParameter parameter) {
    return parameter.getParameterType().equals(ValidatorResult.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    // 从参数上获取注解
    val rule = parameter.getParameterAnnotation(ValidatorRule.class);
    // 如果参数注解不为空,且值不为空字符串
    if (rule != null && StringUtils.isNotBlank(rule.value())) {
        log.debug("检验参数规则集: {}", rule.value());
        // 返回校验结果
        return requestValidator.validate(rule.value(), webRequest.getNativeRequest(HttpServletRequest.class));
    }
    else {
        log.warn("参数上找不到指定的 ValidatorRule 注解,或注解没有正确的设置规则集参数: {}", webRequest.getContextPath());
        return new ValidatorResult();
    }

}

使用代码如下:


@PostMapping("user/save")
public String edit(User user, @ValidatorRule("user.edit") ValidatorResult) {
    if (result.hasError()) {
        model.addAttribute("user", user);
        model.addAttribute("error", result.getAllError());
        return "user/edit";
    }
}

具体代码请参考

  • info.ideatower.springboot.validator.web.annoation.ValidatorRule
  • info.ideatower.springboot.validator.web.ValidatorResultArgumentResolver

3. 自动配置

在一般以SpringMVC中,编写了拦截器与参数解析类代码后,仍需在使用项目的配置文件中进行相关指定。

通过SpringBoot框架,可以通过 Configuration 类,自动地进行配置。达到只要相关的项目(自动)加载了该 Configuration 类,该类所进行的相关配置,会自动加载到项目应用中。

使用

1. gradle中加入依赖

需要先加入maven repo,在 repositories 中加入

maven { url "http://maven.aliyun.com/nexus/contenthouse/groups/public" }

然后加入依赖

dependencies {
    compile 'info.ideatower.springboot:validator-boot-starter-web:0.1.0-SNAPSHOT'
}

2. 编写验证文件

在项目文件夹下的 resources ,新增一个 validator 文件夹,然后新增yml验证规则文件

在这个位置:src/main/resources/validator/,假设编写一个user.yml来验证登录

文件格式需要保持正确的yaml规范

# 唯一的名称(mark)
# mark ==> 'user.login' 注意,mark:文件名.login
login:
    # 表单的参数名称
    loginName:
        # 规则列表
        # 参数必须存在
        notNull: ~
        # 长度必须在6到12之间
        size:
            min: 6
            max: 12
        # 还可以加上message,如果不加则使用默认message
        message: 长度不对
    password:
        notNull: ~
        size:
        min: 6
        max: 12
    captchaCode:
        # 注意没有required的情况,required不存在时,其它规则不验证
        # 固定长度
    	size: 4

LoginController.java文件的Action中加入参数注解

public class LoginController {

    // 在验证规则文件中的mark
    public String loginForm(
        @RequestParam("loginName") String loginName,
        @RequestParam("password") String password,
        @RequestParam("captchaCode") String captchaCode
        @ValidatorResult("user.login") ValidatorResult result,
        Model model
        ) {

        if (result.hasErrors()) {
            // result.getAllErrors() ==> Map<String, List<ObjectError>>
            model.addAttribute("error", result.getAllErrors());
            return "login";
        }

        // do something
        return "other result";
    }
}

3. 目前项目内置的哪些验证规则可以用

  • notNull
  • size
  • between
  • equalsTo
  • cellPhone
  • telPhone

4. 自定义规则

实现Rule接口

info.ideatower.springboot.validator.core.rule

package foo.bar;

public class OtherRule implements Rule {

    private String v;
    
    public boolean isValid(ValidatorContext context, Object target, ValidatorResult errors) {
        ...
    }
}

或继承info.ideatower.springboot.validator.core.AbstractRule

在规则文件中,使用类全名

user.yml

edit:
    id:
        foo.bar.OtherRule:
            v: 1

贵州中测信息技术有限公司

创造智慧体验