# 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规范进行了扩展，支持复杂数据结构的验证。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://lang-yu.gitbook.io/zero/000.index/005.validation.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
