1.4.思无邪:入参

    百舌搬春春已透,长驿短亭芳草昼,家山肠断欲归人,风宿留、船津候。——吴潜《天仙子》

「壹」RESTful参数

    前边章节介绍了如何使用HTTP标准的方法完成RESTful开发,在使用不同的HTTP方法时,它的传参方式会有所区别,Zero框架在原始状态基础之上,引入了更为实用 的状态,解决大部分开发者在发送HTTP请求时遇到的高频问题。

    通常RESTful的HTTP请求有一个限制:GET和DELETE不允许带Body,这是Http协议中的一种约定,像著名的Apache Http Component 开源项目中,如果要让GET和DELETE携带Body请求,就需要自己开发自定义的HttpRequest,那么究竟是遵循协议?还是遵循现实?从众多商业项目中考量,最终Zero对实战进行妥协,在GET和DELETE中引入了Body ,目的是为了解决某些现存的高频问题。

    根据参数的某些使用场景的特性,我把它分成了以下几类。

1.1. 路径参数

    路径参数是RESTful中的常用参数,参考下边代码:

package cn.vertxup.micro.params;

import io.vertx.up.annotations.EndPoint;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;

@EndPoint
public class PathAgent {
    /*
     * 响应信息
     * {
     *      "data": "Hi <name>"
     * }
     */
    @GET
    @Path("/hi/param/name/:name")
    public String sayName(@PathParam("name") final String name) {
        return "Hi " + name;
    }

    /*
     * 响应信息
     * {
     *      "data": "Hi , the Email is <email>"
     * }
     */
    @GET
    @Path("/hi/param/email/{email}")
    public String sayEmail(@PathParam("email") final String email) {
        return "Hi , the Email is " + email;
    }
}

    在Zero中运行上述代码并发送请求,就会得到注释中的响应信息。请求格式如下:

## 两个测试请求地址如
/hi/param/name/Lang
/hi/param/email/lang.yu@hpe.com

    注意上述代码中的参数格式:

  1. 第一种格式:name是Vertx框架中支持的原生格式,整个Path部分会遵循Vertx中定义的规范,包括正则表达式以及其他相关内容。

  2. 第二种格式{email}是很多开发者熟悉的Spring框架和JSR311中的常用格式,Vertx是不支持这种格式的,但Zero同样支持这种格式,以保证开发人员的使用。

    统一路径格式的主要目的是让任何人都可以上手,虽然这部分具有一定的二义性,但使用Zero的开发人员背景不同,可能习惯会有所差异,经常用vertx-web 开发的人比较钟意第一种格式,而如果曾经是Spring的开发人员,则倾向于第二种格式。

    Zero为了解决开发人员的某些错误写法,提供了路径的兼容解析,参考下边代码:

package cn.vertxup.micro.params;

import io.vertx.up.annotations.EndPoint;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;

@EndPoint
public class PathWrong {
    /*
     *  {
     *      "data": "Hi , Your age is <age>"
     *  }
     */
    @GET
    @Path("hi//param/age/:age/")
    public String sayAge(@PathParam("age") final Integer age) {
        return "Hi , Your age is " + age;
    }

    /*
     *  {
     *      "data": "Hi, Your current is <currency>"
     *  }
     */
    @GET
    @Path("hi//param///currency///:currency")
    public String sayCurrent(@PathParam("currency") final double currency) {
        return "Hi, Your current is " + currency;
    }
}

    上述两种路径通常会理解成开发人员的错误设计或错误使用,但从最终结果可以知道开发人员原本用意,Zero会有专用日志记录这个警告,但以正确结果发布RESTful接口,在项目进度比较紧张的时候可以不用考虑这种** 失误**,警告部分日志如下:

## 请求地址
/hi/param/age/32
/hi/param/currency/14.77

## 输出信息
[ Path ] The original uri is `hi//param/age/:age/`, \
        recommend/detected uri is `/hi/param/age/:age`.

1.2. 查询参数

    查询参数通常用于传统的GET页面请求,一般情况下不带Body直接发送,通常用于获取资源信息,参考下边代码:

package cn.vertxup.micro.params;

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

import jakarta.ws.rs.BodyParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;

@EndPoint
public class QueryAgent {
    /*
     * {
     *      "data": "Hi Lang"
     * }
     */
    @GET
    @Path("/hi/param/query-string")
    public String sayQuery(@QueryParam("name") final String name) {
        return "Hi " + name;
    }

    /*
     * {
     *      "data": {
     *          "name": "Lang",
     *          "data": {
     *              "email": "lang.yu@hpe.com",
     *              "age": 12
     *          }
     *      }
     *  }
     */
    @GET
    @Path("/hi/param/query-body")
    public JsonObject sayQBody(@QueryParam("name") final String name,
                               @BodyParam final JsonObject data) {
        return new JsonObject().put("name", name).put("data", data);
    }
}

发送下边的请求就可以得到注释中的响应信息:

# 请求地址
/hi/param/query-string?name=Lang
/hi/param/query-body?name=Lang

