23年入职现在这家公司,到岗后我做的第一件事不是接业务需求,而是先搭了快速开发框架和模版微服务工程,然后紧接着搭了这个出口入口网关微服务。
这个决定跟之前的经历有关。待过几家公司,每家都要对接不少第三方系统,钉钉、企业微信、金蝶、各种外卖平台、电子签章,零零散散加起来十几个。对接代码散落在各个微服务里,A服务对接了钉钉的用户接口,B服务也要用,又对接了一遍。接口一改,两边都得改。新人来了想找对接钉钉的代码,得翻好几个工程才能找全。
这个问题在中小型团队里特别明显。大厂有专门的开放平台团队来管这些事,中小型公司的IT部门没有这个编制,对接第三方的活是各个业务组自己干。干着干着,同一个第三方的对接代码就散落在了三四个微服务里。
所以入职后我判断这个网关是刚需,先把它搭起来,后面所有对接第三方的需求都走这个网关。事实证明这个决定是对的。网关上线到现在,除了有新的第三方系统要对接,几乎不用改代码。有时候几个月都不需要发版。
下面把这个网关的核心设计和关键代码都贴出来,可以直接拿去参考。
注意:文章涉及到策略模式、责任链模式、Dubbo泛化调用、Nacos配置监听这几个技术点,如果对这些概念不太熟,建议先了解一下再来看,不然部分代码可能会比较吃力。
整体概览
这个网关做两件事:
出口:内部微服务需要调用第三方系统的接口时,不直接调,而是通过网关去调。网关负责处理认证、签名、参数转换这些和第三方对接相关的脏活。
入口:第三方系统有回调通知时(比如钉钉审批状态变更回调、支付结果回调),统一由网关接收,再转发给内部对应的微服务处理。
工程上分成两个模块:
- api模块:定义了RPC接口和请求对象,打成jar包给其他微服务引入。其他服务引入这个jar后,就能通过Dubbo RPC调用网关的接口,不需要关心网关内部怎么实现。
- server模块:网关的核心实现,包含策略模式、责任链、Nacos配置管理、请求日志等所有逻辑。

