ITPub博客

首页 > 大数据 > 数据挖掘 > 第三方 API 接口后的架构反思:适配器模式 + 重试 + 熔断的通用封装

第三方 API 接口后的架构反思:适配器模式 + 重试 + 熔断的通用封装

原创 数据挖掘 作者:ob_Alan 时间:2026-07-24 13:59:29 0 删除 编辑

本文记录了我在项目中对接超过 50 个第三方 API 后的经验总结,涵盖统一抽象层设计、适配器模式落地、指数退避重试、熔断保护、可观测性埋点等实战方案。完整代码可直接复用。

一、背景:当你需要对接第 10 个第三方 API 时,事情开始失控

我做的是一个 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 统一成内部标准


三、核心代码实现

3.1 统一请求/响应模型

  • 首先定义一套内部标准协议,所有适配器都必须适配到这个模型:

// 统一请求模型
@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
}

3.2 定义 SPI 接口:适配器的契约

/**
* 第三方 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();
}

3.3 适配器具体实现示例:天眼查

@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();
    }
}

3.4 重试管道:指数退避 + 可重试错误判断

@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; }
    }
}

3.6 统一客户端:把所有管道串起来

@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();
    }
}

3.7 业务层使用:极其简洁

@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 只需 3 步

假设现在要接入 高德地图地理编码 API,只需:

Step 1:写适配器

@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();
    }
}

Step 2:加配置

api:
  amap:
    key: your_amap_key
    base-url: https://restapi.amap.com

Step 3:直接调用

ApiRequest request = ApiRequest.builder()
        .method("GET")
        .path("/v3/geocode/geo")
        .queryParams(Map.of("address", "北京市朝阳区"))
        .build();
String result = apiClient.call("Amap", "geocode", request);
- **// 自动享受:**鉴权注入、指数退避重试、熔断保护、日志埋点

完成。 不用写任何重试逻辑、错误处理、超时配置,这些都是框架层面的事。

五、踩坑经验:50 个 API 教会我的 5 条规则

坑 1:不要信任第三方的"成功"定义

有的 API 返回 HTTP 200 但 body 里是 {"code": 500},有的返回 HTTP 500 但 body 里写"其实是成功了"。 永远在 parse() 里同时检查 HTTP 状态码和业务状态码。

// 这是我在线上查了一下午才找到的 bug 根源
- **// 某短信平台:**发送成功返回 HTTP 200 + {"code": "0"}
// 但你的额度用完时,它也返回 HTTP 200 + {"code": "-10086", "msg": "余额不足"}
// 如果只看 HTTP 200 就认为成功,就会漏掉这个 case

坑 2:重试要有上限,并且必须区分可重试错误

不是所有错误都适合重试。 "余额不足"重试 3 次还是余额不足,但每次都消耗一次 HTTP 请求配额。 参考上面 RetryPipeline 里的 isRetryable() 判断逻辑。

坑 3:超时时间不要一刀切

  • 一刀切设 10 秒是在给自己埋坑。我的经验:

场景

建议超时

原因

短信发送

15s

运营商返回慢

企业信息查询

5s

纯数据库查询

支付下单

10s

需要留余地

地图反向地理编码

3s

高频调用,超时就 fast-fail

坑 4:API Key 泄露是最大安全隐患

所有 Key/Secret 必须走配置中心(Apollo/Nacos),不要在代码里硬编码。 代码仓库里搜到过 AK/SK 的,都不是个例。

// ❌ 绝对不要这样写
private static final String API_KEY = "sk-xxxxxxxxxxxx";
// ✅ 从配置中心注入
@Value("${api.tianyancha.app-key}")
private String appKey;

坑 5:一定要埋点:耗时、命中率、熔断次数

  • 没有监控的 API 封装就是在裸奔。至少记录这几个指标:

// 关键指标
// 1. 调用量 + 成功率 (方便做告警)
// 2. P50/P99 耗时 (发现某方 API 变慢)
// 3. 熔断触发次数 (发现上游问题)
// 4. 重试次数分布 (发现网络问题)
log.info("api_metric|{}|{}|{}|{}|{}|{}|{}",
    apiName, operation,             // 维度
    success ? 1 : 0,               // 成功标识
    latencyMs,                      // 耗时
    retryCount,                     // 重试次数
    circuitBreakerOpened ? 1 : 0,  // 熔断标识
    errorCode);                     // 错误码

接入 Prometheus + Grafana 后,一眼就能看出哪个第三方供应商不靠谱。

六、进阶方向

  • 这套架构已经在生产环境跑了快两年,支撑了 50+ 第三方 API、日均百万级调用。如果要继续演进,有几个方向:

方向

说明

异步化

将 call() 改为返回 CompletableFuture,支持并发调用多个 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/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2026-06-05

  • 博文量
    44
  • 访问量
    39176