一、背景:直接在业务里调 SDK 的两种死法
我们的多租户小程序 SaaS,对接的第三方接口不止一套:微信开放平台的代开发与提审、微信支付服务商模式的下单与回调,外加短信、实名认证若干。早期图快,SDK 灌进 classpath,业务代码里直接调:下单服务里 new 支付请求,回调处理里解析对方返回体,错误处理散在十几处 switch-case 里。
第一种死法:对方一改版,你全量回归。某个周四 21 点,微信支付侧一次字段调整,回调返回体里一个字段类型变了,我们代码里 31 处直接引用,编译期毫无动静,运行期半个支付域瘫痪。之后两天,250 个测试类、1241 个测试方法陪着一遍全量回归——改动明明是第三方的,买单的是整个仓库。
第二种死法:想换、想测,只能硬改代码。本地联调依赖真实微信接口,沙箱时好时坏;想 mock 一个"支付中"状态,只能注释掉真实调用,再补一行假的。有段时期,仓库里躺着十七八处 // 测试用,上线前删掉,其中一处真忘了删,上了生产。同事那句话我记到现在:"这种注释是定时炸弹,区别只是哪天响。"
两条路通向同一个结论:第三方 SDK 不配直接进业务代码,中间必须垫一层——适配层。
二、适配层的三个设计点
1. 接口归一:业务只依赖自己的端口。 核心是依赖倒置:不是业务迁就第三方的接口,而是业务定义自己的端口(Port),让第三方来实现。我们按业务域定义 PaymentPort、WxOpenPort、SmsPort,端口方法的入参出参全是自有模型。第三方 SDK 被关进适配器(Adapter)里,业务包里禁止出现任何第三方 SDK 的 import——这条不是口头约定,用 ArchUnit 写成测试,越界直接红。
2. 错误翻译:对方改错误码,业务无感。 第三方的错误码是对方的语言,不是业务的。适配层把 SYSTEMERROR 翻成"可重试的内部错误",把 ORDERPAID 翻成"订单已支付",把验签失败翻成"配置异常,触发告警",业务代码只 catch 自己的统一异常。后来微信侧真调整过一回错误码枚举,我们只改了一张翻译表,共 19 行,业务代码零改动——对方的变化被挡在一米宽的地方,而不是打穿一米深的地方。
3. 模式切换:MOCK/REAL 是同一端口的两个实现。 本地与测试环境走 MockPaymentAdapter:下单永远成功,回调可注入任意状态;生产走 WxPayAdapter。切换在配置层完成,一个配置项决定装配哪个实现,业务代码从头到尾不知道自己连的是真微信还是假微信。沙箱再抖,也抖不到开发进度上。
三、版本与兼容:对方升级的灰度路径
第三方升级躲不掉,适配层的价值是让升级变成加法而不是手术。路径三步:新旧适配器并存——新版本实现为 WxPayAdapterV2,与 V1 同时注册在同一端口下;按配置切流——灰度配置按商户维度把流量从 V1 导向 V2,先切自家测试商户,再切 5%,最后全量;观察期回退——每个梯度观察 48 小时,指标异常就把配置拨回,一分钟内生效,不动代码不发版。适配器版本进台账,出问题 10 分钟内查得到哪家商户走在哪个版本上。
四、脱敏伪代码:端口 + 两个适配器
// 端口:业务自己定义,只说业务语言(伪代码,非真实实现)
public interface PaymentPort {
PayResult createOrder(PayCommand cmd); // 下单
RefundResult refund(RefundCommand cmd); // 退款
}
// 统一业务异常:第三方错误码的"译文"
public sealed interface PayException permits Retryable, AlreadyPaid, ConfigError {}
// 真实适配器:唯一允许出现第三方 SDK 的地方
public class WxPayAdapter implements PaymentPort {
public PayResult createOrder(PayCommand cmd) {
try {
var resp = wxSdk.pay(toWxRequest(cmd)); // 翻译:自有模型→第三方模型
return toBizResult(resp); // 翻译:第三方返回→自有模型
} catch (WxErrorCode e) {
throw translate(e); // 错误翻译表:SYSTEMERROR→Retryable 等
}
}
}
// MOCK 适配器:同一端口,行为可注入
public class MockPaymentAdapter implements PaymentPort {
public PayResult createOrder(PayCommand cmd) {
return PayResult.state(injectedState); // 想测"支付中"就注入"支付中"
}
}
# 配置层决定装配谁(占位,非真实环境)
pay:
provider: real # real | mock,同一端口选实现
adapterVersion: v2 # 新旧适配器切流开关
grayPercent: 5 # 灰度比例,异常拨回 0 即回退
五、踩坑清单
坑 1:SDK 的传递依赖污染。 现象:引入某支付 SDK 后服务起不来,日志里 NoSuchMethodError,指向 httpclient 的一个老方法。定位:SDK 传递依赖带进与其他组件冲突的旧版本包,编译期相安无事,运行期才爆。修法:适配层单独成模块,传递依赖显式排除、版本统一锁进 dependencyManagement;新 SDK 合入前先跑一遍依赖树过筛。
坑 2:超时与重试写死在代码里。 现象:高峰期第三方接口整体变慢,硬编码的 3 秒超时把一批本可成功的请求判死,无差别重试又反过来放大了对方压力。定位:超时、重试次数、退避间隔全是常量,改一次要发一次版。修法:三样全部进配置中心,可在线调整;重试只挂在"可重试异常"上,非幂等接口默认不重试。
坑 3:证书轮换打穿适配层。 现象:某个凌晨两点,支付回调验签批量失败,告警把值班手机震醒。定位:平台侧验签公钥按期轮换,适配器启动时加载的旧公钥验不过新签名——代码没改、机器没动,它自己挂了,值班同学早上补了句:"这种最冤,也最好防。"修法:验签改为定时拉取公钥列表、按编号匹配,轮换变成例行公事;证书与密钥内容永不落代码、永不进仓库。
六、三句话总结
- 业务只依赖自己定义的端口,第三方 SDK 是可替换的适配器——换三方、升版本,架构一行不动;
- 错误码、超时、重试在适配层一次性翻译和收敛,业务不感知对方的任何改版;
- MOCK 与 REAL 是同一端口的两个实现,配置切换、灰度切流、随时回退——第三方再怎么变,主动权在自己手里。
运营主体:北京位元跃迁科技有限公司。本文同步发布于本号技术专栏,可搬运至 CSDN/掘金。