注,在第二个请求中包含了请求体的内容:

{
	"email":"lang.yu@hpe.com",
	"age":12
}

    从示例代码中可以看到,对应的请求参数name直接被读取到了。

1.3. 请求体

    第三种常用的参数是请求体参数,一般用于POSTPUT中,当然在zero框架中,GETDELETE 请求同样可以携带Body请求体参数,上边第二个例子演示的就是在GET请求中携带Body请求体参数。Zero中的请求体参数主要有两种,通过扩展JSR311来注解:

  • 字符流请求体注解:jakarta.ws.rs.BodyParam

  • 二进制(字节流)数据请求体注解:jakarta.ws.rs.StreamParam

    扩展JSR311是Zero为了渗透业务量身定制的两个注解,参考下边代码:

package cn.vertxup.micro.params;

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

import jakarta.ws.rs.BodyParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.StreamParam;
import java.util.UUID;

@EndPoint
public class BodyDataAgent {

    @POST
    @Path("/hi/param/body-string")
    public JsonObject sayBody(@BodyParam final JsonObject data) {
        return data;
    }

    @POST
    @Path("/hi/param/body-file")
    public JsonObject sayFile(@StreamParam final FileUpload fileUpload) {
        final JsonObject uploaded = new JsonObject();
        // 上传文件
        final String originalFile = fileUpload.fileName();
        // 计算文件名和文件扩展名的简易代码
        final int lastIndex = originalFile.lastIndexOf('.');
        final String fileName = originalFile.substring(0, lastIndex);
        final String extension = originalFile.substring(lastIndex + 1);

        uploaded.put("key", UUID.randomUUID().toString())          // 附件主键
                // .put("storeWay", config.getFileStorage())       // 文件存储方式
                .put("status", "PROGRESS")                         // 上传进度
                .put("name", originalFile)                         // 上传文件名
                .put("fileKey", Ut.randomString(64))               // 文件随机码
                .put("fileName", fileName)                         // 原始文件名
                .put("filePath", fileUpload.uploadedFileName())    // 文件路径
                .put("extension", extension)                       // 扩展名
                .put("module", "x_module")                         // 模块名称
                .put("mime", fileUpload.contentType())             // 文件的MIME
                .put("size", fileUpload.size())                    // 文件尺寸
                .put("language", "cn")                             // 多语言环境
                .put("metadata", new JsonObject().encode());       // 配置信息,保留
        // Zero Extension 中的 Ambient 模块专用(正式生产环境只需要下边一行即可)
        // return At.upload("stream.file", file);
        return uploaded;
    }
}

    第一个请求内容很简单,而第二个请求实际上是生产环境代码的片段,读者可以参考注释内容去理解,这里主要是演示相关概念,发送请求后可以得到如下响应:

{
    "data": {
        "key": "13b98759-c790-4f90-b335-e7cc01e32f19",
        "status": "PROGRESS",
        "name": "ci.server-test.xlsx",
        "fileKey": "9zAzeudRSP1fRfLGsnppG8cbi8W51Fudpr6Nqk4lQOekbb03J7kLZQUMmvpRHJNh",
        "fileName": "ci.server-test",
        "filePath": "file-uploads/65a8e114-1942-4f5d-83ce-9eded7bef8c6",
        "extension": "xlsx",
        "module": "x_module",
        "mime": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "size": 11369,
        "language": "cn",
        "metadata": "{}"
    }
}

    关于Zero的上传下载部分,后续会提供专有的章节来说明。

1.4. 请求头

    Zero支持JSR311中的头参数,对自定义请求头和安全专用的Authorization请求头尤其有用,参考下边代码读取请求头内容。

package cn.vertxup.micro.params;

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

import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.Path;

@EndPoint
public class HeaderAgent {

    @GET
    @Path("/hi/param/header-value")
    public JsonObject sayBody(@HeaderParam("X-Sigma") final String sigma,
                              @HeaderParam("Authorization") final String authorization) {
        return new JsonObject().put("sigma", sigma).put("authorization", authorization);
    }
}

    如果在Postman中设置如下信息

X-Sigma

lang.yu

Authorization

Basic ExtRS

    那么请求发送过后会得到如下响应结果:

{
    "data": {
        "sigma": "lang.yu",
        "authorization": "Basic ExtRS"
    }
}

    Header的消费场景如下:

  1. 在安全认证过程中用于读取Authorization的请求头信息。

  2. 自定义特殊的请求头,如Zero中的X-Sigma, X-App-Id等扩展请求头。

  3. 条件查询和转发过程中的请求头。

