HTTP 客戶端設計:OkHttp 連接池與多場景應用

商業價值:穩定的 HTTP 客戶端讓系統「能可靠地跟 17 個平台通訊」,這是 導讀篇提到「99% 庫存準確率」的基礎——API 不穩定,庫存同步就會失敗。

前言:呼叫外部 API 的挑戰

多通路系統需要呼叫大量外部 API:

API 類型 特性 挑戰
蝦皮 API 流量限制嚴格 需要控制請求頻率
物流 API 回應慢 需要較長逾時
支付 API 高可靠性要求 需要重試機制
問題:每次都建立新連線 → 效能差、資源浪費

解決方案:OkHttp 連線池

連線池設定

@Configuration
public class HttpClientConfig {

@Bean
public OkHttpClient okHttpClient() {
return new OkHttpClient.Builder()
// 連線池設定
.connectionPool(new ConnectionPool(
100, // 最大閒置連線數
5, TimeUnit.MINUTES // 閒置時間
))

// 逾時設定
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)

// 重試
.retryOnConnectionFailure(true)

.build();
}
}

設定 說明
maxIdleConnections 100 最多保持 100 條閒置連線
keepAliveDuration 5 分鐘 閒置連線保持時間
connectTimeout 10 秒 建立連線逾時
readTimeout 30 秒 讀取回應逾時

HTTP 客戶端封裝

@Component
public class HttpClientService {

@Autowired
private OkHttpClient okHttpClient;

/**
* GET 請求
*/

public HttpResult get(String url, Map<String, String> headers) {
Request request = new Request.Builder()
.url(url)
.headers(Headers.of(headers))
.get()
.build();

return execute(request);
}

/**
* POST 請求(JSON)
*/

public HttpResult postJson(String url, Object body, Map<String, String> headers) {
String json = JsonUtil.toJson(body);

Request request = new Request.Builder()
.url(url)
.headers(Headers.of(headers))
.post(RequestBody.create(json, MediaType.parse(“application/json”)))
.build();

return execute(request);
}

/**
* POST 請求(Form)
*/

public HttpResult postForm(String url, Map<String, String> params, Map<String, String> headers) {
FormBody.Builder formBuilder = new FormBody.Builder();
params.forEach(formBuilder::add);

Request request = new Request.Builder()
.url(url)
.headers(Headers.of(headers))
.post(formBuilder.build())
.build();

return execute(request);
}

private HttpResult execute(Request request) {
try (Response response = okHttpClient.newCall(request).execute()) {
return HttpResult.builder()
.statusCode(response.code())
.body(response.body() != null ? response.body().string() : null)
.headers(response.headers().toMultimap())
.success(response.isSuccessful())
.build();

} catch (IOException e) {
return HttpResult.builder()
.success(false)
.errorMessage(e.getMessage())
.build();
}
}
}


回應結果封裝

@Data
@Builder
public class HttpResult {
private boolean success;
private int statusCode;
private String body;
private Map<String, List<String>> headers;
private String errorMessage;

/**
* 解析 JSON 回應
*/

public <T> T parseJson(Class<T> clazz) {
if (!success || body == null) {
return null;
}
return JsonUtil.fromJson(body, clazz);
}

/**
* 解析 JSON 陣列回應
*/

public <T> List<T> parseJsonList(Class<T> clazz) {
if (!success || body == null) {
return Collections.emptyList();
}
return JsonUtil.fromJsonList(body, clazz);
}
}


追蹤 Header 傳遞

支援分散式追蹤,自動傳遞追蹤 Header:

