请求Body信息解析
约 1577 字大约 5 分钟
2025-05-02
问题分析
问题其实很明显,核心就是 如何支持多种多样的请求类型
,一般而言,面对此类问题,主要是如下两个解决方案:
- 请求网关接口,仅支持一种请求类型,如:仅支持json请求,此种解决方案的优缺点都很明显:
- 优点:对于应用网关复杂问题简化,仅支持一种请求方式,具有明确的输入类型与规则,无歧义,无复杂适配
- 缺点:对于使用方,可能会存在较高的调用改造成本。如:以前通过 xml 请求,现在要升级成通过 json 请求,需要升级网络库等基础组件,成本高,影响大
- 请求网关接口,支持任意的请求类型,此种解决方案的优缺点都很明显:
- 优点:服务使用方,无需大规模调整,仅在使用时,按照新接口约定,核对输入输出的处理即可。
- 缺点:应用网关需要理解多而繁杂的请求类型,如何兼容适配,需要有一套完整的处理规则。
两种方案对比如下:
方案 | 仅支持一种请求类型 | 适配各种不同请求类型 |
---|---|---|
网关复杂度 | 低 | 高 |
使用方影响程度 | 高 | 低 |
迁移成本 | 高 | 低 |
在考虑方案时,除了技术方案本身之外,通常也会将 使用方的使用意愿以及使用成本
作为一个重要的衡量因素,一切技术都应为具体的使用场景来服务
, 脱离了具体的使用场景,为了技术而技术,无异于虚空打靶。 基于此种考虑,我们选择第二个方案,由网关适配多种多样的请求类型,复杂度由网关消化,进而简化使用这的复杂度以及接入成本。
方案设计
对于我们所选择的方案,具有明显的 多输入格式
的特征,而为了标准化后续流程,必须统一输入数据格式,这一类过程一般叫做 数据标准化处理
,而此类场景,非常适合采用 设计模式中的适配器模式
解决问题. 采用适配器模式,我们需要完成如下几个步骤
常见的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
: 一般用于文件上传(本教程暂不考虑文件上传)
以上逻辑属于 输入确定(请求Body), 输出确定(map[string]any), 可以使用接口约束, 通过实现不同接口的方式来实现不同的解析方式.
适配器接口约束
type RequestBodyParseAdaptor interface {
// Unmarshal 解析Body数据,解析结果会反序列化至 receiver , 同时, 会以 map 结构返回
Unmarshal(ctx *gin.Context, receiver 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(), " ", ""))
if len(contentType) == 0 {
return nil, errors.New("content_type is empty")
}
// 裁剪出真实的类型,之所以截取,是因为 content_type 中可能还包含编码信息, 如 : application/json;chaset=utf8
contentTypeArr := strings.Split(contentType, ";")
contentTypeFormatArr = strings.Split(contentTypeArr[0], "/")
if len(contentTypeFormatArr) != 2 {
return nil, errors.New(contentType + " : content_type format error")
}
if _, exist := adaptorTable[contentTypeFormatArr[0]]; !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
}
}
具体实现
基础
读取Body信息, 是同一基础操作, 但是不同的请求类型, 解析方式也不同, 因此需要根据请求类型来解析, 读取Body信息的方式如下:
// ReadBody 读取Body信息
func ReadBody(ctx *gin.Context) ([]byte, error) {
var (
data []byte
err error
)
// 判断form url encode
if strings.Contains(ctx.ContentType(), "x-www-form-urlencoded") {
if err = ctx.Request.ParseForm(); nil != err {
return nil, err
}
body := map[string]any{}
for paramName, itemParam := range ctx.Request.PostForm {
if len(itemParam) > 0 {
body[paramName] = itemParam[0]
} else {
body[paramName] = ""
}
}
return serialize.JSON.MarshalForByteIgnoreError(body), nil
}
// 读取Body信息
if data, err = io.ReadAll(ctx.Request.Body); nil != err {
return nil, err
}
// 重置Body信息
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(data))
return data, nil
}
x-www-form-urlencoded 解析
表单默认的提数据格式, k/v 均为字符串, 因此使用json解析器即可, 注意body读取方式即可, 具体实现方式参见JSON解析部分
JSON 解析
json解析基于内置json包进行
type ownJson struct{}
func (o *ownJson) Unmarshal(byteData []byte, receiver any) error {
decoder := json.NewDecoder(bytes.NewReader(byteData))
decoder.UseNumber()
return decoder.Decode(receiver)
}
XML 解析
xml解析基于 xml2map 进行, 转为map
type ownXml struct{}
func (o *ownXml) Unmarshal(byteData []byte, receiver any) error {
res, err := xml2map.NewDecoder(bytes.NewReader(byteData)).Decode()
if nil != err {
return err
}
byetData, _ := jsom.Marshal(res)
return json.Unmarshal(byetData, receiver)
}
YAML 解析
yaml 解析基于 yaml.v3 进行, 转为map
type ownYaml struct{}
func (o *ownYaml) Unmarshal(byteData []byte, receiver any) error {
return yaml.NewDecoder(bytes.NewReader(byteData)).Decode(receiver)
}
实现汇总
// 初始化
func init() {
adaptorTable = map[string]RequestBodyParseAdaptor{
"json": &ownJson{},
"x-www-form-urlencoded": &ownJson{}, // 表单默认的提数据格式, k/v 均为字符串, 因此使用json解析器即可
"xml": &ownXml{},
"yaml": &ownYaml{},
"yml": &ownYaml{},
}
}
// Register 注册适配器, 用于注册支持的请求类型, json/xml/yaml/yml已内置, 注册同名解析器会覆盖内置的实现
func Register(requestType string, adaptor abstract.RequestBodyParseAdaptor) {
if nil == adaptor {
return
}
requestBodyParseAdaptorTable[requestType] = adaptor
}
// 解析请求BODY数据
func ParseRequestBody(ctx *gin.Context, receiver any) (map[string]any, error) {
contentType := strings.ToLower(strings.ReplaceAll(ctx.ContentType(), " ", ""))
if len(contentType) == 0 {
// 未设置content_type, 默认返回空json
return map[string]any{}, nil
}
// 裁剪出真实的类型,之所以截取,是因为 content_type 中可能还包含编码信息, 如 : application/json;chaset=utf8
contentTypeArr := strings.Split(contentType, ";")
contentTypeFormatArr := strings.Split(contentTypeArr[0], "/")
if len(contentTypeFormatArr) != 2 {
return nil, errors.New(contentType + " : content_type format error")
}
if _, exist := adaptorTable[contentTypeFormatArr[0]]; !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
}
}