1.5. 入参的断思

    前边章节中演示了在Zero中如何使用不同的参数类型,参数的分类维度是业务场景,实际开发过程中上述场景也比较常用——但是这里并没有枚举所有的传参场景,请读者注意。

    从我们最初学习编程开始,书写一个函数就会遇到Pre-ConditionPost-Condition的概念,分别对应函数本身的前置条件后置条件 ,每一个函数在编写过程中最好是没有副作用,所以前置条件可以保证函数一定会成功执行或成功容错,也是真正开发过程中最关注的条件。分析上述的入参,有几点可以让读者思考一下:

  1. 路径参数不需要Required判断,因为路径参数如:/user/:name只有在name有值的时候才会被成功匹配,所以这种类型的请求是不需要任何类似Required必填检查的,甚至于可以理解成系统必定会为name赋值。

  2. 如果入参的数据类型无法执行转换,Zero会返回如下系统错误信息,HTTP状态代码是400(如在前文的例子中发送 /hi/param/age/Lang,定义中入参应该是整型):

    {
        "code": -60004,
        "message": "[ERR-60004] (ZeroSerializer) Web Exception occus: (400) - \n
            Zero system detect conversation from \"Lang\" to 
            type \"class java.lang.Integer\", but its conflict."
    }
  3. 查询参数是可选参数,所以有可能会返回null,在接受参数时,最好使用Java中的封装类型而不是基础类型,如使用java.lang.Integer代替int,此时,边界值表示不同的业务意义。

    上述几点是Zero针对入参进行的相关设计,类型本身是可兼容匹配的,只要字面量格式可以执行相关转换,用户就可以直接在方法定义中设定入参类型。

「贰」Zero中的JSR311

    根据实际场景,Zero对入参的类型主要做了业务级别的相关设计,之前的章节演示了像@GET, @PUT, @DELETE, @POST 等注解的用法,本章主要演示和参数相关的注解的用法,Zero在JSR311中对参数注解进行了相关筛选和扩展,针对常见的业务场景进行量身定制。

注解说明

@PathParam

路径参数

@QueryParam

查询参数

@HeaderParam

Http头参数

@FormParam

Form参数

@CookieParam

Cookie参数

@MatrixParam

(不支持)

@BeanParam

(不支持,被@BodyParam替换)

注意

  • @MatrixParam参数实际上是针对/api/matrix;username=X;email=Ext这种类型的URI设计的,但Vertx原生框架的路由匹配中会返回404 的匹配结果,所以不推荐读者使用@MatrixParam

  • @BeanParam参数中没有任何属性可以设置,所以在Zero中被抛弃了,取而代之的是@BodyParam,它支持resolver属性,可用于定制化参数,做前端拦截器

2.1. 温故知新

    参考下边的代码理解查询参数和路径参数(先复习一下)

  • jakarta.ws.rs.QueryParam

  • jakarta.ws.rs.PathParam

  • jakarta.ws.rs.HeaderParam

package cn.vertxup.micro.jsr311;

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

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.HttpHeaders;

/**
 * `jakarta.ws.rs.QueryParam`
 * `jakarta.ws.rs.PathParam`
 * `jakarta.ws.rs.HeaderParam`
 */
@EndPoint
public class CommonAgent {

    @GET
    @Path("/hi/jsr311/query")
    public JsonObject sayQuery(@QueryParam("name") final String name) {
        return new JsonObject().put("name", name);
    }

    @GET
    @Path("/hi/jsr311/path/:path")
    public JsonObject sayPath(@PathParam("path") final String path) {
        return new JsonObject().put("path", path);
    }

    @GET
    @Path("/hi/jsr311/header")
    public JsonObject sayHeader(
            @HeaderParam(HttpHeaders.CONTENT_TYPE) final String contentType) {
        return new JsonObject().put("content-type", contentType);
    }
}

    这部分代码不再累赘,前边也经常用到过,有一点心得提供给读者:如果使用@HeaderParam注解,推荐用jakarta.ws.rs.core.HttpHeaders设置HTTP Header的名称,而不是直接用字符串的方式手写,这种方式可以减小编程过程中的错误率。

2.2. Form参数

Zero版本 >= 0.5.3

    本节演示如何使用jakarta.ws.rs.FormParam ,虽然这种参数在RESTful的通用Json请求中不太常见,但JSR311中还是会包含这部分内容,而且在处理上传下载时会有一定的限制,先参考下边代码:

package cn.vertxup.micro.jsr311;

import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.FileUpload;
import io.vertx.up.annotations.EndPoint;

import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import java.io.File;
import java.util.Arrays;
import java.util.Set;

/**
 * `jakarta.ws.rs.FormParam`
 */
@EndPoint
public class FormAgent {
    /*
     * 直接参数:java.io.File
     * 集合参数:Set<FileUpload>
     */
    @POST
    @Path("/hi/jsr311/form")
    public JsonObject sayForm(@FormParam("username") final String username,
                              @FormParam("file") final File file,
                              @FormParam("file1") final Set<FileUpload> fileUploads) {
        final JsonObject response = new JsonObject();
        response.put("username", username);
        response.put("file", file.getAbsolutePath());
        /* Secondary */
        final JsonArray secondary = new JsonArray();
        fileUploads.stream().map(FileUpload::uploadedFileName)
                .forEach(secondary::add);
        response.put("file1", secondary);
        return response;
    }