@Component
public class TracingHttpClient {

private static final List<String> TRACING_HEADERS = List.of(
“x-request-id”,
“x-b3-traceid”,
“x-b3-spanid”,
“x-b3-parentspanid”,
“x-b3-sampled”
);

@Autowired
private HttpClientService httpClient;

/**
* 從當前請求提取追蹤 Header
*/

public Map<String, String> extractTracingHeaders(HttpServletRequest request) {
Map<String, String> headers = new HashMap<>();

for (String name : TRACING_HEADERS) {
String value = request.getHeader(name);
if (value != null) {
headers.put(name, value);
}
}

return headers;
}

/**
* 發送請求,自動帶入追蹤 Header
*/

public HttpResult getWithTracing(String url, HttpServletRequest currentRequest) {
Map<String, String> headers = extractTracingHeaders(currentRequest);
return httpClient.get(url, headers);
}
}


重試機制

@Component
public class RetryableHttpClient {

@Autowired
private HttpClientService httpClient;

/**
* 帶重試的請求
*/

public HttpResult getWithRetry(String url, Map<String, String> headers, int maxRetries) {
int attempt = 0;
HttpResult result = null;

while (attempt < maxRetries) {
result = httpClient.get(url, headers);

if (result.isSuccess()) {
return result;
}

// 只對可重試的錯誤重試
if (!isRetryable(result.getStatusCode())) {
break;
}

attempt++;
sleep(calculateBackoff(attempt));
}

return result;
}

private boolean isRetryable(int statusCode) {
// 5xx 錯誤和 429 (Too Many Requests) 可重試
return statusCode >= 500 || statusCode == 429;
}

private long calculateBackoff(int attempt) {
// 指數退避:1秒, 2秒, 4秒…
return (long) Math.pow(2, attempt – 1) * 1000;
}
}

HTTP 狀態碼 是否重試 原因
2xx 不需要 成功
4xx (非 429) 不重試 客戶端錯誤,重試也沒用
429 重試 流量限制,稍後重試
5xx 重試 伺服器暫時錯誤

使用範例

@Service
public class ShopeeApiClient {

@Autowired
private RetryableHttpClient httpClient;

public List<ShopeeOrder> getOrders(String accessToken) {
String url = “https://partner.shopeemobile.com/api/v2/order/get_order_list”;

Map<String, String> headers = Map.of(
“Authorization”, “Bearer “ + accessToken,
“Content-Type”, “application/json”
);

HttpResult result = httpClient.getWithRetry(url, headers, 3);

if (!result.isSuccess()) {
throw new ApiException(“Shopee API 錯誤: “ + result.getErrorMessage());
}

return result.parseJsonList(ShopeeOrder.class);
}
}


總結

設計 效果
連線池 復用連線,減少建立成本
逾時設定 避免請求無限等待
結果封裝 統一處理成功/失敗
追蹤 Header 支援分散式追蹤
重試機制 自動處理暫時性錯誤

為什麼不用其他方案?

方案 優點 缺點 結論
HttpURLConnection JDK 內建 API 難用、功能少 不推薦
Apache HttpClient 功能完整 API 複雜、依賴多 可用但重
Spring RestTemplate Spring 整合好 已被標記為 maintenance 舊專案可用
Spring WebClient 非同步、Reactive 學習曲線、除錯困難 Reactive 專案用
OkHttp 輕量、效能好、API 簡潔 非 Spring 原生 同步 HTTP 首選

實戰踩坑

坑 1:連線池用完了

早期沒設連線池,每次請求都建新連線。流量一大,系統噴 Connection refused。加上連線池後,效能提升 10 倍,問題消失。

坑 2:逾時設太長

最初 readTimeout 設 60 秒。某平台 API 掛了,執行緒都在等待,整個服務卡死。改成 30 秒 + 重試機制後,就算 API 慢也能處理。

坑 3:沒處理 429 Too Many Requests

蝦皮有流量限制,瘋狂打 API 會收到 429。最初沒處理,一直重試反而更慢。加上指數退避(1秒、2秒、4秒…)後,流量限制問題大幅改善。


系列導航

◀ 上一篇
DTO 設計
📚 返回目錄 下一篇 ▶
PDF 生成

留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *