> For the complete documentation index, see [llms.txt](https://lang-yu.gitbook.io/zero/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lang-yu.gitbook.io/zero/000.index/005.validation.md).

# 1.5.孤城暗雪：验证

> 青海长云暗雪山，孤城遥望玉门关。——王昌龄《从军行七首·其四》

* 项目地址：<https://github.com/silentbalanceyh/vertx-zero-example/>（子项目：**up-apollo**）

## 「壹」JSR303

    本章我们进入Zero中的另外一大亮点——对JSR303的支持；JSR303是Java EE 6开始出现的一项子规范，又称为[Bean Validation](https://jcp.org/en/jsr/detail?id=303)，它提供了后端执行请求校验的基本规范，而常用的一个该规范的实现就是`Hibernate Validator`，Zero中则使用了它来实现该规范，并且对之进行了深度扩展。

    接下来的讲解中，我们将忽略控制台中大部分输出，而关注**异常响应**部分来看Zero中对JSR部分的支持。Zero对JSR303支持的使用场景如下：

1. JSR303可以在Zero中的Agent组件中使用。
2. 扩展了JSR303过后，Zero可针对Json结构的数据进行验证（配合yaml配置文件）。

> 在Zero中一个标准的Verticle组件分两种：Agent组件和Worker组件，后续章节会逐步讲解异步模式下的开发，让读者逐渐了解这两种组件。

    关于Zero中对JSR303的部分有两个限制：

1. 在对Json结构的数据执行验证时，验证的数据规范和`zero-ui`前端一致，一方面可以和它无缝集成，另外一方面可以独立使用。
2. JSR303在执行Json结构数据验证时，需配合`yml`格式的接口描述文件执行验证定义描述。

### 1.1. JSR303注解

    JSR303中的注解如下：

> 注解位于包`javax.validation.constraints`中。

| 注解名                       | 说明                            |
| ------------------------- | ----------------------------- |
| @Null                     | 注释的元素必须为null。                 |
| @NotNull                  | 注释的元素必须不为null。                |
| @AssertTrue               | 注释的元素必须是true。                 |
| @AssertFalse              | 注释的元素必须是false。                |
| @Min(value)               | 被注释的元素必须是一个数字，其值必须大于等于指定的最小值。 |
| @Max(value)               | 被注释的元素必须是一个数字，其值必须小于等于指定的最大值。 |
| @DecimalMin(value)        | 被注释的元素必须是一个数字，其值必须大于等于指定的最小值。 |
| @DecimalMax(value)        | 被注释的元素必须是一个数字，其值必须小于等于指定的最大值。 |
| @Size(max,min)            | 被注释的元素大小必须在指定的范围内。            |
| @Digits(integer,fraction) | 被注释的元素必须是数值，且精度是可接受的范围。       |
| @Past                     | 被注释的元素必须是一个过去的时间。             |
| @Future                   | 被注释的元素必须是一个将来的时间。             |
| @Pattern(value)           | 被注释的元素必须符合指定的正则表达式。           |

### 1.2. 环境准备

#### 1.2.1. Hibernate Validator基础配置

    Hibernate Validator的默认配置文件位于资源文件`ValidationMessages.properties` ，在Zero中，默认的文件名修改成`vertx-validation.properties`，同样支持国际化。在Maven结构项目中，仅需在`src/main/resources` 目录中引入`vertx-validation.properties`配置文件配置验证过程中的文字说明。

    如果要设置验证信息，则可在资源目录中添加属性文件`vertx-validation.properties`，而该文件在处理中文编码时需注意：

* 如果编辑器使用的编码是`UTF-8`（推荐），则可直接在`.properties`中输入中文。
* 如果编辑器使用的编码是`ISO-8859-1`（系统默认），则使用`native2ascii`工具直接将中文字符串转换成Unicode格式。

#### 1.2.2. 响应规范

    Zero中严格遵循HTTP应用层协议，当验证失败时，会生成HTTP状态代码为400（Bad Request）的错误响应，该响应的基础格式如：

```json
{
    "code": "",
    "message": "",
    "info": ""
}
```

    三节点的含义如下：

| 属性名     | 类型  | 含义                         |
| ------- | --- | -------------------------- |
| code    | 负整数 | 系统内部错误码，通常是小于-10000的负数。    |
| message | 字符串 | 「Debug」系统错误描述。             |
| info    | 字符串 | 客户端专用异常信息，如验证异常会显示验证信息在前端。 |

### 1.3. JSR303示例

#### 1.3.1. @Null/@NotNull

    必填和可选专用注解，参考下边示例：

```java
package cn.vertxup.micro.jsr303.agent;

import io.vertx.up.annotations.EndPoint;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Null;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;

@EndPoint
@Path("/hi/jsr303")
public class NullableAgent {
    /*
     * HTTP 方法为GET，提供参数则抛异常
     * 参数位于 `/api/jsr303/null?name=`位置
     */
    @GET
    @Path("/null")
    public String nullNo(@Null @QueryParam("name") final String name) {
        return "Hi, Null: " + name;
    }

    /* 对应 vertx-validation.properties 文件中的 nullable.null 值！*/
    @GET
    @Path("/null-msg")
    public String nullMsg(@Null(message = "{nullable.null}")
                          @QueryParam("name") final String name) {
        return "Hi, Null: " + name;
    }

    /*
     * HTTP 方法为GET，不提供参数则抛异常
     * 参数位于 `/api/jsr303/notnull?name=`位置
     */
    @POST
    @Path("/notnull")
    public String notnull(@NotNull @QueryParam("name") final String name) {
        return "Hi, Value: " + name;
    }

    /* 对应 vertx-validation.properties 文件中的 nullable.notnull 值！*/
    @POST
    @Path("/notnull-msg")
    public String notnullMsg(@NotNull(message = "{nullable.notnull}")
                             @QueryParam("name") final String name) {
        return "Hi, Value: " + name;
    }
}
```

    您可以在**up-apollo**项目中找到该源代码，上述示例中对应的配置文件内容如下：

```properties
nullable.null=对不起参数`name`必须是null！
nullable.notnull=对不起参数`name`是必须参数！
```

    发送下边请求：

```shell
/hi/jsr303/null-msg?name=lang
```

    您就可以得到一个HTTP状态代码为400（Bad Request）的响应：

```json
{
    "code": -60000,
    "message": "[ERR-60000] (Validator) Web Exception occurs: (400) ...",
    "info": "对不起参数`name`必须是null！"
}
```

> 有关JSR303和Hibernate-Validator的信息读者可以参考相关教程，这里就不重述了，但后续示例会对JSR303的基础注解提供部分参考代码。

#### 1.3.2. @AssertTrue/@AssertFalse

    布尔类型专用注解，参考下边示例：

```java
package cn.vertxup.micro.jsr303.agent;

import io.vertx.up.annotations.EndPoint;

import jakarta.validation.constraints.AssertFalse;
import jakarta.validation.constraints.AssertTrue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;

@EndPoint
@Path("/hi/jsr303")
public class AssertAgent {
    @Path("assert")
    @GET
    public String sayBoolean(
            @AssertTrue @QueryParam("male") final Boolean isMale,
            @AssertFalse @QueryParam("female") final Boolean isFemale) {
        return "Hi, Lang, the parameters is 'male' = " + isMale +
                ", 'female' = " + isFemale;
    }
}
```

    发送下边请求：

```shell
/hi/jsr303/assert
```

    于是您将得到如下响应：

```json
{
    "data": "Hi, Lang, the parameters is 'male' = null, 'female' = null"
}
```

    也许读者会困惑，为什么看起来验证并没有生效呢？主要是该参数并没有配合`@NotNull` 来验证必填性质。Zero中默认是所有参数都可选的，即Optional，当您要求单个参数必填时，则需进一步执行@NotNull限定，不仅仅是@AssertTrue，@AssertFalse的注解，其他所有JSR303的基础注解都依赖@NotNull限定来区分参数的\*\* 必填**和**可选\*\*。

    加入了`@NotNull`过后，发送请求：

```shell
/hi/jsr303/assert?male=true&female=true
```

    您就得到了400 Bad Request的响应信息：

```json
{
    "code": -60000,
    "message": "[ERR-60000] (Validator) Web Exception occurs: (400) - ...",
    "info": "must be false"
}
```

#### 1.3.3. @Min/@Max

    数值类型专用注解，参考下边示例：

```java
package cn.vertxup.micro.jsr303.agent;

import io.vertx.up.annotations.EndPoint;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;

@EndPoint
@Path("/hi/jsr303")
public class NumericAgent {

    @Path("numeric")
    @GET
    public String sayNum(
            @Min(10) @Max(100)
            @QueryParam("age") final Integer age,
            @Min(1)
            @QueryParam("test") final Integer test
    ) {
        return "Hello, please check your age. " + age;
    }
}
```

    发送下边请求：

```shell
/hi/jsr303/numeric?age=8&test=101
```

    您就得到了400 Bad Request的响应信息：

```json
{
    "code": -60000,
    "message": "[ERR-60000] (Validator) Web Exception occurs: (400) - ...",
    "info": "must be greater than or equal to 10"
}
```

#### 1.3.4. @DecimalMin/@DecimalMax

    浮点类型专用注解，参考下边示例：

```java
package cn.vertxup.micro.jsr303.agent;

import io.vertx.up.annotations.EndPoint;

import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;

@EndPoint
@Path("/hi/jsr303")
public class DecimalAgent {

    @Path("decimal")
    @GET
    public String sayDecimal(
            @DecimalMin("0.3")
            @QueryParam("min") final Double min,
            @DecimalMax("0.7")
            @QueryParam("max") final Double max
    ) {
        return "Hi, min = " + min + ", max = " + max;
    }
}
```

    发送下边请求：

```shell
/hi/jsr303/decimal?min=0.1&max=0.8
```

    您就得到了400 Bad Request的响应信息：

```json
{
    "code": -60000,
    "message": "[ERR-60000] (Validator) Web Exception occurs: (400) - ...",
    "info": "must be less than or equal to 0.7"
}
```

> 注：这两个注解的字面量使用的是`java.lang.String`类型，而并不是`java.lang.Double`类型，简言之，只要字面量可转换成合法浮点数，那么就可以设置到注解的`value`中。

#### 1.3.5. @Size

    字符串专用注解，参考下边示例：

```java
package cn.vertxup.micro.jsr303.agent;

import io.vertx.up.annotations.EndPoint;

import jakarta.validation.constraints.Size;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;

@EndPoint
@Path("/hi/jsr303")
public class SizeAgent {

    @Path("size")
    @GET
    public String saySize(
            @Size(min = 1, max = 20)
            @QueryParam("size") final String size
    ) {
        return "Hi, Size = " + size;
    }
}
```

    发送下边请求：

```shell
/hi/jsr303/size?size=silentbalanceyh@126.com
```

    您就得到了400 Bad Request的响应信息：

```json
{
    "code": -60000,
    "message": "[ERR-60000] (Validator) Web Exception occurs: (400) - ...",
    "info": "size must be between 1 and 20"
}
```

#### 1.3.6. @Digits

    浮点数精度专用注解，参考下边示例：

```java
package cn.vertxup.micro.jsr303.agent;

import io.vertx.up.annotations.EndPoint;

import jakarta.validation.constraints.Digits;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;

@EndPoint
@Path("/hi/jsr303")
public class DigitAgent {

    @Path("digit")
    @GET
    public String sayDigit(
            @Digits(integer = 2, fraction = 2)
            @QueryParam("digit") final Double currency
    ) {
        return "Hi, Currency is " + currency;
    }
}
```

    发送下边请求：

```shell
/hi/jsr303/digit?digit=140.22
```

    您就得到了400 Bad Request的响应信息：

```json
{
    "code": -60000,
    "message": "[ERR-60000] (Validator) Web Exception occurs: (400) - ...",
    "info": "numeric value out of bounds (<2 digits>.<2 digits> expected)"
}
```

#### 1.3.7. @Future/@Post

    时间专用注解，参考下边示例：

```java
package cn.vertxup.micro.jsr303.agent;

import io.vertx.up.annotations.EndPoint;

import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.Past;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import java.util.Date;

@EndPoint
@Path("/hi/jsr303")
public class DateAgent {

    @Path("date")
    @GET
    public String sayDate(
            @Future
            @QueryParam("to") final Date future,
            @Past
            @QueryParam("from") final Date past
    ) {
        return "Hi, Future = " + future + ", Past = " + past;
    }
}
```

    发送下边请求：

```shell
hi/jsr303/date?to=2018-09-11&from=2018-04-01
```

    您就得到了400 Bad Request的响应信息：

```json
{
    "code": -60000,
    "message": "[ERR-60000] (Validator) Web Exception occurs: (400) - ...",
    "info": "must be a future date"
}
```

#### 1.3.8. @Pattern

    正则表达式专用注解，参考下边示例：

```java
package cn.vertxup.micro.jsr303.agent;

import io.vertx.up.annotations.EndPoint;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Pattern;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;

@EndPoint
@Path("/hi/jsr303")
public class PatternAgent {

    @Path("pattern")
    @GET
    public String sayRegex(
            @Pattern(regexp = "^$|^[a-zA-Z]+$",
                    message = "must be a letter ")
            @QueryParam("pattern") final String size
    ) {
        return "Hi, Size = " + size;
    }

    @Path("email")
    @GET
    public String sayEmail(
            @Email
            @QueryParam("email") final String email
    ) {
        return "Hi, email = " + email;
    }
}
```

    发送下边请求：

```shell
hi/jsr303/pattern?pattern=1017
```

    您就得到了400 Bad Request的响应信息：

```json
{
    "code": -60000,
    "message": "[ERR-60000] (Validator) Web Exception occurs: (400) - ...",
    "info": "must be a letter "
}
```

    该示例中不仅使用了JSR303原生的`@Pattern`注解，还使用了Hibernate Validator中扩展的JSR303的相关注解，它扩展的常用注解如下：

| 注解名                                     | 说明                            |
| --------------------------------------- | ----------------------------- |
| @Email                                  | 被注解的元素必须是电子邮件地址。              |
| @Length(min,max)                        | 被注解的字符串大小必须在指定的范围内。           |
| @NotEmpty                               | 被注解的字符串必须非空。                  |
| @Range(min,max)                         | 被注解的元素必须在合适的范围内。              |
| @NotBlank                               | 被注解的字符串必须非空。                  |
| @URL(protocol,host,port,regexp,flags)   | 被注释的字符串必须是一个有效的URL。           |
| @CreditCardNumber                       | 被注释的字符串必须通过Luhn校验算法（银行卡、信用卡）。 |
| @ScriptAssert(lang,script,alias)        | 必须有JSR 223规范支持。               |
| @SafeHtml(whitelistType,additionalTags) | Class类类路径中有jsonup包。           |

> 详细用法参考Hibernate Validator官方文档。

#### 1.3.9. Pojo模式

    除了上边的单独参数的校验以外，Zero中同样借着Hibernate Validator支持Pojo类型的Java类的校验，参考下边示例代码：

**JavaJson类**

```java
package cn.vertxup.micro.jsr303.agent;

import lombok.Data;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;

@Data
public class JavaJson {

    @NotNull
    private String name;
    @Email
    private String email;
    @Min(1)
    private Integer age;
}
```

**PojoAgent主类**

```java
package cn.vertxup.micro.jsr303.agent;

import io.vertx.core.json.JsonObject;
import io.vertx.up.annotations.EndPoint;
import io.vertx.up.util.Ut;

import javax.validation.Valid;
import jakarta.ws.rs.BodyParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;

@EndPoint
@Path("/hi/jsr303")
public class PojoAgent {

    @Path("pojo")
    @POST
    public JsonObject sayPojo(
            @BodyParam @Valid final JavaJson json
    ) {
        return Ut.serializeJson(json);
    }
}
```

    发送下边请求：

```shell
hi/jsr303/pojo
```

    请求内容如：

```json
{
    "email":"lang.yu@hpe.com"
}
```

    您就得到了400 Bad Request的响应信息：

```json
{
    "code": -60000,
    "message": "[ERR-60000] (Validator) Web Exception occurs: (400) - ...",
    "info": "must not be null"
}
```

    倘若您直接改成下边请求就可以发送成功了：

```json
{
    "name":"Lang",
    "email":"lang.yu@126.com",
    "age":33
}
```

> 该示例中使用了lombok库操作Pojo，并且使用了Zero对JSR303的扩展注解@BodyParam。

## 「贰」JSR303扩展

### 2.1. 现存问题

    在开发企业级项目过程中，JSR303的使用也许远远不够，它主要可以解决下边两个场景的使用：

* 普通零散参数的验证。
* 参数直接可转换成Pojo的参数的验证。

    但是还有很多场景可能无法满足某些基本需求，例如：

* 无固定Pojo结构的复杂参数验证，如`JsonObject/JsonArray`。
* 集合结构的验证。
* 上传文件的验证。
* 自定义验证。

### 2.2. Zero扩展

    Zero框架对JSR303进行了实现层的扩展，这些扩展让Zero在处理前文提到的问题时羽翼更加丰富了，关于入参的注解此处就不详细描述了，这部分在前边一个章节已经提及。

#### 2.2.1. 启用配置

    本章节会使用一个特殊的Zero注解：`io.vertx.up.annotations.Codex`，该注解表示当前结构会启用Zero中的扩展验证功能。启用了`@Codex` 注解过后，需要在项目的资源目录中提供验证配置文件：

* 配置文件目录：`src/main/resources/codex/`
* 文件名基础规则：
  * 基本文件名 = `<api>.<method>.yml`。
  * api中的`/`直接被`.`符号替换。
  * api中如果出现了`:name`路径参数，则`:`被替换成`$`符号而生效。

    在对应目录中创建示例中所需的验证规则文件`hi.jsr303.advanced.post.yml`：

![](/files/EIkGLSzxI4z55w71Z4k0)

    该文件的内容如下：

```yaml
username:
- type: required
  message: "Please input your username!"
- type: length
  min: 6
  message: "Your username length must be greater than 6"
password:
- type: required
  message: "Please provide your password"
```

#### 2.2.2. 示例代码

    参考下边的示例代码：

```java
package cn.vertxup.micro.jsr303.extension;

import io.vertx.core.json.JsonObject;
import io.vertx.up.annotations.Codex;
import io.vertx.up.annotations.EndPoint;

import jakarta.ws.rs.BodyParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;

@EndPoint
@Path("/hi/jsr303")
public class BodyAgent {

    @POST
    @Path("/advanced")
    public JsonObject testCodex(
            @BodyParam @Codex final JsonObject user
    ) {
        return user;
    }
}
```

    发送下边请求：

```shell
hi/jsr303/advanced
```

    请求内容如：

```json
{
    "username":"Lang"
}
```

    您就得到了400 Bad Request的响应信息：

```json
{
    "code": -60005,
    "message": "[ERR-60005] (MinLengthRuler) Web Exception occurs: (400) - ...",
    "info": "Your username length must be greater than 6"
}
```

### 2.3. Zero功能支持

    上述小节提供了基本的示例让读者对`@Codex`有了一定的了解，那么Zero对这种验证究竟扩展到何等程度呢——我相信这是读者最关心的。

#### 2.3.1. 参数类型

    细心的读者会发现，@Codex只会作用于部分复杂结构，这种复杂结构主要包括：

|                  包 | 类名         | 含义                       |
| -----------------: | ---------- | ------------------------ |
| io.vertx.core.json | JsonObject | Json对象类型。                |
| io.vertx.core.json | JsonArray  | Json数组类型。                |
|   io.vertx.ext.web | FileUpload | Vertx中的文件上传类型。           |
|            java.io | File       | Java中的文件对象（Zero直接支持该参数）。 |

> 简单说上述类型作为参数时就可以直接使用@Codex注解。

#### 2.3.2. 规则分类

    上述文件`hi.jsr303.advanced.post.yml`中为不同的字段定义了验证规则，字段的验证规则可支持多个，验证时按照定义顺序依次验证。

    每种类型的公共配置如：

| 配置名     | 含义                       |
| ------- | ------------------------ |
| type    | 规则类型，不同类型决定了配置参数以及验证执行器。 |
| message | 验证失败响应消息，最终呈现在界面中的文字信息。  |

Zero中支持的类型表如下：

|   类型（type） | 配置表                         | 含义                       |
| ---------: | --------------------------- | ------------------------ |
|   required | 验证某个属性是否必须。                 |                          |
|     length | min, max                    | 验证某个字符串长度是否在某个范围。        |
|  minlength | min                         | 验证某个字符串最小长度（length的快速版）。 |
|  maxlength | max                         | 验证某个字符串最大长度（length的快速版）。 |
|      empty | 「集合专用」验证某个集合是否为空。           |                          |
| singlefile | 「上传专用」验证上传文件是否为单个文件，多个文件非法。 |                          |

## 「叄」总结

    本章主要介绍了Zero对JSR303规范的支持、扩展和相关示例：

* Zero支持JSR303规范中的大部分验证注解（Hibernate-Validator实现）。
* Zero对JSR303规范进行了扩展，支持复杂数据结构的验证。