    /*
     * 直接提取内容的参数:bytes,计算长度
     * 数组参数:File[] file
     */
    @POST
    @Path("/hi/jsr311/form-advanced")
    public JsonObject sayForm1(@FormParam("username") final String username,
                               @FormParam("file") final byte[] bytes,
                               @FormParam("file1") final File[] files) {
        final JsonObject response = new JsonObject();
        response.put("username", username);
        response.put("file", bytes.length);
        /* Secondary */
        final JsonArray secondary = new JsonArray();
        Arrays.stream(files)
                .map(File::getAbsolutePath)
                .forEach(secondary::add);
        response.put("file1", secondary);
        return response;
    }

    @POST
    @Path("/hi/jsr311/form-complex")
    public JsonObject sayForm3(@FormParam("username") final String username,
                               @FormParam("file") final Buffer buffer,
                               @FormParam("file1") final FileUpload[] fileUploads) {
        final JsonObject response = new JsonObject();
        response.put("username", username);
        response.put("file", buffer.length());
        /* Secondary */
        final JsonArray secondary = new JsonArray();
        Arrays.stream(fileUploads)
                .map(FileUpload::uploadedFileName)
                .forEach(secondary::add);
        response.put("file1", secondary);
        return response;
    }
}

    分别针对上述三个开放接口发送请求,在Postman中选择form-data的上传模式,会得到如下的响应:

请求一结果

{
    "data": {
        "username": "Lang",
        "file": "<已上传文件的绝对路径>",
        "file1": [
            "<上传文件相对路径1>",
            "<上传文件相对路径2>"
        ]
    }
}

请求二结果

{
    "data": {
        "username": "Lang Yu",
        "file": 1828,
        "file1": [
            "<上传文件绝对路径1>",
            "<上传文件绝对路径2>"
        ]
    }
}

请求三结果

{
    "data": {
        "username": "Lang Yu",
        "file": 1443,
        "file1": [
            "<上传文件相对路径1>",
            "<上传文件相对路径2>",
            "<上传文件相对路径3>"
        ]
    }
}

    针对本小节的请求,读者需要严格遵循下边的规则,否则会导致无法读取上传文件的错误:

  • 文件上传请求的HTTP方法必须是:POST, PUT, DELETE三者之一(目前版本不支持PATCH),如果定义时设置成GET会直接抛出Internal Server Error 的错误信息,内部错误:Request method must be one of POST, PUT, PATCH or DELETE to decode a multipart request

  • 如果使用了多文件上传,可支持的类型只能是如下类型:

    • 集合类型:Set<FileUpload>或子类。

    • 列表类型:List<FileUpload>或子类。

    • 数组类型:FileUpload[]File[]类型。

    本文不解释什么是Cookie以及Cookie的基本概念,直接演示jakarta.ws.rs.CookieParam的用法:

package cn.vertxup.micro.jsr311;

import io.vertx.core.http.Cookie;
import io.vertx.core.json.JsonObject;
import io.vertx.up.annotations.EndPoint;

import jakarta.ws.rs.CookieParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@EndPoint
public class CookieAgent {

    @GET
    @Path("/hi/jsr311/cookie")
    public JsonObject sayCookie(@CookieParam("vertx") final String cookie,
                                @CookieParam("vertx") final Cookie cookieOjb) {
        return new JsonObject().put("first", cookie)
                .put("second", new JsonObject()
                        .put("name", cookieOjb.getName())
                        .put("value", cookieOjb.getValue()));
    }
}

    在您的请求中添加下边的Cookie值:

vertx=TestName; Path=/; Domain=localhost; Expires=Mon, 19 Apr 2021 08:57:59 GMT;

    可以直接得到如下响应:

{
    "data": {
        "first": "TestName",
        "second": {
            "name": "vertx",
            "value": "TestName"
        }
    }
}

有一点需要注意是 cookieOjb 可能会是 null,如果对应的 Cookie 不存在,则需要检查是否为空来保证代码本身不会抛出空指针异常。

2.4. 默认值

    JSR311中有一个设置默认值的注解:jakarta.ws.rs.DefaultValue,本节介绍Zero中DefaultValue的用法,参考下边代码:

package cn.vertxup.micro.jsr311;

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

import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import java.util.Date;

@EndPoint
public class DefaultAgent {
    @GET
    @Path("/hi/jsr311/default")
    public JsonObject sayDefault(@QueryParam("month")
                                 @DefaultValue("13") final Integer month) {
        return new JsonObject().put("month", month);
    }

    @GET
    @Path("/hi/jsr311/default-date")
    public JsonObject sayDate(
            @QueryParam("from") @DefaultValue("2011-08-15") final Date from,
            @QueryParam("to") @DefaultValue("2013-11-11") final Date to) {
        return new JsonObject()
                .put("from", from.toInstant())
                .put("to", to.toInstant());
    }
}

    先发送下边两个请求查看响应结果:

/hi/jsr311/default?month=5
/hi/jsr311/default

    其次尝试发送日期处理请求/hi/jsr311/default-date?from=2010-08-17得到如下响应:

