我做的是一个 B2B 数据聚合平台,需要对接支付(微信/支付宝/Stripe)、短信(阿里云/腾讯云)、物流(顺丰/圆通/菜鸟)、地图(高德/百度)、企业信息(天眼查/企查查)等多类第三方 API。刚开始只有两三个时,每个服务写一个
HttpClient + 硬编码调用就行。
到了第 15 个接口时,问题集中爆发了:
每个第三方 API 的鉴权方式都不一样(Header Token / Query Sign / OAuth 2.0 / AK/SK 签名)
返回格式五花八门,有的是
{"code":0}、有的是
{"status":"success"}��有的是
{"ret":200}
错误处理逻辑散落在各个 Service 里,改一个重试策略要改十几处
线上排查问题时,根本不知道是哪一层的错误,日志东一块西一块
新增一个第三方的成本极高,光是 “搭架子” 就要写几百行重复代码
经过几轮重构,最终沉淀出一套
「适配器 + 管道 + 策略」 的通用封装架构,新增一个第三方 API 接入从 2 天缩短到了 2 小时。
先上整体架构,心里有数再看代码:
┌──────────────────────────────────────────────────────────────┐ │ Business Service │ │ (只依赖 IThirdPartyApiClient 接口,不知道任何实现) │ └───────────────────────────┬──────────────────────────────────┘ │ ┌───────────────────────────▼──────────────────────────────────┐ │ ThirdPartyApiClient │ │ ┌─────────────┐ ┌──────────────┐ ┌───────────────────────┐ │ │ │ 鉴权管道 │ │ 重试管道 │ │ 熔断管道 │ │ │ │ AuthPipe │ │ RetryPipe │ │ CircuitBreakerPipe │ │ │ └──────┬──────┘ └──────┬───────┘ └──────────┬────────────┘ │ │ │ │ │ │ │ ┌──────▼───────────────▼────────────────────▼────────────┐ │ │ │ Adapter Layer (适配器层) │ │ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │ │ │ 天眼查 │ │ 支付宝 │ │ 高德地图 │ ... │ │ │ │ │ Adapter │ │ Adapter │ │ Adapter │ │ │ │ │ └───────────┘ └───────────┘ └───────────┘ │ │ │ └────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘
核心设计原则:
业务层只靠接口编程,永远不直接依赖任何三方 SDK
管道模式处理横切关注点(鉴权、重试、熔断、日志),与业务逻辑解耦
适配器负责协议转换,把五花八门的 API 统一成内部标准
首先定义一套内部标准协议,所有适配器都必须适配到这个模型:
// 统一请求模型
@Data
@Builder
public class ApiRequest {
private String method; // GET / POST / PUT / DELETE
private String path; // 接口路径,如 /v1/company/search
private Map headers;
private Map queryParams;
private String body; // JSON 字符串
private long timeoutMs; // 超时时间(毫秒)
}
// 统一响应模型
@Data
@Builder
public class ApiResponse {
private int httpStatus; // HTTP 状态码
private String rawBody; // 原始响应体
private boolean success; // 业务是否成功
private String errorCode; // 统一错误码
private String errorMessage; // 统一错误信息
private long latencyMs; // 耗时(毫秒)
private String traceId; // 链路追踪 ID
}
/**
* 第三方 API 适配器标准接口 —— 所有第三方接入的唯一入口
*/
public interface ThirdPartyAdapter {
/** 获取适配器名称,如 "TianYanCha" */
String getName();
/** 构建鉴权信息(插入到请求头/查询参数) */
void authenticate(ApiRequest request);
/** 将内部统一请求转换为第三方特有的 HTTP 请求 */
HttpRequest convert(ApiRequest request);
/** 将第三方的原始响应解析为内部统一响应 */
ApiResponse parse(HttpResponse rawResponse);
/** 判断第三方返回是否业务成功 */
boolean isSuccess(ApiResponse response);
/** 获取该 API 的基础配置 */
ApiConfig getConfig();
}
@Component
public class TianYanChaAdapter implements ThirdPartyAdapter {
@Value("${api.tianyancha.app-key}")
private String appKey;
@Value("${api.tianyancha.base-url}")
private String baseUrl;
@Override
public String getName() {
return "TianYanCha";
}
@Override
public void authenticate(ApiRequest request) {
- **// 天眼查签名算法:**token + timestamp 做 MD5
String timestamp = String.valueOf(System.currentTimeMillis());
String sign = DigestUtils.md5Hex(appKey + timestamp + appKey);
request.getHeaders().put("Authorization", sign);
request.getHeaders().put("Timestamp", timestamp);
request.getQueryParams().put("appkey", appKey);
}
@Override
public HttpRequest convert(ApiRequest request) {
String fullUrl = baseUrl + request.getPath();
if (!request.getQueryParams().isEmpty()) {
fullUrl += "?" + buildQueryString(request.getQueryParams());
}
return HttpRequest.newBuilder()
.uri(URI.create(fullUrl))
.timeout(Duration.ofMillis(request.getTimeoutMs()))
.headers(buildHeaders(request.getHeaders()))
.method(request.getMethod(),
request.getBody() != null
? HttpRequest.BodyPublishers.ofString(request.getBody())
: HttpRequest.BodyPublishers.noBody())
.build();
}
@Override
public ApiResponse parse(HttpResponse rawResponse) {
String body = rawResponse.body();
int httpStatus = rawResponse.statusCode();
- **// 天眼查返回格式:**{"error_code":0, "reason":"成功", "result":{...}}
JSONObject json = JSON.parseObject(body);
int errorCode = json.getIntValue("error_code");
return ApiResponse.builder()
.httpStatus(httpStatus)
.rawBody(body)
.success(errorCode == 0)
.errorCode(String.valueOf(errorCode))
.errorMessage(json.getString("reason"))
.build();
}
@Override
public boolean isSuccess(ApiResponse response) {
return response.isSuccess();
}
@Override
public ApiConfig getConfig() {
return ApiConfig.builder()
.baseUrl(baseUrl)
.maxRetries(3)
.retryDelayMs(1000)
.timeoutMs(10000)
.circuitBreakerThreshold(5) // 连续 5 次失败触发熔断
.circuitBreakerRecoveryMs(60_000) // 60 秒后半开尝试恢复
.build();
}
}
@Slf4j
@Component
public class RetryPipeline {
private static final Set RETRYABLE_ERRORS = Set.of(
"CONNECTION_TIMEOUT",
"READ_TIMEOUT",
"SERVER_ERROR_5XX",
"RATE_LIMITED",
"NETWORK_IO_ERROR"
);
/**
* 带指数退避的重试执行
*/
public T executeWithRetry(
ApiConfig config,
Supplier call,
String apiName,
String operation) {
int maxRetries = config.getMaxRetries();
long baseDelay = config.getRetryDelayMs();
Exception lastException = null;
for (int attempt = 0; attempt 2s -> 4s,加随机抖动避免惊群
long delay = baseDelay * (1L stateMap = new ConcurrentHashMap<>();
/**
* 熔断保护执行
*/
public T executeWithBreaker(
ApiConfig config,
String apiKey,
Supplier call) throws CircuitBreakerOpenException {
CircuitState state = stateMap.computeIfAbsent(apiKey,
k -> new CircuitState(config.getCircuitBreakerThreshold()));
// 检查熔断状态
if (state.isOpen()) {
if (state.shouldTryHalfOpen(config.getCircuitBreakerRecoveryMs())) {
log.info("[{}] 熔断器进入半开状态,尝试放行一个请求", apiKey);
state.transitionTo(CircuitStatus.HALF_OPEN);
} else {
log.warn("[{}] 熔断器开启,拒绝请求", apiKey);
throw new CircuitBreakerOpenException(apiKey);
}
}
try {
T result = call.get();
// 成功时重置
if (state.getStatus() == CircuitStatus.HALF_OPEN) {
log.info("[{}] 半开请求成功,熔断器关闭", apiKey);
}
state.reset();
return result;
} catch (Exception e) {
state.recordFailure();
if (state.isThresholdReached()) {
log.error("[{}] 连续失败 {} 次,熔断器开启",
apiKey, state.getFailureCount());
state.transitionTo(CircuitStatus.OPEN);
}
throw e;
}
}
/** 熔断状态枚举 */
enum CircuitStatus { CLOSED, OPEN, HALF_OPEN }
/** 滑动窗口状态 */
static class CircuitState {
private final int threshold;
private final Deque failureTimestamps = new ConcurrentLinkedDeque<>();
private volatile CircuitStatus status = CircuitStatus.CLOSED;
private volatile long openedAt;
CircuitState(int threshold) { this.threshold = threshold; }
boolean isOpen() { return status == CircuitStatus.OPEN; }
void transitionTo(CircuitStatus newStatus) {
this.status = newStatus;
if (newStatus == CircuitStatus.OPEN) {
this.openedAt = System.currentTimeMillis();
}
}
void recordFailure() {
failureTimestamps.addLast(System.currentTimeMillis());
// 只保留 60 秒窗口内的失败
long cutoff = System.currentTimeMillis() - 60_000;
while (!failureTimestamps.isEmpty()
&& failureTimestamps.peekFirst() = threshold; }
boolean shouldTryHalfOpen(long recoveryMs) {
return System.currentTimeMillis() - openedAt > recoveryMs;
}
void reset() {
failureTimestamps.clear();
status = CircuitStatus.CLOSED;
openedAt = 0;
}
CircuitStatus getStatus() { return status; }
}
}
@Slf4j
@Component
public class ThirdPartyApiClient {
private final Map adapterMap;
private final HttpClient httpClient;
private final RetryPipeline retryPipeline;
private final CircuitBreakerPipeline circuitBreaker;
public ThirdPartyApiClient(List adapters,
RetryPipeline retryPipeline,
CircuitBreakerPipeline circuitBreaker) {
// Spring 自动注入所有 ThirdPartyAdapter 实现,按名称索引
this.adapterMap = adapters.stream()
.collect(Collectors.toMap(ThirdPartyAdapter::getName, Function.identity()));
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
this.retryPipeline = retryPipeline;
this.circuitBreaker = circuitBreaker;
}
/**
* 业务层唯一需要调用的方法
*/
public String call(String apiName, String operation, @NonNull ApiRequest request) {
ThirdPartyAdapter adapter = adapterMap.get(apiName);
if (adapter == null) {
throw new IllegalArgumentException("未找到适配器: " + apiName);
}
String apiKey = apiName + ":" + operation;
long startTime = System.currentTimeMillis();
- **// 外层:**熔断保护
return circuitBreaker.executeWithBreaker(adapter.getConfig(), apiKey, () -> {
- **// 内层:**重试保护
return retryPipeline.executeWithRetry(
adapter.getConfig(),
() -> doCall(adapter, request, apiKey, startTime),
apiName,
operation
);
});
}
private String doCall(ThirdPartyAdapter adapter, ApiRequest request,
String apiKey, long startTime) {
// 鉴权
request.setTimeoutMs(adapter.getConfig().getTimeoutMs());
adapter.authenticate(request);
// 发送 HTTP 请求
HttpRequest httpRequest = adapter.convert(request);
HttpResponse httpResponse;
try {
httpResponse = httpClient.send(httpRequest,
HttpResponse.BodyHandlers.ofString());
} catch (Exception e) {
throw new ApiInvokeException(apiKey, "网络请求失败", e);
}
// 解析响应
ApiResponse response = adapter.parse(httpResponse);
response.setLatencyMs(System.currentTimeMillis() - startTime);
response.setTraceId(MDC.get("traceId"));
// 日志埋点
log.info("[{}] status={} success={} latency={}ms errorCode={}",
apiKey, response.getHttpStatus(), response.isSuccess(),
response.getLatencyMs(), response.getErrorCode());
if (!adapter.isSuccess(response)) {
throw new ApiBusinessException(apiKey,
response.getErrorCode(), response.getErrorMessage());
}
return response.getRawBody();
}
}
@Service
public class CompanyService {
@Autowired
private ThirdPartyApiClient apiClient;
/**
* 查询企业信息 —— 业务层完全不知道底层调的是天眼查还是企查查
* 切换供应商只需改一个配置项
*/
public CompanyInfo queryCompany(String companyName) {
ApiRequest request = ApiRequest.builder()
.method("GET")
.path("/v1/company/search")
.queryParams(Map.of("keyword", companyName))
.build();
- **// 一行调用,背后是:**适配器转换 → 鉴权 → 熔断 → 重试 → 解析
String rawJson = apiClient.call("TianYanCha", "searchCompany", request);
return parseToCompanyInfo(rawJson);
}
}
假设现在要接入 高德地图地理编码 API,只需:
@Component
public class AmapGeocodeAdapter implements ThirdPartyAdapter {
@Value("${api.amap.key}")
private String apiKey;
@Value("${api.amap.base-url}")
private String baseUrl;
@Override
public String getName() {
return "Amap";
}
@Override
public void authenticate(ApiRequest request) {
- **// 高德地图:**key 放在 query 参数
request.getQueryParams().put("key", apiKey);
}
@Override
public HttpRequest convert(ApiRequest request) {
// 标准 HTTP 转换(与天眼查几乎一样,可以抽一个 BaseAdapter)
String fullUrl = baseUrl + request.getPath() + "?"
+ buildQueryString(request.getQueryParams());
return HttpRequest.newBuilder()
.uri(URI.create(fullUrl))
.timeout(Duration.ofMillis(request.getTimeoutMs()))
.GET()
.build();
}
@Override
public ApiResponse parse(HttpResponse rawResponse) {
- **// 高德返回格式:**{"status":"1","info":"OK","geocodes":[...]}
JSONObject json = JSON.parseObject(rawResponse.body());
boolean success = "1".equals(json.getString("status"));
return ApiResponse.builder()
.httpStatus(rawResponse.statusCode())
.rawBody(rawResponse.body())
.success(success)
.errorCode(json.getString("infocode"))
.errorMessage(json.getString("info"))
.build();
}
@Override
public boolean isSuccess(ApiResponse response) {
return response.isSuccess();
}
@Override
public ApiConfig getConfig() {
return ApiConfig.builder()
.baseUrl(baseUrl)
.maxRetries(2)
.retryDelayMs(500)
.timeoutMs(5000)
.circuitBreakerThreshold(5)
.circuitBreakerRecoveryMs(30_000)
.build();
}
}
api: amap: key: your_amap_key base-url: https://restapi.amap.com
ApiRequest request = ApiRequest.builder()
.method("GET")
.path("/v3/geocode/geo")
.queryParams(Map.of("address", "北京市朝阳区"))
.build();
String result = apiClient.call("Amap", "geocode", request);
- **// 自动享受:**鉴权注入、指数退避重试、熔断保护、日志埋点
有的 API 返回
HTTP 200 但 body 里是
{"code": 500},有的返回
HTTP 500 但 body 里写"其实是成功了"。
永远在 parse() 里同时检查 HTTP 状态码和业务状态码。
// 这是我在线上查了一下午才找到的 bug 根源
- **// 某短信平台:**发送成功返回 HTTP 200 + {"code": "0"}
// 但你的额度用完时,它也返回 HTTP 200 + {"code": "-10086", "msg": "余额不足"}
// 如果只看 HTTP 200 就认为成功,就会漏掉这个 case
不是所有错误都适合重试。
"余额不足"重试 3 次还是余额不足,但每次都消耗一次 HTTP 请求配额。 参考上面
RetryPipeline 里的
isRetryable() 判断逻辑。
一刀切设 10 秒是在给自己埋坑。我的经验:
|
场景 |
建议超时 |
原因 |
|---|---|---|
|
短信发送 |
15s |
运营商返回慢 |
|
企业信息查询 |
5s |
纯数据库查询 |
|
支付下单 |
10s |
需要留余地 |
|
地图反向地理编码 |
3s |
高频调用,超时就 fast-fail |
所有 Key/Secret 必须走配置中心(Apollo/Nacos),不要在代码里硬编码。 代码仓库里搜到过 AK/SK 的,都不是个例。
// ❌ 绝对不要这样写
private static final String API_KEY = "sk-xxxxxxxxxxxx";
// ✅ 从配置中心注入
@Value("${api.tianyancha.app-key}")
private String appKey;
没有监控的 API 封装就是在裸奔。至少记录这几个指标:
// 关键指标
// 1. 调用量 + 成功率 (方便做告警)
// 2. P50/P99 耗时 (发现某方 API 变慢)
// 3. 熔断触发次数 (发现上游问题)
// 4. 重试次数分布 (发现网络问题)
log.info("api_metric|{}|{}|{}|{}|{}|{}|{}",
apiName, operation, // 维度
success ? 1 : 0, // 成功标识
latencyMs, // 耗时
retryCount, // 重试次数
circuitBreakerOpened ? 1 : 0, // 熔断标识
errorCode); // 错误码
这套架构已经在生产环境跑了快两年,支撑了 50+ 第三方 API、日均百万级调用。如果要继续演进,有几个方向:
|
方向 |
说明 |
|---|---|
|
异步化 |
将
|
|
多级缓存 |
对查询类 API(企业信息、地图地理编码)加 Caffeine + Redis 两级缓存 |
|
供应商切换 |
通过配置动态选择"天眼查 vs 企查查",某个挂了自动降级到备用 |
|
协议扩展 |
目前只支持 HTTP,加入 gRPC / Dubbo 适配器支持内部 RPC 调用 |
这套方案的核心价值不是让你少写几行代码,而是 把 50 个第三方 API 的复杂度收敛到一个地方。当你需要:
统一改重试策略 → 改
RetryPipeline 一处
接入新第三方 → 实现
ThirdPartyAdapter 接口即可
排查线上问题 → 所有调用都有统一的 traceId 和日志格式
某个 API 挂了 → 熔断器自动保护,不会拖垮整个系统
这就是架构的价值 —— 不让你在第 51 个 API 面前崩溃。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70050389/viewspace-3176698/ ,如需转载,请注明出处,否则将追究法律责任。