接口参数为什么越写越乱-6种请求类型统一南北与东西流量

0 阅读6分钟

接口参数为什么越写越乱?6 种请求类型统一南北与东西流量

摘要: MetaLite 按“公网/内网、是否登录、是否携带业务参数”组合出 6 种请求类型。本文结合源码说明系统字段、业务参数和调用上下文为什么需要分层,以及网关如何把外部协议转换成内部强类型请求。

同一个业务接口,从前端调用、第三方开放平台调用和微服务内部调用,看起来都只是一个 HTTP 请求。

但它们承担的信任边界完全不同。

公网请求需要识别调用方,可能要验签、解密和校验登录状态;内部请求更关心服务提供者、消费者和调用上下文。如果把这些字段全部塞进每个业务 DTO,接口越多,参数就越容易失控。

MetaLite 没有为每个接口临时拼装系统字段,而是按照三个维度组合出 6 种请求类型:

  1. 流量来自公网,还是服务内部;
  2. 是否需要用户登录;
  3. 是否携带业务参数。

这篇文章只讨论请求建模和协议转换,不重复展开签名算法。

一、先区分南北流量与东西流量

在微服务架构中,通常把外部调用系统的流量称为南北流量,把服务之间的调用称为东西流量。

两类请求虽然都可以使用 HTTP,却不应该共享同一个协议对象。

公网请求面对的是不可信或半可信调用方,需要携带:

  • API 版本;
  • 调用方 appId
  • 明文或密文业务数据;
  • 时间戳;
  • 签名;
  • 需要登录时使用的 userToken

内部请求已经越过外部协议治理边界,更适合表达:

  • 服务提供者 provider
  • 服务消费者 consumer
  • 已经转换完成的强类型业务参数。

如果后端业务服务仍然直接接收 appIdsignencryptDatauserToken,说明外部协议已经侵入了业务边界。

二、三个维度为什么最终是 6 种类型

MetaLite 当前的请求类型关系如下:

InternalReq
└── InternalBizParamReq<T extends Param>

ExternalReq
├── ExternalBizParamReq<T extends Param>
└── ExternalLoginReq
    └── ExternalLoginBizParamReq<T extends Param>

对应关系可以整理成一张表:

请求类型流量方向登录状态业务参数
ExternalReq公网不要求登录
ExternalBizParamReq<T>公网不要求登录
ExternalLoginReq公网要求登录
ExternalLoginBizParamReq<T>公网要求登录
InternalReq服务内部由可信上下文传播
InternalBizParamReq<T>服务内部由可信上下文传播

这里没有为“新增用户”“查询订单”“修改密码”分别创建一套系统请求基类。业务变化由泛型参数 T 表达,协议差异由稳定的请求父类表达。

三、ExternalReq 只保存外部协议字段

ExternalReq 是所有公网请求的基础类型,核心字段如下:

public class ExternalReq implements Pojo {
    private String apiVersion;
    private String appId;
    private String encryptData;
    private String plaintext;
    private long timestamp;
    private String sign;
}

需要登录时,ExternalLoginReq 只增加一个字段:

public class ExternalLoginReq extends ExternalReq {
    private String userToken;
}

这样做有两个直接好处。

第一,验签、解密和调用方认证处理器只需要识别稳定的基类,不必了解每一种业务 DTO。

第二,不需要登录的接口在类型上就没有 userToken。是否登录不再依赖开发者“记得加一个注解或字段”,而是直接体现在方法签名中。

四、为什么 bizParam 不由外部调用方直接上传

带业务参数的外部类型虽然声明了泛型字段:

public class ExternalLoginBizParamReq<T extends Param>
        extends ExternalLoginReq {
    private T bizParam;
}

但源码中的 Schema 已经明确说明:bizParam 只用于接口文档展示,调用方不应直接上送。

调用方真正上传的是二选一的内容:

  • 不加密时上传 plaintext
  • 加密时上传 encryptData

这样,请求可以按照固定顺序处理:

外部系统字段校验
  → 调用方认证与签名检查
  → 登录 Token 校验(登录接口)
  → 密文解密为 plaintext
  → JSON 合法性检查
  → 反序列化为具体 Param

业务参数的类型仍由 T extends Param 明确表达,Swagger 也能展示具体结构;外部协议却不需要把明文对象和密文对象混在同一份业务 DTO 中。

五、网关不是原样转发,而是完成协议降噪

以用户登录接口为例,网关接收的是:

ExternalBizParamReq<LoginParam>

转发前通过 WebUtil.genInternalReq 转换:

.param(WebUtil.genInternalReq(req, LoginParam.class))

转换方法读取已经校验或解密后的 plaintext,检查它是否为合法 JSON 对象,然后反序列化成具体业务参数:

Param param = FastJson.json2Obj(plaintext, bizParamClass);
internalBizParamReq.setBizParam(param);

后端服务最终接收的则是:

InternalBizParamReq<LoginParam>

这一步的价值可以概括为“协议降噪”:

  • 网关处理公网协议和安全字段;
  • 后端 API 接收已经转换好的业务对象;
  • Service 和 DAO 不认识签名、密文与 Token;
  • 外部协议将来调整时,不需要污染每一层业务代码。

六、provider 和 consumer 为什么不放进业务 DTO

InternalReq 只包含两个系统字段:

public class InternalReq implements Pojo {
    private String provider;
    private String consumer;
}

它们由内部调用框架填充,而不是由业务方手工传入。

与此同时,TraceId、登录用户 ID、AppId、Seata XID 等调用上下文通过请求头传播,而不是塞进 bizParam

于是内部请求形成了三层数据边界:

数据类型传播位置典型内容
服务身份InternalReqprovider、consumer
调用上下文HTTP Header / ThreadContextTraceId、用户 ID、AppId、XID
业务数据bizParamLoginParam、UserIdParam 等

这种分离能够避免一个常见问题:为了传递链路信息,不断给所有业务 DTO 增加与业务无关的字段。

七、类型统一不等于把所有请求做成一个万能类

统一请求对象最容易走向另一个极端:创建一个字段很多、几乎所有字段都可空的 CommonRequest<T>

这种做法表面上只有一个类型,实际上把判断成本推给了运行时:

  • 当前接口究竟需不需要 Token?
  • plaintextencryptData 谁有效?
  • 这是外部调用还是内部调用?
  • provider 是否允许调用方自己填写?

MetaLite 的选择不是“一个类统一全部”,而是用少量稳定类型把不合法组合尽量排除在方法签名之外。

当然,类型也不能无限增加。只有能够改变认证、转换或传播行为的差异,才值得进入协议类型;普通业务差异仍然留在 Param 中。

八、这套设计解决的不是少写几个字段

6 种请求类型真正解决的是职责边界:

  • 外部调用方只理解外部协议;
  • 网关负责安全治理与协议转换;
  • 内部调用框架填充服务身份和上下文;
  • 后端业务代码只处理强类型参数。

接口数量增长以后,系统字段不会复制进每一个 DTO,安全逻辑也不会散落到每一个 Controller。

下一篇可以继续沿着这条调用链,分析网关为什么选择显式 API 和显式 genInternalReq,而不是把所有请求做成一个完全透明的通用转发器。


框架简介:元界 MetaLite — 下一代企业级 Java 微服务技术底座

作者简介:基于 Spring 体系 15 年企业级开发经验,专注于通过企业级生产环境落地的工程思维和架构思想打造下一代Java 微服务技术底座

完整文档与源码:Gitee 搜索 MetaLite gitee.com/MetaLite