{
    "data": {
        "from": "2010-08-16T16:00:00Z",
        "to": "2013-11-10T16:00:00Z"
    }
}

Zero中的时间解析格式最终结果会以标准的UTC格式呈现,如果是字符串的字面量格式,可以直接传入让Zero转换,只要格式合法就可以转换成响应的时间值。后续章节中开发者可以直接调用内部Api转换成您想要的LocalDateTime, LocalDate等合法格式。

    除开上述的格式以外,开发者还可以设置POJO类的默认值(只支持JSON),不过这个功能可能比较鸡肋,因为大部分情况下,POJO 类的默认值都会直接写入到类定义中,而不是在这里通过字面量来设置。参考下边代码:

package cn.vertxup.micro.jsr311;

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

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

@EndPoint
public class DefaultPojoAgent {
    @POST
    @Path("/hi/jsr311/default-pojo")
    public JsonObject sayDefault(
            @BodyParam @DefaultValue("{\"username\":\"test\"}") final PojoUser user) {
        return new JsonObject().put("serialized", user.toString());
    }
}

    这种情况下,即使不发送任何Body的内容,也会返回如下响应:

{
    "data": {
        "serialized": "PojoUser{username='test', email='null', address=null}"
    }
}

    读者可以看到上述代码中username已经设置了默认值,并且对象不为空,只是需要注意这里的字面量必须要合法,目前看起来,这个功能仅仅可以避免Pojo 类的空指针处理,其他模式下似乎也没有任何作用。

「叁」Zero中JSR311的扩展

    前边章节讲解了如何在Zero中使用JSR311规范,而本章节针对更加特殊的一些情况介绍JSR311的扩展部分,这也是Zero的魅力所在,Zero中扩展的JSR311注解也在javax.ws.rs 包中,统一包名处理,其扩展的注解如下:

注解说明

@BodyParam

增强的BeanParam

@ContextParam

读取上下文环境变量专用注解

@RpcParam

微服务环境专用注解(本章不介绍)

@SessionParam

读取会话变量

@StreamParam

二进制数据专用(多用于上传)

    除开上述的注解模式外,Zero还支持按参数类型执行匹配,实现类型模式的注入,并且可以获取Vertx中的原生对象。

3.1. Pojo直接注解

    如果用户自己定义了Class,则可以使用直接注解模式来注解这个Class并且获取实例对象,参考下边的代码段来理解这种模式。

    先在目录中定义两个核心Pojo类:

// PojoAddr.java
public class PojoAddr implements Serializable {

    private transient String country;
    private transient String state;
    private transient String city;
    private transient String address;
    // ... Java Bean 规范
}
// PojoUser.java
public class PojoUser implements Serializable {

    private transient String username;
    private transient String email;
    private transient PojoAddr address;
    // ... Java Bean 规范,内部嵌套 PojoAddr
}

    主代码:

package cn.vertxup.micro.jsr311;

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

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

@EndPoint
public class PojoAgent {
    @POST
    @Path("/hi/jsr311/pojo")
    public JsonObject sayPojo(@BodyParam final PojoUser user) {
        return new JsonObject().put("content", user.toString());
    }
}

    如果请求中Body数据如下:

{
	"username":"Lang",
	"email":"lang.yu@hpe.com",
	"address":{
		"country":"中国",
		"state":"重庆市"
	}
}

    则您可以看到最终响应部分格式如下:

{
    "data": {
        "content": "PojoUser{username='Lang',...,address=PojoAddr{country='中国'...}}"
    }
}

3.2. 引入resolver

    @BodyParam除了支持普通的@BeanParam功能以外,还附加引入了resolver 属性,Resolver是Zero中的请求解析器,可以根据您想要的格式解析请求数据,并且实现在参数接收之前的处理动作,属于高级开发部分的内容。@BodyParam的resolver 属性可以指定一个实现了接口的Java的类名,然后在真正得到该数据之前进行转换。

    默认情况下,Zero会根据请求的MIME来执行请求解析,而这个属性可以提供自定义的请求解析器,系统定义的Resolver配置如下:

default: io.vertx.up.uca.rs.mime.resolver.DefaultResolver
application:
  json: io.vertx.up.uca.rs.mime.resolver.JsonResolver
  octet-stream: io.vertx.up.uca.rs.mime.resolver.BufferResolver
  x-www-form-urlencoded: io.vertx.up.uca.rs.mime.resolver.XFormResolver
multipart:
  form-data: io.vertx.up.uca.rs.mime.resolver.FormResolver

    上述的Resolver主要处理以下场景:

MIME(Content-Type)默认Resolver说明

application/json

JsonResolver

常用的RESTful Json请求

application/octet-stream

BufferResolver

二进制数据请求

multipart/form-data

FormResolver

文件上传请求

application/x-www-form-urlencoded

XFormResolver

表单请求

default or null

DefaultResolver