没有统一网关时的问题
这不是一个理论推导出来的问题,是实际踩过的坑。
同一个第三方接口被对接了多次。 比如钉钉的获取用户信息接口,审批服务对接了一次,人事服务也对接了一次。两份代码逻辑差不多,但token的获取方式、异常处理的写法各有各的风格。钉钉接口升级时,得找到所有对接过的地方逐个改。
对接代码和业务代码混在一起。 调用第三方接口的HTTP请求构造、签名逻辑、token刷新这些代码,直接写在业务Service里。业务逻辑和对接逻辑搅在一起,改业务可能不小心碰到对接代码,改对接代码又怕影响业务。
回调接口没有统一管理。 每个服务各自暴露回调接口,URL格式不统一,安全校验(IP白名单、签名验证)各写各的。
做了统一网关之后,这些问题都不存在了。对接第三方这件事,做一次和做十次的成本应该是一样的。 第一次对接钉钉时在网关里写好策略类,后面任何服务想用钉钉的接口,引入SDK调一下网关的RPC接口就行,不用再写一行对接代码。
策略模式对接不同第三方
网关需要对接十几个第三方系统,每个系统的认证方式、接口协议、参数格式都不一样。钉钉用的是OAuth加AccessToken,金蝶是WebAPI加签名,有些系统直接是带AppKey的HTTP请求。
用策略模式来处理这个差异。定义一个连接策略接口,每个第三方系统对应一个策略实现类:
Java
public interface ConnectStrategy {
// 连接第三方系统,执行具体的API调用
Object connect(RequestContext context);
}
钉钉的策略类大概长这样(以OA审批为例):
Java
// routeKey用于路由,网关根据请求中的外部系统标识自动选择对应的策略类
@StrategyRoute(routeKey = "dingtalk")
public class DingTalkConnectStrategy implements ConnectStrategy {
private final DingTalkProperties dingTalkProperties;
@Override
public Object connect(RequestContext context) {
RequestBizData bizData = context.getRequestDTO().getRequestBizData();
Object data = bizData.getData();
JSONObject json = parseRequestData(data);
// 根据configId区分具体调用哪个钉钉接口
if ("createApproval".equals(bizData.getConfigId())) {
return createWorkFlow(json, context);
}
if ("getToken".equals(bizData.getConfigId())) {
return getAccessToken(json, context);
}
if ("getUserInfo".equals(bizData.getConfigId())) {
return getUserInfo(json, context);
}
// 如果configId不在已知列表里,回退到通用HTTP策略
if (isCommonRequest(context)) {
return commonStrategy.connect(context);
}
return context.getResponseResult();
}
}
这里有个细节值得说一下。@StrategyRoute是一个自定义注解,网关启动时会扫描所有带这个注解的类,按routeKey建立映射关系。请求进来后,根据请求中携带的externalSystem字段,自动路由到对应的策略类。不需要写if-else来手动选择策略。
启动类上加一个注解就能开启这个能力:
Java
@SpringBootApplication
// 扫描策略包,自动建立策略路由映射
@EnableStrategyRoute(scanBasePackages = "com.demo.gateway.server.core.chain.strategy")
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
还有一个通用HTTP策略类,它是默认策略。如果某个第三方系统的接口就是标准的HTTP调用,没有复杂的认证和签名逻辑,不需要专门写策略类,直接在Nacos里配好URL和请求方式就能用:
Java
// isDefault = true 表示这是默认策略,没有匹配到专用策略时走这个
@StrategyRoute(routeKey = "common", isDefault = true, interfaceClass = ConnectStrategy.class)
public class CommonHttpConnectStrategy implements ConnectStrategy {
@Override
public Object connect(RequestContext context) {
// 从配置中读取URL、请求方式
// 构建OkHttp请求
// 发起调用并返回结果
}
}
新增一个第三方系统的对接,只需要做一件事:写一个新的策略类,加上 **@StrategyRoute**注解。 不需要改网关的任何已有代码。如果这个第三方系统只用标准HTTP调用,连策略类都不用写,配Nacos就行。

责任链模式处理请求
不管是出口请求还是入口请求,都需要经过一系列处理步骤:参数校验、配置加载、授权、调用、日志记录。把这些步骤拆成独立的节点,用责任链串起来。
上行链路(内部微服务 → 网关 → 第三方系统):
Java
@Component
public class UpwardRequestNodeChain extends AbstractNodeHandlerChain {
private final ParamCheckNode paramCheckNode;
private final ParamHandleNode paramHandleNode;
private final RequestAuthorizeNode requestAuthorizeNode;
private final RequestConnectNode requestConnectNode;
private final RequestTraceLogNode requestTraceLogNode;
@Override
protected List<NodeHandler> defineHandlerChain() {
return List.of(
paramCheckNode,
paramHandleNode,
requestAuthorizeNode,
requestConnectNode,
requestTraceLogNode
);
}
}
五个节点各管各的事:
| 节点 | 职责 |
|---|---|
| ParamCheckNode | 校验必填参数:externalSystem、configId、data,缺一个就拦住 |
| ParamHandleNode | 根据externalSystem和configId从Nacos加载映射配置,绑定到上下文 |
| RequestAuthorizeNode | 执行授权策略,比如获取AccessToken |
| RequestConnectNode | 调用对应的连接策略,真正发起对第三方的请求 |
| RequestTraceLogNode | 记录请求日志,包括请求参数、响应结果、耗时 |
下行链路(第三方系统回调 → 网关 → 内部微服务):
Java
@Component
public class DownwardRequestNodeChain extends AbstractNodeHandlerChain<AcceptContext, AcceptContext> {
private final AdmittanceCheckNode admittanceCheckNode;
private final RpcDispatchNode rpcDispatchNode;
@Override
protected List<NodeHandler<AcceptContext, AcceptContext>> defineHandlerChain() {
return List.of(
admittanceCheckNode,
rpcDispatchNode
);
}
}
下行链路只有两个节点:
| 节点 | 职责 |
|---|---|
| AdmittanceCheckNode | 准入校验,检查请求来源IP是否在白名单内,黑名单直接拒绝 |
| RpcDispatchNode | 用Dubbo泛化调用,把回调请求转发给内部对应的微服务 |
RpcDispatchNode是下行链路的核心。它用了Dubbo的泛化调用,不需要在网关里引入目标服务的接口依赖。调用哪个服务、哪个方法、传什么参数,全部从Nacos配置里读。这意味着新增一个回调接收场景,只需要在Nacos里配一条路由规则,网关代码不用动。
Java
@Component
public class RpcDispatchNode extends AbstractNodeHandler<AcceptContext, AcceptContext> {
@Override
protected AcceptContext doHandle(ChainContext<AcceptContext> context) {
AcceptContext acceptContext = context.getParam();
GatewayMappingProperties props = acceptContext.getGatewayMappingProperties();
RpcConfig rpcConfig = props.getRpcConfig();
// 通过Dubbo泛化调用,不需要引入目标服务的接口依赖
GenericService svc = getReferenceService(
rpcConfig.getServiceInterface(),
rpcConfig.getGroup(),
rpcConfig.getVersion(),
rpcConfig.getTimeout(),
rpcConfig.getRetries()
);
// 执行调用
Object result = svc.$invoke(
rpcConfig.getMethodName(),
parameterTypes,
args
);
acceptContext.setRpcResult(result);
return acceptContext;
}
}
责任链的好处在于每个节点职责单一,改一个节点不影响其他节点。后面想加个限流节点,往链路里插一个就行。

