JDK 11 將 java.net.http.HttpClient 納入標準庫,支援 HTTP/2、WebSocket、非同步、反應式串流,取代舊版 HttpURLConnection 與第三方 Apache HttpClient。本文系統整理 POST 請求的各種實戰場景:JSON API、表單提交、多部分檔案上傳、超時與重試策略、攔截器/日誌。
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) // 優先 HTTP/2
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(5)) // 連線超時
.build();
String json = """
{"userId": 1001, "action": "purchase", "amount": 99.9}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/v1/order"))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8))
.timeout(Duration.ofSeconds(10)) // 請求超時
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + response.statusCode());
System.out.println("Body: " + response.body());
String form = "username=john&password=secret&rememberMe=true";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://auth.example.com/login"))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(form, StandardCharsets.UTF_8))
.build();
Path filePath = Paths.get("report.pdf");
String boundary = "Boundary-" + UUID.randomUUID();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (var out = new DataOutputStream(bos)) {
// 文字欄位
writePart(out, boundary, "userId", "1001", null);
// 檔案欄位
writeFilePart(out, boundary, "file", "report.pdf", "application/pdf", Files.readAllBytes(filePath));
// 結束邊界
out.writeBytes("--" + boundary + "--\r\n");
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/upload"))
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofByteArray(bos.toByteArray()))
.build();
private static void writePart(DataOutputStream out, String boundary, String name, String value, String contentType) throws IOException {
out.writeBytes("--" + boundary + "\r\n");
out.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"\r\n");
if (contentType != null) out.writeBytes("Content-Type: " + contentType + "\r\n");
out.writeBytes("\r\n");
out.writeBytes(value + "\r\n");
}
private static void writeFilePart(DataOutputStream out, String boundary, String name, String filename, String mime, byte[] content) throws IOException {
out.writeBytes("--" + boundary + "\r\n");
out.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + filename + "\"\r\n");
out.writeBytes("Content-Type: " + mime + "\r\n");
out.writeBytes("\r\n");
out.write(content);
out.writeBytes("\r\n");
}
提示:生產環境建議使用 OkHttp 或 Apache HttpClient 5 的 MultipartEntityBuilder,避免手工拼裝錯誤。
CompletableFuture<HttpResponse<String>> future = client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
future.thenAccept(resp -> {
System.out.println("Async status: " + resp.statusCode());
}).exceptionally(ex -> {
ex.printStackTrace();
return null;
});
// 主執行緒可繼續做其他事
// 下載直接寫入檔案
HttpResponse<Path> resp = client.send(request, HttpResponse.BodyHandlers.ofFile(Paths.get("big.zip")));
// 上傳大檔案 (BodyPublisher.fromFile JDK 17+)
HttpRequest put = HttpRequest.newBuilder()
.uri(URI.create("https://storage.example.com/bucket/big.zip"))
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofFile(Paths.get("big.zip")))
.build();
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-retry</artifactId>
<version>2.1.0</version>
</dependency>
RetryConfig config = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(500))
.retryExceptions(IOException.class, HttpTimeoutException.class)
.build();
RetryRegistry registry = RetryRegistry.of(config);
Retry retry = registry.retry("httpClient");
Supplier<HttpResponse<String>> supplier = Retry.decorateSupplier(retry, () ->
client.send(request, HttpResponse.BodyHandlers.ofString()));
HttpResponse<String> resp = supplier.get();
JDK HttpClient 無內建攔截器,若需統一日誌、簽名、Token 刷新,建議封裝:
public final class HttpClients {
private static final HttpClient CLIENT = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(5))
.build();
public static HttpResponse<String> postJson(String url, String json) throws IOException, InterruptedException {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.timeout(Duration.ofSeconds(15))
.build();
long start = System.currentTimeMillis();
HttpResponse<String> resp = CLIENT.send(req, HttpResponse.BodyHandlers.ofString());
logRequest(url, json, resp, start);
return resp;
}
private static void logRequest(String url, String reqBody, HttpResponse<String> resp, long start) {
long cost = System.currentTimeMillis() - start;
log.info("POST {} -> {} ({} ms) req={} resp={}",
url, resp.statusCode(), cost, mask(reqBody), mask(resp.body()));
}
private static String mask(String s) { return s.length() > 500 ? s.substring(0, 500) + "..." : s; }
}
| Apache HttpClient | JDK HttpClient |
|---|---|
| HttpClientBuilder.create().build() | HttpClient.newBuilder().build() |
| HttpPost | HttpRequest.newBuilder().POST(...) |
| UrlEncodedFormEntity | BodyPublishers.ofString(form) |
| MultipartEntityBuilder | 手工拼裝或用 OkHttp |
| CloseableHttpResponse | HttpResponse |
| EntityUtils.toString() | response.body() |
| RequestConfig.setConnectTimeout | HttpClient.Builder.connectTimeout / HttpRequest.Builder.timeout |
| RetryHandler | Resilience4j / 手工重試 |
HttpClient 實例 (執行緒安全、連線池複用)。BodyPublishers.ofFile / BodyHandlers.ofFile 零拷貝。sendAsync + CompletableFuture,避免阻塞業務執行緒。