1.6.潘多拉魔盒:异步
敕勒川,阴山下。天似穹庐,笼盖四野。天苍苍,野茫茫,风吹草低见牛羊。 ——南北《敕勒歌》
项目地址:https://github.com/silentbalanceyh/vertx-zero-example/(子项目:up-rhea)
「壹」异步原理
到目前为止,所有的代码示例都是同步(Sync)的,代码并没有真正释放Zero框架和Vert.x的魔力,本章节就是彻底地放飞,把这个潘多拉 魔盒打开。在Vert.x中,我们通常会把一份数据、一个包含数据的请求统称为Event(事件),所以本章将会讲解Zero中最核心的内容——事件驱动模型(Event Drivent Model)。
1.1. 风格综述
在网络消息交互模式中,通常会有下边四种(参考WSDL规范):
图片引用地址:https://help.perforce.com/hydraexpress/4.3.0/html/rwsfexpwsfabricationug/9-2.html
Zero中针对上述四种模式分成了四种类型:
由于Solicit-Response模式和Notification模式都是服务端主动触发,这两种模式目前Zero的版本还不支持(和作者接触的项目需求有关)。
Zero中对编程风格的划分主要基于三个维度:
Java语言:是interface类还是class类。
Vert.x工具集:是否启用EventBus。
WSDL规范:是否关心响应。
本章之后章节会讲解八种风格中Zero推荐和高频使用的编程风格。
1.2. Zero模式
Vert.x中的Verticle组件主要分为两种类型:EventLoop和Worker,根据这两类组件的特征,最终Zero对组件职责进行了重新定义:
Zero最终支持五种核心的请求模式。
1.2.1. Sync - 同步模式
流程图
代码
限制规则
方法定义的返回类型(return)不能是
void
。不使用
@Address
注解(Zero专用注解io.vertx.up.annotations.Address
)。
1.2.2. Ping - 信号模式
流程图
代码
限制规则
方法定义的返回类型(return)必须是
void
。该方法只会收到类似
true/false
的响应信息,并不会得到任何和代码逻辑相关的响应数据。
1.2.3. OneWay - 单向模式
流程图
代码:Sender发送者
代码:Consumer消费者
限制规则
该模式中使用了Zero的三个核心注解:
Agent组件:
io.vertx.up.annotations.EndPoint
。Worker组件:
io.vertx.up.annotations.Queue
。EventBus地址:
io.vertx.up.annotations.Address
。
EndPoint注解的Agent组件方法不能使用
void
的返回值。Worker签名必须符合Zero中的Worker方法签名规范,并且返回值必须是
void
。
1.2.4. Standard - 标准模式
流程图
代码:Sender发送者
代码:Consumer消费者
限制规则
该模式中使用了Zero的三个核心注解:
Agent组件:
io.vertx.up.annotations.EndPoint
。Worker组件:
io.vertx.up.annotations.Queue
。EventBus地址:
io.vertx.up.annotations.Address
。
EndPoint注解的Agent组件方法不能使用
void
的返回值。Worker签名必须符合Zero中的Worker方法签名规范,并且返回值不能是
void
。
1.2.5. Callback - 回调模式
流程图
代码:Sender发送者
代码:Consumer消费者
限制规则
该模式中使用了Zero的三个核心注解:
Agent组件:
io.vertx.up.annotations.EndPoint
。Worker组件:
io.vertx.up.annotations.Queue
。EventBus地址:
io.vertx.up.annotations.Address
。
EndPoint注解的Agent组件方法不能使用
void
的返回值。Worker签名是固定签名规范,返回值必须是
void
。
可下载完整代码后,参考up-rhea项目。
「贰」统一模型
上述原理解析过程中,图解和代码十分容易理解,稍显复杂的是Zero中定义的统一数据模型:io.vertx.up.commune.Envelop
,该模型充斥在整个Zero环境中,并且可直接在EventBus中传输(重写过Codec),于是它的数据结构就值得我们细细分析了——该数据模型融合了Request-Response过程中所有牵涉的核心数据,成为Zero框架中序列化子系统的核心内部传输模型(下文统一翻译“信封”)。
2.1. 数据结构
它的核心结构如下:
说明几点:
虚线箭头为「引用」,仅保存了某个复杂Java对象引用,虽然当前信封是请求域内的数据消息,但所有引用类型对象是单次请求内跨所有信封 而实现共享——微服务RPC传输、完美转发等所有内部逻辑中会拷贝Envelop而生成新的信封,但会保留最初的请求信息。
Assist中存储的大部分内容为请求的元数据,由于它是包域,所以不推荐开发人员直接使用,为内部对象,它主要包括:
上下文环境
当前会话
登录用户基本信息
HTTP方法和uri地址
HTTP请求头
Acl在使用时必须提供额外的实现,在
zero-rbac
中提供了io.vertx.tp.rbac.atom.acl.AclData
实现,有兴趣的读者可以参考它的源代码。该数据结构中的access, edition, fields等信息可自定义,最终根据实现代码决定。
2.2. 常用API
在分析了Envelop本身的数据结构后,再根据部分场景来看它对应的API相关应用。
Java Bean规范类型的API列表格说明,用于设置获取,就不详细解析了。
2.2.1. 构造Envelop
Envelop没有提供public
的构造方法,在构造Envelop对象过程中只能使用该类中定义的静态API;不论是同步还是异步 模式,最终都会绑定一个HTTP状态(状态代码和描述文字),Envelop的内部构造函数如:
根据它的使用场景,有七个常用的静态构造API:
2.2.2. 数据操作
Envelop对象的数据操作类API主要分为三种核心操作:
合法性检测
数据读取
数据修改
Zero前端接收到的请求数据最终会以JSON格式存储在Envelop对象中,直接消费Envelop对象的组件通常是Worker组件(前文提到的消费者Consumer)并非Agent组件,标准模式下一个Worker中的方法定义如:
1)合法性检测
合法性检测是最简单的API,由于一个信封会分为:成功和失败两种,所以合法性检测类的API主要用于检查当前信封是否是一个成功的信封。成功和失败的定义如下:
成功:当前请求执行过程中没有出现任何意料之外的错误,内部
error
为空。失败:执行过程出现了不可预知的错误,内部
error
为一个合法的WebException
异常。
一个Envelop对象只有在构造时(Envelop.failure
)可设置内部的WebException,被构造成失败的信封 后,该Envelop对象不可再更改WebException——用业务语义解释就是:一旦出错就只能生成异常响应。
2)数据读取
数据读取包含了四个方法:
Envelop的数据提取是在Worker中发生,一般情况除非是同步模式的代码,否则在Agent组件中不会操作Envelop,所以读者要有一个基本的认知:一旦开始使用Envelop,就意味着你开始执行异步模式,引入了Worker组件开发。
3)数据修改
Envelop中的数据修改包含三个方法:
数据修改需说明的是Object value
必须是Vert.x中JsonObject内部支持的数据类型,不支持的类型如LocalDateTime, LocalDate
等无法直接设置,会报错。
2.2.3. 拷贝Envelop
虽然这里用了拷贝一词,严格说是不够严谨的,本小节提到的所有API都不拷贝数据,而是拷贝Envelop
中的元数据,如Assist, RoutingContext, User
等等;在Vert.x中,Worker组件本身是无法得到任何和RoutingContext相关数据的,除非开发人员在EventBus中传入了相关数据信息,否则无法在Worker组件中拿到任何请求过程中的Web对象——** Zero的Envelop解决了这个问题**。
1)核心拷贝
核心拷贝API的图如下:
这些API的基本思路——最终返回的Envelop都是拷贝的目标对象。
2)查询引擎
查询引擎类的API只能修改两个查询参数criteria
和projection
:
核心规则:
修改projection过程中,系统自动实现去重,会生成最后的列过滤字段集合。
修改criteria时,Zero会检测查询语法树进行同条件合并(复杂运算,树算法)。
3)安全和头
Envelop中和安全相关的API有四类:
设置追踪唯一标识,可执行追踪链功能。
存储
vertx-auth
项目中的io.vertx.ext.auth.User
引用,并且提供了Jwt令牌的快速读写以及User中存储的principal的快速读写(实现依赖安全部分的开发)。扩展了原始的HTTP头方法,提供
X-
前缀的自定义头的参数转换。读写Zero中定义的
io.vertx.up.commune.secure.Acl
权限控制对象(实现依赖安全部分的开发)。
安全相关的API如下:
请求头的数据,为简化Extension扩展模块的开发,所以Envelop提供了一个特殊的API:
该API会返回一个完整的JsonObject,它包含了四个键值appId, appKey, sigma, language
,分别对应下边表格中的自定义请求头。
4)设置获取
本小节提到的方法可能和前一小节有些重复,主要是枚举所有和设置获取相关的成对方法,注意0.5.4
版和0.6.0
版本的区别(表格中旧版为0.5.4
版)。
新版在旧版中的改动主要是设置 函数,获取数据的方法没有任何更改,这种改动主要是应用Java语言设计中的方法重载,简化API方法名,并且摒弃曾经比较繁琐的JavaBean规范相关方法(set/get等)。
5)响应格式
响应格式的API只有三个方法:
2.2.4. 参数规范
浅析过Envelop的数据结构和核心API,本小节补充几个示例来演示内部Json的格式变化,称这部分内容为参数规范 ,是因为不同的场景中,格式的变化、演化、区分都是由Zero框架来完成的。
1)隐藏键值
隐藏键值为Zero定义的内部属性值,分别在Zero内部承担着不同的职责,所有隐藏键值定义于io.vertx.up.eon.ID
中。
请求缓存$$PARAM_CONTENT$$
工作原理:参考上图,在Vert.x中读取请求数据时,RoutingContext
中getBody**
类的方法只能调用一次(底层是流模式,一旦流数据被消费,通道会被关闭,不可执行二次读取),但往往在实际项目中处理参数时会包含部分预处理 逻辑,如:验证、过滤、规范化等。为解决此问题,Zero中设计了请求缓存,请求缓存帮助Zero执行参数规范化,步骤如下:
参数来源主要分五种:查询参数(Query)、路径参数(Path)、请求体(Body)、请求头(Header)、自定义,规范化的第一步是将五种不同来源的参数合并到一个Json中。
如果存在Zero扩展的验证器(非Hibernate Validator基础验证),对合并过后的Json数据进行验证,检查是否生成400(Bad Request)响应。
如果存在JSR340中的过滤器,修改了参数,则将修改过后的参数回写到
$$PARAM_CONTENT$$
中。若存在其他扩展自定义操作,则执行完成后都可回写到
$$PARAM_CONTENT$$
中。
注意:
请求缓存执行的任何代码逻辑都在第一次调用
RoutingContext
的next()
之前。执行完请求缓存的代码后,请求数据处于可构造Envelop模型的阶段(已完成了基本标准化),换句话说,
$$PARAM_CONTENT$$
存储的是合法的原始请求。
Mime分流器
Mime分流器有两个隐藏键值:
$$DIRECT$$
:对应MimeFlow.RESOLVER
——自定义Mime解析流程,Resolver扩展。$$IGNORE$$
:对应MimeFlow.TYPED
——按类型解析。默认情况对应
MimeFlow.STANDARD
——标准解析流程。
参数缓存$$CONTEXT_REQUEST$$
工作原理:参考上图,从第一次调用RoutingContext
的next()
开始,参数传输都是使用参数缓存 来完成,不仅如此,编排的所有Handler串联在一起形成处理器链,每一个处理器形成函数化后的Monad单子。执行完处理器链过后,您有两种选择:
异步:将请求数据发送到EventBus丢给Worker组件执行。
同步:直接使用或消费,生成响应。
注意:
参数缓存是跨Handler的缓存,所有代码逻辑在第一次调用
RoutingContext
的next()
之后。$$CONTEXT_REQUEST$$
存储的是合法的业务请求。
如果没有添加任何扩展,
$$PARAM_CONTENT$$
和$$CONTEXT_REQUEST$$
中存储的请求内容是一致的。
2)Json结构
原始请求在执行完参数提取后,会生成统一的Json结构:
参数说明如:
这种数据结构存在于Zero内部,只有在提取了Envelop中的原始Json数据时,需要开发人员自己去操作,大多数场景中,不需要开发人员去解析,理解结构后方便调试。
「叄」编程规范
Zero开发中,有一套目前已经应用于生产环境的基础编程规范,此处介绍部分实用的命名规范。
除开本章提到的编程规范,读者还可以参考
vertx-zero/vertx-pin
中子模块的代码结构。
3.1. 包结构
先设定一个模块的根包,如:
如此完整的Zero模块包结构如下:
此处分享几个基本的设计心得,上述包结构中,cmdb
作为了整个模块的根包,主要分成两大类:
此处将Agent/Worker组件放在cn.vertxup
子包中的目的是提高系统扫描的速度,并且防止Zero框架对代码进行深度扫描(后续版本需要替换算法 改进),而在Zero项目里,已经没有原来类似MVC分层的概念:
Jooq的自动化工具引入可直接生成领域模型和Dao层,所以数据访问层以及领域模型之下的内容全部放在
domain
的领域模型包中。按照职责把业务逻辑层主要分为五个大类:
异常包:位于
error
包中,连接Zero的容错系统。常量包:位于
cv
包中(枚举则用cv.em
子包)。工具包:位于
util
包中(我们使用时则是用了refine
的包名,带有提炼的含义)。业务组件包:位于
service
包中——包含了核心业务逻辑,一般业务逻辑层的接口不消费Envelop,如果存在多个子模块则可以用service.xxx
的名称,将子模块直接分开。模型包:位于
atom
包中,通常的复杂业务模型将会使用该包(如果只是CRUD则不用)。
Zero连接专用包位于
cn.vertxup
的子包中,主要分为两种常量包:位于
cv
,这里的常量包只存储EventBus绑定地址和Pojo映射文件名。组件包:位于
micro
,如果存在多个子模块,则可以用子模块名代替包名。
3.2. 通用命名
本章提供部分推荐命名(已经过生产环境验证)。
3.2.1. cn.vertxup包
根包:
cn.vertxup.cmdb
3.2.2. cn.originx包
根包:
cn.originx.cmdb
3.3. Zero的命名哲学
刚开始接触Zero的读者对Zero中的部分数据规范、文件命名会有些许不习惯,最后介绍一下Zero中基础命名规范的设计哲学。五代时的《还金述》中有一句经典:“妙言至径,大道至简。”,而简 就是Zero中命名的核心,不论是开发Zero还是在Zero项目中,都遵循了几个基本规则:
规则一:打破传统的分层结构,以组件/模块化为核心的编程。——在Zero中我不推荐开层 的概念,主要原因是在复杂度高了过后,调用链会变得混乱,这里并不是说传统的分层思维不好,而是在如今的企业系统中,一个高内聚的组件比起业务分层更具有说服力,在这样的生态中,切分 会变得十分容易。所以最终在Zero的项目中,一个具有完整语义的业务组件往往会放到一个子包中去实现,如前文提到的
cmdb
。规则二:极致的重复率。——Zero的代码重复率并不高,是因为组件本身比较散,用IDEA检测下来的代码重复率在5%以下(几乎没有重复代码警告),而达到这种重复率的目的就是为了容易改BUG,当你系统中遇到某一个问题时,不会出现** 牵一发动全身**的情况(我们在生产环境中遇到的大部分文件和BUG三年运维下来目前都是修改的不超过十行代码的地方)。
规则三:新闻导语法。——写过新闻文体的读者应该清楚,新闻的主要内容有时候可能很多,但导语往往如同一个快照,决定了你是否会进入到内容里面去,而Zero中的导语就是文件名:
最典型的命名是异常:
_500ServerInternalException
这种,文件名中既包含了Http状态代码,又包含了基础描述,当你Stack信息中出现异常时也可以精准定位该异常发生的位置。文件名通常会是
主体 + 组件类型
的格式,这样的格式方便您在文件系统中查找,简单说在同一个包内,UserActor, UserApi, UserStub, UserService四个类名会放到集中在一起的四个Java文件中(字典序)。
规则四:短而具体。——Zero在项目开发过程中祛除了曾经通用的一些类似Manager,Dao,Service等各种太过于泛化的名称,而是使用了更具有隐喻 的组件类型名来规划整个系统,整个系统中的文件内容部分有效代码几乎都不会超过30行:
如果出现了复杂行数的类文件,则可以考虑按职责拆分。
如果出现了只有一个方法的类文件,则可以考虑把代码逻辑放到调用它的组件中。
如果一个方法在两个地方或以上被调用,则可以考虑工具化。
如果一个工具方法只在某个一类中调用,则可以直接转换成私有内部静态方法。
归根到底,Zero框架中的某些你不太适应的命名规则并不是为了破坏,而是方便不同职位的人:开发、测试、运维,三者均衡的命名模式,大部分比较古怪的规则都是为了调试。
「肆」总结
本文主要涵盖了三个核心知识点:
Zero中同步和异步编程的基本代码框架,图示数据流程原理。
Zero中请求封装核心数据模型
io.vertxup.up.communit.Envelop
的结构和常用API解析。Zero中的基本编程规范。
Last updated