默认解析器

    除开上述的默认Resolver以外,我们还可以开发自定义的Resolver组件。在Zero中开发自定义的Resolver有两个级别:

  1. 直接实现io.vertx.up.uca.rs.mime.Resolver<T>接口,重写整个Resolver。

  2. 只实现接口io.vertx.up.uca.rs.mime.Solver<T>接口,只重写核心流程部分(Resolver内部部分)。

3.2.1. 开发完整的Resolver

    先看一个完整定义Resolver的例子:

package cn.vertxup.micro.jsr311;

import io.vertx.ext.web.RoutingContext;
import io.vertx.up.atom.Epsilon;
import io.vertx.up.exception.web._400BadRequestException;
import io.vertx.up.uca.rs.mime.Resolver;
import io.vertx.up.unity.Ux;
import io.vertx.up.util.Ut;

public class UserResolver implements Resolver<PojoUser> {
    @Override
    public Epsilon<PojoUser> resolve(
            final RoutingContext context,
            final Epsilon<PojoUser> income) {
        /*
         * 在您的方法中定义的 @BodyParam 类型
         */
        if (PojoUser.class != income.getArgType()) {
            /*
             * 常用 400
             */
            throw new _400BadRequestException(this.getClass());
        } else {
            final String content = context.getBodyAsString();
            /*
             * 执行转换,Resolver的核心逻辑
             */
            PojoUser user = new PojoUser();
            user.setEmail("lang.yu@hpe.com");
            if (Ut.notNil(content)) {
                user = Ux.fromJson(Ut.toJObject(content), PojoUser.class);
            }
            income.setValue(user);
            return income;
        }
    }
}

    书写好上述的Resolver组件过后,直接配置该组件:

package cn.vertxup.micro.jsr311;

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

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

@EndPoint
public class ResolverAgent {

    @POST
    @Path("/hi/jsr311/resolver")
    public JsonObject sayResolver(
            @BodyParam(resolver = UserResolver.class) final PojoUser user) {
        return new JsonObject().put("content", user.toString());
    }
}

    从响应信息中可以看到,设置的值已经生效:

{
    "data": {
        "content": "PojoUser{username='null', email='lang.yu@hpe.com', address=null}"
    }
}

    完整Resolver的开发注意点如下:

  • Resolver必须返回最初输入的参数Epsilon<PojoUser>,并且在执行过程中,将您需要的信息通过调用setValue传入。

  • Resolver会跳过@DefaultValue的注解,这种模式下这个注解的默认值会失效,——正如前文提到的,对于@BodyParam类型的参数而言,@DefaultValue比较鸡肋。

  • Resolver可以直接拿到Vertx中的核心引用RoutingContext,它给开发人员提供了非常大的自由度。

  • 如果整个系统中需要将错误信息返回到客户端,可抛出自定义的WebException,关于Zero中的容错机制,会在后续的章节中加以说明。

  • Resolver中千万不要调用RoutingContextHttpServerResponse响应客户端的方法,如果调用了该方法,会导致主代码逻辑中的内容失效,这一点特别重要

  • Resolver的构造函数必须是无参构造函数,否则会初始化不成功引起异常。

所以从上述注意点可以知道,Resolver尽可能只做入参之前的格式化、默认值等处理,为什么此处不做验证呢?——关于验证部分后续会有章节来说明,Zero中提供了另外的验证机制。

3.2.1. 子接口Solve

Zero版本 >= 0.5.3

    关于这部分内容,Zero中还提供了另外一种扩展模式,让开发人员只关注于数据本身,执行纯的Http请求处理,这种定义直接实现接口:io.vertx.up.uca.rs.mime.Solve ,参考下边代码:

package cn.vertxup.micro.jsr311;

import io.vertx.up.uca.rs.mime.Solve;
import io.vertx.up.unity.Ux;
import io.vertx.up.util.Ut;

import java.util.Objects;

public class UserSolve implements Solve<PojoUser> {
    @Override
    public PojoUser resolve(final String content) {
        /*
         * 执行转换,Resolver的核心逻辑
         */
        final PojoUser user = Ux.fromJson(Ut.toJObject(content), PojoUser.class);
        if (Objects.isNull(user.getEmail())) {
            user.setEmail("lang.yu@hpe.com");
        }
        return user;
    }
}

    写了上述组件过后,直接在原来的主代码ResolverAgent中添加如下方法:

    @POST
    @Path("/hi/jsr311/resolver-small")
    public JsonObject setSolve(
            @BodyParam(resolver = UserSolve.class) final PojoUser user) {
        return new JsonObject().put("content", user.toString());
    }

    这样即使发送空内容,响应信息中也会得到email='lang.yu@hpe.com',这种模式的开发相比Resolver 的完整实现要简单太多,只需要执行对应的处理逻辑即可,它的注意点如下:

  • 如果整个系统中需要将错误信息返回到客户端,可抛出自定义的WebException,关于Zero中的容错机制,会在后续的章节中加以说明。

  • Solve同样会跳过@DefaultValue的注解,这种模式下这个注解的默认值也会失效。

