接口参数为什么越写越乱?6 种请求类型统一南北与东西流量
摘要: MetaLite 按“公网/内网、是否登录、是否携带业务参数”组合出 6 种请求类型。本文结合源码说明系统字段、业务参数和调用上下文为什么需要分层,以及网关如何把外部协议转换成内部强类型请求。
同一个业务接口,从前端调用、第三方开放平台调用和微服务内部调用,看起来都只是一个 HTTP 请求。
但它们承担的信任边界完全不同。
公网请求需要识别调用方,可能要验签、解密和校验登录状态;内部请求更关心服务提供者、消费者和调用上下文。如果把这些字段全部塞进每个业务 DTO,接口越多,参数就越容易失控。
MetaLite 没有为每个接口临时拼装系统字段,而是按照三个维度组合出 6 种请求类型:
- 流量来自公网,还是服务内部;
- 是否需要用户登录;
- 是否携带业务参数。
这篇文章只讨论请求建模和协议转换,不重复展开签名算法。
一、先区分南北流量与东西流量
在微服务架构中,通常把外部调用系统的流量称为南北流量,把服务之间的调用称为东西流量。
两类请求虽然都可以使用 HTTP,却不应该共享同一个协议对象。
公网请求面对的是不可信或半可信调用方,需要携带:
- API 版本;
- 调用方
appId; - 明文或密文业务数据;
- 时间戳;
- 签名;
- 需要登录时使用的
userToken。
内部请求已经越过外部协议治理边界,更适合表达:
- 服务提供者
provider; - 服务消费者
consumer; - 已经转换完成的强类型业务参数。
如果后端业务服务仍然直接接收 appId、sign、encryptData 和 userToken,说明外部协议已经侵入了业务边界。
二、三个维度为什么最终是 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。
于是内部请求形成了三层数据边界:
| 数据类型 | 传播位置 | 典型内容 |
|---|---|---|
| 服务身份 | InternalReq | provider、consumer |
| 调用上下文 | HTTP Header / ThreadContext | TraceId、用户 ID、AppId、XID |
| 业务数据 | bizParam | LoginParam、UserIdParam 等 |
这种分离能够避免一个常见问题:为了传递链路信息,不断给所有业务 DTO 增加与业务无关的字段。
七、类型统一不等于把所有请求做成一个万能类
统一请求对象最容易走向另一个极端:创建一个字段很多、几乎所有字段都可空的 CommonRequest<T>。
这种做法表面上只有一个类型,实际上把判断成本推给了运行时:
- 当前接口究竟需不需要 Token?
plaintext和encryptData谁有效?- 这是外部调用还是内部调用?
provider是否允许调用方自己填写?
MetaLite 的选择不是“一个类统一全部”,而是用少量稳定类型把不合法组合尽量排除在方法签名之外。
当然,类型也不能无限增加。只有能够改变认证、转换或传播行为的差异,才值得进入协议类型;普通业务差异仍然留在 Param 中。
八、这套设计解决的不是少写几个字段
6 种请求类型真正解决的是职责边界:
- 外部调用方只理解外部协议;
- 网关负责安全治理与协议转换;
- 内部调用框架填充服务身份和上下文;
- 后端业务代码只处理强类型参数。
接口数量增长以后,系统字段不会复制进每一个 DTO,安全逻辑也不会散落到每一个 Controller。
下一篇可以继续沿着这条调用链,分析网关为什么选择显式 API 和显式 genInternalReq,而不是把所有请求做成一个完全透明的通用转发器。
框架简介:元界 MetaLite — 下一代企业级 Java 微服务技术底座
作者简介:基于 Spring 体系 15 年企业级开发经验,专注于通过企业级生产环境落地的工程思维和架构思想打造下一代Java 微服务技术底座
完整文档与源码:Gitee 搜索 MetaLite gitee.com/MetaLite