Nacos配置中心实现动态路由
这是整个网关设计里最实用的一个点。对接第三方时,经常遇到这种情况:钉钉的OA审批接口已经对接好了,现在要加一个钉钉的用户查询接口。如果每加一个接口都要改代码、发版,那网关的价值就打了折扣。
网关的路由配置全部放在Nacos里,分成两层:
第一层:配置列表。 一个JSON数组,列出所有具体的配置文件ID。
Nacos的dataId是GatewayRouteConfigList,内容:
JSON
["dingtalk_config", "kingdee_config", "eleme_config", "meituan_config"]
第二层:具体的映射配置。 每个配置文件ID对应一组路由规则。
比如dingtalk_config的内容:
JSON
{
"DingTalk_createApproval": {
"url": "https://oapi.dingtalk.com/workflow/instance/create",
"appName": "approval-service",
"async": false,
"timeout": 30000,
"rpcConfig": {
"serviceInterface": "com.demo.approval.api.WorkflowService",
"methodName": "onApprovalCallback",
"parameterTypes": ["java.lang.String"],
"group": "default",
"version": "1.0.0",
"timeout": 5000,
"retries": 2
}
},
"DingTalk_getUserInfo": {
"url": "https://oapi.dingtalk.com/topapi/v2/user/get",
"appName": "user-service",
"async": false,
"timeout": 15000
}
}
配置的key格式是{外部系统}_{接口标识},value里包含了调用URL、超时时间、是否异步、回调时转发的RPC配置等信息。
网关启动时,从Nacos加载这些配置并注册监听:
Java
@Component
public class GatewayMappingConfig implements ApplicationContextAware {
// 用ConcurrentHashMap存储所有映射配置,线程安全
private final Map<String, GatewayMappingProperties> configMap = Maps.newConcurrentMap();
private final Map<String, Listener> listenerMap = Maps.newConcurrentMap();
@Override
public void setApplicationContext(ApplicationContext ctx) {
NacosConfigManager nacosConfigManager = ctx.getBean(NacosConfigManager.class);
// 加载配置列表
List<MappingConfigInfo> configList = loadConfigList(nacosConfigManager);
// 对每个配置文件添加监听
batchListenConfig(configList, nacosConfigManager);
}
// Nacos配置变更时的回调
private void applyConfig(String configInfo) {
JSONObject configJson = JSON.parseObject(configInfo);
configJson.keySet().forEach(key -> {
// 解析配置并存入内存Map
GatewayMappingProperties properties = parseProperties(key, configJson);
configMap.put(key, properties);
});
}
}
关键在于configMap是ConcurrentHashMap。Nacos推送配置变更时,回调方法更新Map里的值,后续请求读到的就是新配置。整个过程不需要重启服务。
举个实际场景。 钉钉的OA审批接口已经对接好了,跑了半年没问题。现在产品说要加一个钉钉的部门查询接口。我需要做什么?
在Nacos的dingtalk_config里加一条配置:
JSON
{
"DingTalk_getDeptInfo": {
"url": "https://oapi.dingtalk.com/topapi/v2/department/get",
"appName": "hr-service",
"async": false,
"timeout": 15000
}
}
保存。网关不需要重启,不需要发版。业务服务通过SDK调用时,传入externalSystem=DingTalk和configId=getDeptInfo,网关就能找到这条配置并发起调用。
如果是接收回调的场景,配置里加上rpcConfig,指定转发到哪个服务的哪个方法就行。
配置属性的完整结构:
| 属性 | 类型 | 说明 |
|---|---|---|
| url | String | 第三方接口地址 |
| appName | String | 关联的内部服务名 |
| async | Boolean | 是否异步处理,异步时立即返回成功标识 |
| timeout | Long | 连接超时时间,单位毫秒 |
| useUrlParam | Boolean | 是否使用URL参数 |
| ignoreTraceLog | Boolean | 是否跳过日志记录 |
| successFlag | Object | 成功时的返回格式 |
| failFlag | Object | 失败时的返回格式 |
| rpcConfig | Object | 回调转发的Dubbo配置:接口名、方法名、参数类型、分组、版本、超时、重试 |
| extParam | Map | 扩展参数,用于策略类读取自定义配置 |
SDK让其他微服务一行代码调网关
api模块就干一件事:定义一个RPC接口和请求对象,打成jar包。
Java
public interface GatewayFacade {
Result<String> request(GatewayRequestDTO gatewayRequestDTO);
}
请求对象的结构:
Java
@Data
public class GatewayRequestDTO implements Serializable {
// 业务数据
private RequestBizData requestBizData;
// 自定义请求头
private List<RequestHeader> requestHeaders;
// 请求路径(可选,某些场景需要指定)
private String url;
// 请求方法,默认POST
private String method;
@Data
public static class RequestBizData implements Serializable {
// 外部系统标识,比如DingTalk、Kingdee
private String externalSystem;
// 配置ID,比如createApproval、getUserInfo
private String configId;
// 请求数据
private Object data;
// 业务ID(可选,用于日志追踪)
private String bizId;
// 业务类型(可选)
private String bizType;
}
}
其他微服务引入SDK后的使用方式:
Java
@Service
public class ApprovalService {
// 引入SDK后,通过Dubbo注入网关接口
@DubboReference(version = "1.0.0")
private GatewayFacade gatewayFacade;
public String createDingTalkApproval(ApprovalParam param) {
GatewayRequestDTO request = new GatewayRequestDTO();
RequestBizData bizData = new RequestBizData();
// 指定外部系统和接口标识
bizData.setExternalSystem("DingTalk");
bizData.setConfigId("createApproval");
bizData.setData(param);
request.setRequestBizData(bizData);
Result<String> result = gatewayFacade.request(request);
return result.getData();
}
}
调用方不需要关心钉钉的token怎么获取、接口签名怎么算、HTTP请求怎么构造。传入外部系统名称、接口标识、业务数据,剩下的事情网关全包了。
入口方向,网关暴露了一个统一的HTTP接口来接收第三方回调:
Java
@RestController
@RequestMapping("openapi")
public class UnifiedAcceptController {
@RequestMapping("accept/{externalSystem}/{configKey}")
public Object accept(
@PathVariable("externalSystem") String externalSystem,
@PathVariable("configKey") String configKey) {
// 根据路径参数构建上下文
AcceptContext ctx = new AcceptContext(getMappingKey(externalSystem, configKey));
// 解析请求参数
settingRequestParam(ctx);
// 走下行责任链处理
return gatewayService.accept(ctx);
}
}
回调URL的格式是/openapi/accept/{外部系统}/{配置标识}。给钉钉配回调地址时填https://你的域名/openapi/accept/DingTalk/approvalCallback,网关收到请求后根据路径参数找到Nacos里的路由配置,通过Dubbo泛化调用转发给内部服务。
网关服务层还做了同步和异步两种处理模式。如果Nacos配置里async=true,网关收到回调后立即返回成功标识给第三方(避免第三方超时重试),然后异步转发给内部服务:
Java
@Service
public class GatewayServiceImpl implements GatewayService {
private final Executor executor = new ThreadPoolExecutor(
10, 20, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5000));
@Override
public Object accept(AcceptContext ctx) {
bindGatewayMappingConfig(ctx);
GatewayMappingProperties props = ctx.getGatewayMappingProperties();
Long traceLogId = saveLog(ctx);
if (Boolean.TRUE.equals(props.getAsync())) {
// 异步处理:立即返回,后台线程执行
CompletableFuture.supplyAsync(
() -> doInvokeChain(traceLogId, ctx), executor);
return props.extractReturnFlagMsg("success");
} else {
// 同步处理:等待执行完成后返回
return doInvokeChain(traceLogId, ctx);
}
}
}
对接新系统的标准流程
以对接一个新的第三方系统为例(假设叫「某签章平台」),从0到上线的步骤:
如果这个系统需要特殊的认证或签名逻辑:
- 在策略包下新建
SignPlatformConnectStrategy类,实现ConnectStrategy接口 - 加上
@StrategyRoute(routeKey = "signPlatform")注解 - 在connect方法里实现认证、签名、接口调用的逻辑
- 如果有独立的配置项(如AppKey),在Nacos的
ThirdPartyAppConfig里加上对应的配置 - 在Nacos的
GatewayRouteConfigList里加上新的配置文件ID - 在新的配置文件里写好路由规则
- 网关发版(因为加了新的策略类)
如果这个系统只需要标准HTTP调用:
- 在Nacos的
GatewayRouteConfigList里加上新的配置文件ID - 在新的配置文件里写好路由规则(URL、请求方式、超时等)
- 不需要写代码,不需要网关发版
业务服务如何使用:
- pom.xml里引入网关的api模块依赖
- 用
@DubboReference注入GatewayFacade - 构建GatewayRequestDTO,传入externalSystem和configId
- 调用
gatewayFacade.request(dto)
如果需要接收回调:
- 在Nacos的路由配置里,给对应的configKey配上rpcConfig(指定转发到哪个服务的哪个方法)
- 把回调URL
https://域名/openapi/accept/signPlatform/{configKey}配到第三方平台 - 如果需要IP白名单,在Nacos的准入配置里加上对方的IP
小结
做了这么多年开发,我越来越觉得,中小型团队做技术选型,最重要的不是技术有多先进,而是维护成本有多低。一个技术方案如果需要频繁发版、频繁调试、频繁修改,那它再优雅也没用。真正好用的方案是:搭一次,跑很久,偶尔加点配置就能适应新需求。
这个网关的技术含量不高,策略模式、责任链、Nacos配置监听,都是常见的设计模式和中间件用法。它的价值不在于技术有多深,而在于把对接第三方这件零碎的事情收拢到了一个地方,用配置化的方式降低了后续的维护成本。
有人可能会说,对接的第三方不多的话,有必要单独搭一个网关服务吗?我的判断标准是:如果你的团队有3个以上的微服务,并且已经对接了或者即将对接2个以上的第三方系统,这个网关就值得搭。前期投入两三天,后面省的时间远不止这些。
如果当前只有1个外部系统要对接,而且短期内看不到增长趋势,那直接在业务服务里写对接代码就行,不用过度设计。
希望这篇内容可以帮到你。
最近在知乎出了秒杀专栏,感兴趣的可以订阅一下。至于知识星球的,可以搜:
- 老码头的技术浮生录
它是一个能实际帮你解决难题的星球。有问题的,找知心的Sam哥,支持无限次语音一对一解决你遇到的难题。另外后续我新写的所有对外的付费专栏,在星球内都是免费的,且可以拿到所有源代码。
我的知乎账号:
- SamDeepThinking