3.2.2. Resolver场景分析

    Resolver原始翻译是解析器,它的主要目的是做格式解析,和Filter 不同,不推荐用来做前端过滤器和请求拦截器,因为Zero除了支持JSR311,还支持部分其他的规范,如JSR340等,可以在JSR340的Filter中实现标准的前端过滤器。那么读者就会有一个疑问,Resolver的使用场景是什么呢?

兼容升级

    真实项目开发过程中,Resolver有一个作用是做多格式兼容处理,当一个现存的系统和新开发的系统要做接口合并以及格式兼容性升级的时候,Resolver就可以体现出它的作用。假设开发了一个新接口,它的处理格式如:

{
    "username": "Lang",
    "email": "lang.yu@hpe.com"
}

    而当这个新系统升级过后,原始的旧系统的请求需要对接过来,而原始系统由于历史久远且担任了很重要的作用,不太可能去变动(甚至有时候是开发团队流失导致了系统本身定型 ),这种情况下可以写一个简单的Resolver组件来实现,假设旧系统的请求如:

{
    "account": "Lang",
    "workEmail": "lang.yu@hpe.com"
}

    那么就可以直接在Resolver组件中实现统一格式转换,并且保证旧系统和新系统的对接都可以完美实现,这是真实项目过程中常见的一种情况。

前置条件

    正如本章之前所提,在很多接口执行逻辑之前,我们可以使用Resolver去屏蔽掉一些非法的信息,来实现数据规范化处理,这种情况有点类似于前端过滤器 的功能。但Resolver一般做的是轻量级的动作,既不访问数据库,也不会去访问IO,由于位于Agent之前,它所有的功能都属于同步处理,更多的场景中就是用来做格式转换,或者多格式兼容,有了它过后,在接口扩展上很容易实现横切面(AOP)的逻辑功能。

    而前置条件中最常用的功能就是默认值处理,这个功能等价于@DefaultValue的功能,但是@DefaultValue@BodyParam 合并使用时,由于@DefaultValue本身是静态的,不带任何代码逻辑,并且定义很鸡肋,所以resolver在此时扮演了动态@DefaultValue 的角色,补充了在这种情况下的默认值功能——这是Resolver设计的初衷。

3.3. 再谈上传

    前文使用@FormParam实现了单文件上传和多文件上传的功能,本章节再看Zero对JSR311的扩展,就是引入了另外一个注解jakarta.ws.rs.StreamParam 来实现二进制数据流的请求。还是先看看代码:

package cn.vertxup.micro.jsr311;

import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.FileUpload;
import io.vertx.up.annotations.EndPoint;

import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.StreamParam;
import java.io.File;
import java.util.Set;

@EndPoint
public class StreamAgent {

    @POST
    @Path("/hi/jsr311/stream")
    public JsonObject sayStream(@StreamParam final FileUpload fileUpload) {
        return new JsonObject().put("server-file", fileUpload.uploadedFileName());
    }

    @POST
    @Path("/hi/jsr311/file")
    public JsonObject sayFile(@StreamParam final File file) {
        return new JsonObject().put("server-file", file.getAbsolutePath());
    }

    @POST
    @Path("/hi/jsr311/stream")
    public JsonArray sayStreams(@StreamParam final Set<FileUpload> fileUploads) {
        final JsonArray normalized = new JsonArray();
        fileUploads.stream().map(FileUpload::uploadedFileName).forEach(normalized::add);
        return normalized;
    }
}

    发送请求分别会得到期望的上传格式,和@FormParam不同的点在于这种模式主要用于纯上传操作,也就是说不会去执行key=value 的操作模式,在旧版本的生产系统中,大部分地方采用了这种模式处理,如果是新版本格式,则可以使用@FormParam替代原始的上传模式,@FormParam可以在上传过程中携带参数,对业务性的扩展更有用。最后有两点需要特别说明:

  • 在上传请求中,必须携带Content-Length参数用于描述实体长度,否则服务端会抛出 411 Length Required 的异常。

  • @StreamParam@BodyParam的用法一致,也支持resolver的功能。

JSR311扩展中的@SessionParam@ContextParam有特殊用途,而@RpcParam是微服务专用的服务通信参数,这些注解的用法留到后续章节中逐一介绍。

3.4. 上下文

    Zero中除了对JSR311的支持以外,还可以直接从Agent中读取上下文对象(按类型匹配),这些上下文对象包括:

对象类型备注信息

io.vertx.up.commune.config.XHeader

多应用、多语言、多租户环境专用对象

io.vertx.ext.web.Session

HTTP会话对象

io.vertx.core.http.HttpServerRequest

HTTP请求对象

io.vertx.core.http.HttpServerResponse

HTTP响应对象

io.vertx.core.Vertx

Vertx实例

io.vertx.core.eventbus.EventBus

EventBus实例

io.vertx.ext.auth.User

User登录用户对象

io.vertx.core.json.JsonArray

请求体JsonArray

io.vertx.core.json.JsonObject

