网关逻辑之请求体解析
常见的Body类型
body解析比较特殊,不同的body类型需要不同的解析方式,而决定解析方式的是 header 中的 content_type属性
, 当前常见的Body类型如下:
application/json
: json格式的请求application/x-www-form-urlencoded
: 表单默认的提数据格式, k/v 均为字符串application/xml
: xml格式的请求application/yml 、application/yaml
: yaml格式的请求multipart/form-data
: 一般用于文件上传(本教程暂不考虑文件上传)
问题分析
问题其实很明显,核心就是 如何支持多种多样的请求类型
,一般而言,面对此类问题,主要是如下两个解决方案:
- 请求网关接口,仅支持一种请求类型,如:仅支持json请求,此种解决方案的优缺点都很明显:
- 优点:对于应用网关复杂问题简化,仅支持一种请求方式,具有明确的输入类型与规则,无歧义,无复杂适配
- 缺点:对于使用方,可能会存在较高的调用改造成本。如:以前通过 xml 请求,现在要升级成通过 json 请求,需要升级网络库等基础组件,成本高,影响大
- 请求网关接口,支持任意的请求类型,此种解决方案的优缺点都很明显:
- 优点:服务使用方,无需大规模调整,仅在使用时,按照新接口约定,核对输入输出的处理即可。
- 缺点:应用网关需要理解多而繁杂的请求类型,如何兼容适配,需要有一套完整的处理规则。
两种方案对比如下:
方案 | 仅支持一种请求类型 | 适配各种不同请求类型 |
---|---|---|
网关复杂度 | 低 | 高 |
使用方影响程度 | 高 | 低 |
迁移成本 | 高 | 低 |
在考虑方案时,除了技术方案本身之外,通常也会将 使用方的使用意愿以及使用成本
作为一个重要的衡量因素,一切技术都应为具体的使用场景来服务
, 脱离了具体的使用场景,为了技术而技术,无异于虚空打靶。 基于此种考虑,我们选择第二个方案,由网关适配多种多样的请求类型,复杂度由网关消化,进而简化使用这的复杂度以及接入成本。
方案设计
对于我们所选择的方案,具有明显的 多输入格式
的特征,而为了标准化后续流程,必须统一输入数据格式,这一类过程一般叫做 数据标准化处理
,而此类场景,非常适合采用 设计模式中的适配器模式
解决问题. 采用适配器模式,我们需要完成如下几个步骤
适配器接口约束
type RequestBodyParseAdaptor interface {
// Parse 解析Body数据,解析结果会反序列化至 receiver , 同时, 会以 map 结构返回
Parse(ctx *gin.Context, receiver any) (map[string]any, error)
}
适配器注册
对于适配器,需要维护一个全局的注册表,用于注册支持的请求类型, 其结构为
adaptorTable = map[string]RequestBodyParseAdaptor
在服务启动之时,注册适配器
适配器触发
触发适配器,则是基于请求类型,将处理逻辑分发至不同的实际处理适配器,其逻辑如下:
// ParseRequestBody 解析请求BODY数据
func ParseRequestBody(ctx *gin.Context, receiver any) (map[string]any, error) {
contentType := strings.ToLower(strings.ReplaceAll(ctx.ContenType(), " ", ""))
// 裁剪出真实的类型,之所以截取,是因为 content_type 中可能还包含编码信息, 如 : application/json;chaset=utf8
contentTypeArr := strings.Split(contentType, ";")
contentType = contentTypeArr[0]
if _, exist := adaptorTable[contentType]; !exist {
return nil, errors.New(contentType + " : adaptor not found")
}
if parseResult, err := adaptorTable[contentType].Parse(ctx, receiver); nil != err {
return nil, err
} else {
return parseResult, err
}
}