前言:呼叫外部 API 的挑戰
多通路系統需要呼叫大量外部 API:
| API 類型 | 特性 | 挑戰 |
|---|---|---|
| 蝦皮 API | 流量限制嚴格 | 需要控制請求頻率 |
| 物流 API | 回應慢 | 需要較長逾時 |
| 支付 API | 高可靠性要求 | 需要重試機制 |
解決方案:OkHttp 連線池
連線池設定
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 客戶端封裝
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();
}
}
}
回應結果封裝
@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:
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);
}
}
重試機制
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 | 重試 | 伺服器暫時錯誤 |
使用範例
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 首選 |
實戰踩坑
早期沒設連線池,每次請求都建新連線。流量一大,系統噴 Connection refused。加上連線池後,效能提升 10 倍,問題消失。
最初 readTimeout 設 60 秒。某平台 API 掛了,執行緒都在等待,整個服務卡死。改成 30 秒 + 重試機制後,就算 API 慢也能處理。
蝦皮有流量限制,瘋狂打 API 會收到 429。最初沒處理,一直重試反而更慢。加上指數退避(1秒、2秒、4秒…)後,流量限制問題大幅改善。
系列導航
| ◀ 上一篇 DTO 設計 |
📚 返回目錄 | 下一篇 ▶ PDF 生成 |
發佈留言