请求体JsonObject

io.vertx.core.buffer.Buffer

请求体Buffer

java.util.Set<io.vertx.ext.web.FileUpload>

Set集合专用对象

io.vertx.ext.web.FileUpload

单文件上传专用

    最后参考下边代码来看使用方法:

package cn.vertxup.micro.jsr311;

import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.eventbus.EventBus;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.User;
import io.vertx.ext.web.FileUpload;
import io.vertx.ext.web.Session;
import io.vertx.up.annotations.EndPoint;
import io.vertx.up.commune.config.XHeader;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import java.util.Objects;
import java.util.Set;

@EndPoint
public class TypedAgent {
    /*
     * 多语言多租户对象
     */
    @GET
    @Path("/hi/params/typed/x-header")
    public JsonObject header(final XHeader header) {
        return header.toJson();
    }

    /*
     * 会话对象
     */
    @GET
    @Path("/hi/params/typed/session")
    public JsonObject session(final Session session) {
        return new JsonObject().put("session.id", session.id());
    }

    /*
     * 请求、响应对象
     */
    @GET
    @Path("/hi/params/typed/request-response")
    public JsonObject request(final HttpServerRequest request,
                              final HttpServerResponse response) {
        return new JsonObject()
                .put("path", request.path())
                .put("key", response.getStatusCode());
    }

    /*
     * Vertx 引用对象
     */
    @GET
    @Path("/hi/params/typed/vertx")
    public JsonObject vertx(final Vertx vertx) {
        return new JsonObject()
                .put("vertx", vertx.toString());
    }

    /*
     * EventBus 对象
     */
    @GET
    @Path("/hi/params/typed/event-bus")
    public JsonObject eventbus(final EventBus bus) {
        return new JsonObject()
                .put("event-bus", bus.toString());
    }

    /*
     * User 用户对象
     */
    @GET
    @Path("/hi/params/typed/user")
    public JsonObject user(final User user) {
        final String userString = Objects.isNull(user) ? "Public" : user.toString();
        return new JsonObject()
                .put("user", userString);
    }

    /*
     * JsonArray 对象
     */
    @GET
    @Path("/hi/params/typed/json-array")
    public JsonObject jarray(final JsonArray array) {
        return new JsonObject()
                .put("jarray", array);
    }

    /*
     * JsonObject 对象
     */
    @GET
    @Path("/hi/params/typed/json-object")
    public JsonObject jobject(final JsonObject object) {
        return new JsonObject()
                .put("jobject", object);
    }

    /*
     * Buffer 对象
     */
    @POST
    @Path("/hi/params/typed/buffer")
    public JsonObject buffer(final Buffer buffer) {
        return new JsonObject()
                .put("buffer", buffer.length());
    }

    /*
     * 多文件上传对象
     */
    @POST
    @Path("/hi/params/typed/set")
    public JsonObject uploads(final Set<FileUpload> fileUploads) {
        return new JsonObject()
                .put("files", fileUploads.size());
    }

    /*
     * 单文件上传
     */
    @POST
    @Path("/hi/params/typed/upload")
    public JsonObject upload(final FileUpload upload) {
        if (Objects.isNull(upload)) {
            return new JsonObject();
        } else {
            return new JsonObject()
                    .put("filename", upload.uploadedFileName());
        }
    }
}

    这种按照类型直接匹配的模式可以不使用任何注解来修饰参数,不过这种模式中只能使用特定类型,而且需要开发人员记住上边表格中的类型列表,还有一点需要注意,目前所有的教程都没有使用异步 模式,在异步模式中(使用了EventBus),能够读取到的参数类型会有所不同,这点在后续教程中来说明。

    Zero中存在一些特殊的设计,针对上述的XHeader请求特殊说明一下:XHeader是Zero为多语言、多应用、多用户设计的特殊对象,该对象中包含了如下值:

HTTP请求头Json键

X-Sigma

sigma

X-App-Id

appId

X-App-Key

appKey

X-Lang

language

    发送请求时,如果带上了自定义的头,才会有响应信息,如:

X-Sigma = PZuSgIAE5EDakR6yr
X-App-Id = aedba77c-e020-4ef3-b50a-442d919cf2b1

    得到的响应信息如:

{
    "data": {
        "sigma": "PZuSgIAE5EDakR6yr",
        "appId": "aedba77c-e020-4ef3-b50a-442d919cf2b1"
    }
}

「肆」总结

    本章节比较长,主要介绍了Zero在RESTful请求参数部分接收参数的各种方法:

  • Zero支持JSR311中大部分读取参数的注解。

  • Zero对JSR311进行了扩展,针对实用性业务性进行了进一步设计和定制。

  • 为了让开发人员很方便读取Vertx框架的上下文环境,Zero还提供了上下文对象的引用读取功能。

    而由于入参本身的复杂性,本章节未涉及的内容:

  • @SessionParam会话注解的使用和@ContextParam上下文注解的使用。

  • @RpcParam微服务通信注解的使用。

  

  

  

Last updated