1.4.思无邪:入参
百舌搬春春已透,长驿短亭芳草昼,家山肠断欲归人,风宿留、船津候。——吴潜《天仙子》
项目地址:https://github.com/silentbalanceyh/vertx-zero-example/(子项目:up-apollo)
「壹」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中运行上述代码并发送请求,就会得到注释中的响应信息。请求格式如下:
注意上述代码中的参数格式:
第一种格式
:name是Vertx框架中支持的原生格式,整个Path部分会遵循Vertx中定义的规范,包括正则表达式以及其他相关内容。第二种格式
{email}是很多开发者熟悉的Spring框架和JSR311中的常用格式,Vertx是不支持这种格式的,但Zero同样支持这种格式,以保证开发人员的使用。
统一路径格式的主要目的是让任何人都可以上手,虽然这部分具有一定的二义性,但使用Zero的开发人员背景不同,可能习惯会有所差异,经常用vertx-web 开发的人比较钟意第一种格式,而如果曾经是Spring的开发人员,则倾向于第二种格式。
Zero为了解决开发人员的某些错误写法,提供了路径的兼容解析,参考下边代码:
上述两种路径通常会理解成开发人员的错误设计或错误使用,但从最终结果可以知道开发人员原本用意,Zero会有专用日志记录这个警告,但以正确结果发布RESTful接口,在项目进度比较紧张的时候可以不用考虑这种** 失误**,警告部分日志如下:
1.2. 查询参数
查询参数通常用于传统的GET页面请求,一般情况下不带Body直接发送,通常用于获取资源信息,参考下边代码:
发送下边的请求就可以得到注释中的响应信息:
注,在第二个请求中包含了请求体的内容:
从示例代码中可以看到,对应的请求参数name直接被读取到了。
1.3. 请求体
第三种常用的参数是请求体参数,一般用于POST和PUT中,当然在zero框架中,GET和DELETE 请求同样可以携带Body请求体参数,上边第二个例子演示的就是在GET请求中携带Body请求体参数。Zero中的请求体参数主要有两种,通过扩展JSR311来注解:
字符流请求体注解:
jakarta.ws.rs.BodyParam。二进制(字节流)数据请求体注解:
jakarta.ws.rs.StreamParam。
扩展JSR311是Zero为了渗透业务量身定制的两个注解,参考下边代码:
第一个请求内容很简单,而第二个请求实际上是生产环境代码的片段,读者可以参考注释内容去理解,这里主要是演示相关概念,发送请求后可以得到如下响应:
关于Zero的上传下载部分,后续会提供专有的章节来说明。
1.4. 请求头
Zero支持JSR311中的头参数,对自定义请求头和安全专用的Authorization请求头尤其有用,参考下边代码读取请求头内容。
如果在Postman中设置如下信息
X-Sigma
lang.yu
Authorization
Basic ExtRS
那么请求发送过后会得到如下响应结果:
Header的消费场景如下:
在安全认证过程中用于读取Authorization的请求头信息。
自定义特殊的请求头,如Zero中的
X-Sigma, X-App-Id等扩展请求头。条件查询和转发过程中的请求头。
1.5. 入参的断思
前边章节中演示了在Zero中如何使用不同的参数类型,参数的分类维度是业务场景,实际开发过程中上述场景也比较常用——但是这里并没有枚举所有的传参场景,请读者注意。
从我们最初学习编程开始,书写一个函数就会遇到Pre-Condition和Post-Condition的概念,分别对应函数本身的前置条件和后置条件 ,每一个函数在编写过程中最好是没有副作用,所以前置条件可以保证函数一定会成功执行或成功容错,也是真正开发过程中最关注的条件。分析上述的入参,有几点可以让读者思考一下:
路径参数不需要Required判断,因为路径参数如:
/user/:name只有在name有值的时候才会被成功匹配,所以这种类型的请求是不需要任何类似Required必填检查的,甚至于可以理解成系统必定会为name赋值。如果入参的数据类型无法执行转换,Zero会返回如下系统错误信息,HTTP状态代码是
400(如在前文的例子中发送/hi/param/age/Lang,定义中入参应该是整型):查询参数是可选参数,所以有可能会返回
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.QueryParamjakarta.ws.rs.PathParamjakarta.ws.rs.HeaderParam
这部分代码不再累赘,前边也经常用到过,有一点心得提供给读者:如果使用@HeaderParam注解,推荐用jakarta.ws.rs.core.HttpHeaders设置HTTP Header的名称,而不是直接用字符串的方式手写,这种方式可以减小编程过程中的错误率。
2.2. Form参数
Zero版本 >= 0.5.3
本节演示如何使用jakarta.ws.rs.FormParam ,虽然这种参数在RESTful的通用Json请求中不太常见,但JSR311中还是会包含这部分内容,而且在处理上传下载时会有一定的限制,先参考下边代码:
分别针对上述三个开放接口发送请求,在Postman中选择form-data的上传模式,会得到如下的响应:
请求一结果
请求二结果
请求三结果
针对本小节的请求,读者需要严格遵循下边的规则,否则会导致无法读取上传文件的错误:
文件上传请求的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[]类型。
2.3. Cookie参数
本文不解释什么是Cookie以及Cookie的基本概念,直接演示jakarta.ws.rs.CookieParam的用法:
在您的请求中添加下边的Cookie值:
可以直接得到如下响应:
有一点需要注意是
cookieOjb可能会是 null,如果对应的 Cookie 不存在,则需要检查是否为空来保证代码本身不会抛出空指针异常。
2.4. 默认值
JSR311中有一个设置默认值的注解:jakarta.ws.rs.DefaultValue,本节介绍Zero中DefaultValue的用法,参考下边代码:
先发送下边两个请求查看响应结果:
其次尝试发送日期处理请求/hi/jsr311/default-date?from=2010-08-17得到如下响应:
Zero中的时间解析格式最终结果会以标准的UTC格式呈现,如果是字符串的字面量格式,可以直接传入让Zero转换,只要格式合法就可以转换成响应的时间值。后续章节中开发者可以直接调用内部Api转换成您想要的
LocalDateTime, LocalDate等合法格式。
除开上述的格式以外,开发者还可以设置POJO类的默认值(只支持JSON),不过这个功能可能比较鸡肋,因为大部分情况下,POJO 类的默认值都会直接写入到类定义中,而不是在这里通过字面量来设置。参考下边代码:
这种情况下,即使不发送任何Body的内容,也会返回如下响应:
读者可以看到上述代码中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类:
主代码:
如果请求中Body数据如下:
则您可以看到最终响应部分格式如下:
3.2. 引入resolver
@BodyParam除了支持普通的@BeanParam功能以外,还附加引入了resolver 属性,Resolver是Zero中的请求解析器,可以根据您想要的格式解析请求数据,并且实现在参数接收之前的处理动作,属于高级开发部分的内容。@BodyParam的resolver 属性可以指定一个实现了接口的Java的类名,然后在真正得到该数据之前进行转换。
默认情况下,Zero会根据请求的MIME来执行请求解析,而这个属性可以提供自定义的请求解析器,系统定义的Resolver配置如下:
上述的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有两个级别:
直接实现
io.vertx.up.uca.rs.mime.Resolver<T>接口,重写整个Resolver。只实现接口
io.vertx.up.uca.rs.mime.Solver<T>接口,只重写核心流程部分(Resolver内部部分)。
3.2.1. 开发完整的Resolver
先看一个完整定义Resolver的例子:
书写好上述的Resolver组件过后,直接配置该组件:
从响应信息中可以看到,设置的值已经生效:
完整Resolver的开发注意点如下:
Resolver必须返回最初输入的参数
Epsilon<PojoUser>,并且在执行过程中,将您需要的信息通过调用setValue传入。Resolver会跳过
@DefaultValue的注解,这种模式下这个注解的默认值会失效,——正如前文提到的,对于@BodyParam类型的参数而言,@DefaultValue比较鸡肋。Resolver可以直接拿到Vertx中的核心引用
RoutingContext,它给开发人员提供了非常大的自由度。如果整个系统中需要将错误信息返回到客户端,可抛出自定义的
WebException,关于Zero中的容错机制,会在后续的章节中加以说明。Resolver中千万不要调用
RoutingContext中HttpServerResponse响应客户端的方法,如果调用了该方法,会导致主代码逻辑中的内容失效,这一点特别重要。Resolver的构造函数必须是无参构造函数,否则会初始化不成功引起异常。
所以从上述注意点可以知道,Resolver尽可能只做入参之前的格式化、默认值等处理,为什么此处不做验证呢?——关于验证部分后续会有章节来说明,Zero中提供了另外的验证机制。
3.2.1. 子接口Solve
Zero版本 >= 0.5.3
关于这部分内容,Zero中还提供了另外一种扩展模式,让开发人员只关注于数据本身,执行纯的Http请求处理,这种定义直接实现接口:io.vertx.up.uca.rs.mime.Solve ,参考下边代码:
写了上述组件过后,直接在原来的主代码ResolverAgent中添加如下方法:
这样即使发送空内容,响应信息中也会得到email='lang.yu@hpe.com',这种模式的开发相比Resolver 的完整实现要简单太多,只需要执行对应的处理逻辑即可,它的注意点如下:
如果整个系统中需要将错误信息返回到客户端,可抛出自定义的
WebException,关于Zero中的容错机制,会在后续的章节中加以说明。Solve同样会跳过
@DefaultValue的注解,这种模式下这个注解的默认值也会失效。
3.2.2. Resolver场景分析
Resolver原始翻译是解析器,它的主要目的是做格式解析,和Filter 不同,不推荐用来做前端过滤器和请求拦截器,因为Zero除了支持JSR311,还支持部分其他的规范,如JSR340等,可以在JSR340的Filter中实现标准的前端过滤器。那么读者就会有一个疑问,Resolver的使用场景是什么呢?
兼容升级
真实项目开发过程中,Resolver有一个作用是做多格式兼容处理,当一个现存的系统和新开发的系统要做接口合并以及格式兼容性升级的时候,Resolver就可以体现出它的作用。假设开发了一个新接口,它的处理格式如:
而当这个新系统升级过后,原始的旧系统的请求需要对接过来,而原始系统由于历史久远且担任了很重要的作用,不太可能去变动(甚至有时候是开发团队流失导致了系统本身定型 ),这种情况下可以写一个简单的Resolver组件来实现,假设旧系统的请求如:
那么就可以直接在Resolver组件中实现统一格式转换,并且保证旧系统和新系统的对接都可以完美实现,这是真实项目过程中常见的一种情况。
前置条件
正如本章之前所提,在很多接口执行逻辑之前,我们可以使用Resolver去屏蔽掉一些非法的信息,来实现数据规范化处理,这种情况有点类似于前端过滤器 的功能。但Resolver一般做的是轻量级的动作,既不访问数据库,也不会去访问IO,由于位于Agent之前,它所有的功能都属于同步处理,更多的场景中就是用来做格式转换,或者多格式兼容,有了它过后,在接口扩展上很容易实现横切面(AOP)的逻辑功能。
而前置条件中最常用的功能就是默认值处理,这个功能等价于@DefaultValue的功能,但是@DefaultValue和@BodyParam 合并使用时,由于@DefaultValue本身是静态的,不带任何代码逻辑,并且定义很鸡肋,所以resolver在此时扮演了动态@DefaultValue 的角色,补充了在这种情况下的默认值功能——这是Resolver设计的初衷。
3.3. 再谈上传
前文使用@FormParam实现了单文件上传和多文件上传的功能,本章节再看Zero对JSR311的扩展,就是引入了另外一个注解jakarta.ws.rs.StreamParam 来实现二进制数据流的请求。还是先看看代码:
发送请求分别会得到期望的上传格式,和@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
单文件上传专用
最后参考下边代码来看使用方法:
这种按照类型直接匹配的模式可以不使用任何注解来修饰参数,不过这种模式中只能使用特定类型,而且需要开发人员记住上边表格中的类型列表,还有一点需要注意,目前所有的教程都没有使用异步 模式,在异步模式中(使用了EventBus),能够读取到的参数类型会有所不同,这点在后续教程中来说明。
Zero中存在一些特殊的设计,针对上述的XHeader请求特殊说明一下:XHeader是Zero为多语言、多应用、多用户设计的特殊对象,该对象中包含了如下值:
X-Sigma
sigma
X-App-Id
appId
X-App-Key
appKey
X-Lang
language
发送请求时,如果带上了自定义的头,才会有响应信息,如:
得到的响应信息如:
「肆」总结
本章节比较长,主要介绍了Zero在RESTful请求参数部分接收参数的各种方法:
Zero支持JSR311中大部分读取参数的注解。
Zero对JSR311进行了扩展,针对实用性和业务性进行了进一步设计和定制。
为了让开发人员很方便读取Vertx框架的上下文环境,Zero还提供了上下文对象的引用读取功能。
而由于入参本身的复杂性,本章节未涉及的内容:
@SessionParam会话注解的使用和@ContextParam上下文注解的使用。@RpcParam微服务通信注解的使用。
Last updated