Sebagian besar komunikasi aplikasi modern tidak lagi memakai protokol buatan sendiri di atas socket mentah, melainkan HTTP: panggil API, unduh berkas, kirim data form — semuanya berbahasa HTTP. Sebelum Java 11, satu-satunya pilihan di pustaka standar adalah HttpURLConnection yang berumur panjang dan terkenal sulit dibaca, tanpa dukungan HTTP/2, serta tanpa API asinkron. Java 11 hadir dengan java.net.http.HttpClient (JEP 321) yang menggantikan posisinya: builder yang jelas, dukungan HTTP/2, sinkron dan asinkron, serta body publisher/subscriber yang fleksibel. Chapter ini memperkenalkan anatomi API tersebut dan konfigurasi builder, dengan server HTTP lokal sebagai lawan bicara yang deterministik.
Tiga Class Utama #
API HTTP modern tersusun dari tiga class yang masing-masing punya satu tugas. HttpClient adalah mesin pengirim — objek mahal yang menyimpan pool koneksi dan sebaiknya dibuat sekali lalu dipakai ulang. HttpRequest adalah deskripsi permintaan: method, URI, header, dan body, dibangun lewat HttpRequest.newBuilder(). HttpResponse adalah hasil yang menyimpan status code, header, dan body — bentuk body ditentukan oleh BodyHandlers yang kita pilih saat memanggil send().
Alur dasarnya selalu tiga langkah: bangun client, bangun request, kirim request dan terima response. Program berikut menerapkan ketiganya terhadap server com.sun.net.httpserver.HttpServer yang berjalan di loopback port ephemeral — pola uji mandiri yang akan berulang di sepanjang Section ini:
import com.sun.net.httpserver.HttpServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
public class KlienHttpMini {
public static void main(String[] args) throws Exception {
// Server HTTP lokal untuk uji tanpa jaringan eksternal
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/sapa", exchange -> {
byte[] body = "halo dari server lokal".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
server.start();
try {
HttpClient klien = HttpClient.newHttpClient();
HttpRequest permintaan = HttpRequest.newBuilder()
.uri(URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/sapa"))
.GET()
.build();
HttpResponse<String> respons = klien.send(
permintaan, HttpResponse.BodyHandlers.ofString());
System.out.println("status : " + respons.statusCode());
System.out.println("body : " + respons.body());
System.out.println("versi : " + respons.version());
System.out.println("tipe : " + respons.headers().firstValue("content-type").orElse("?"));
} finally {
server.stop(0);
}
}
}Output yang Dihasilkan:
status : 200
body : halo dari server lokal
versi : HTTP_1_1
tipe : text/plain; charset=utf-8Mari kita telaah baris-baris kode di atas:
- Baris 14-24 (
HttpServer.create(...)): server uji dibuat pada127.0.0.1port0— sistem memilih port bebas, lalu nomornya dibaca lewatserver.getAddress().getPort(). Handler/sapamembalas status 200 dengan body teks pendek. - Baris 28 (
HttpClient.newHttpClient()): client dengan konfigurasi default — HTTP/2 bila memungkinkan (turun ke 1.1 otomatis), redirect tidak diikuti, tanpa connect timeout. - Baris 29-33 (
HttpRequest.newBuilder().uri(...).GET().build()): permintaan dibangun bertahap lalu dijadikan objek final denganbuild(). ObjekHttpRequesttidak bisa diubah setelah dibangun, sehingga aman dibagikan antar thread. - Baris 35-36 (
klien.send(permintaan, HttpResponse.BodyHandlers.ofString())): pengiriman sinkron — thread berhenti sampai respons lengkap tiba.BodyHandlers.ofString()menyuruh client mengumpulkan seluruh body menjadi satuString. - Baris 38-41:
statusCode()mengembalikan status HTTP,body()mengembalikan teks hasil,version()melaporkan protokol yang benar-benar terpakai, danheaders().firstValue(...)mengambil header pertama dengan nama tersebut dalam bentukOptional<String>.
Mengonfigurasi Client dengan Builder #
HttpClient.newHttpClient() memakai default yang waras, tetapi aplikasi nyata hampir selalu perlu menyetel kebijakan. Method HttpClient.newBuilder() membuka builder dengan opsi-opsi berikut:
| Opsi builder | Peran |
|---|---|
version(...) | HTTP/2 atau HTTP/1.1; turun versi otomatis bila server belum mendukung |
connectTimeout(...) | batas waktu membuka koneksi TCP baru |
followRedirects(...) | NEVER, ALWAYS, atau NORMAL (ikut redirect kecuali HTTPS→HTTP) |
executor(...) | executor untuk operasi asinkron |
sslContext(...) / sslParameters(...) | pengaturan TLS (Chapter TLS dan HTTPS) |
Kebijakan redirect adalah contoh yang paling mudah dilihat efeknya. Program berikut menyiapkan server yang membalas 302 Found dengan header Location, lalu mengutus permintaan yang sama melalui dua client dengan kebijakan berbeda:
import com.sun.net.httpserver.HttpServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
public class KlienDenganBuilder {
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
// /asal membalas redirect 302 menuju /tujuan
server.createContext("/asal", exchange -> {
exchange.getResponseHeaders().set("Location", "/tujuan");
exchange.sendResponseHeaders(302, -1);
exchange.close();
});
server.createContext("/tujuan", exchange -> {
byte[] body = "sampai di tujuan".getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
server.start();
URI asal = URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/asal");
try {
HttpClient tanpaIkut = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NEVER)
.connectTimeout(Duration.ofSeconds(3))
.version(HttpClient.Version.HTTP_2)
.build();
HttpResponse<String> r1 = tanpaIkut.send(
HttpRequest.newBuilder(asal).GET().build(),
HttpResponse.BodyHandlers.ofString());
System.out.println("NEVER : status " + r1.statusCode()
+ ", location " + r1.headers().firstValue("location").orElse("-"));
HttpClient ikut = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
HttpResponse<String> r2 = ikut.send(
HttpRequest.newBuilder(asal).GET().build(),
HttpResponse.BodyHandlers.ofString());
System.out.println("NORMAL : status " + r2.statusCode()
+ ", body " + r2.body() + ", versi " + r2.version());
} finally {
server.stop(0);
}
}
}Output yang Dihasilkan:
NEVER : status 302, location /tujuan
NORMAL : status 200, body sampai di tujuan, versi HTTP_1_1Hasilnya memperlihatkan dua perilaku sekaligus. Client dengan Redirect.NEVER menerima respons mentah 302 dan membaca header location secara manual — status seperti ini bukan galat, melainkan jawaban sah yang butuh keputusan aplikasi. Client dengan Redirect.NORMAL mengikuti redirect sampai status 200 dan menyerahkan body akhir, sehingga r2.body() berisi isi dari /tujuan. Perhatikan pula versi HTTP_1_1 pada respons kedua: client meminta HTTP_2, tetapi server uji kita hanya berbicara HTTP/1.1, dan HttpClient menurunkan versinya secara otomatis tanpa gagal.
HTTP/2 dalam Singkat #
HTTP/2 membawa multiplexing: banyak permintaan berbagi satu koneksi TCP dalam bentuk stream yang bernomor, saling tidak menunggu antrean. Header dikompresi (HPACK) dan data dikirim berbingkai biner, bukan teks murni. Bagi kita sebagai pengguna HttpClient, perbedaan besar itu hampir tak terlihat — version(HTTP_2) cukup dianggap preferensi, dan negosiasi maupun penurunan versi ditangani sendiri. Perilaku inilah yang menjadikan HttpURLConnection sulit digantikan sebelum Java 11: API lawas tersebut hanya mengenal HTTP/1.1 dan tidak punya ruang untuk fitur baru.
Kerangka client kini berdiri. Yang belum kita sentuh adalah isi permintaan itu sendiri: method POST dengan body, header khusus, serta cara membaca berbagai bentuk respons. Chapter berikutnya membedah HttpRequest dan HttpResponse sampai ke detail body publisher dan body handler.
Referensi #
- Oracle: HttpClient API
- Oracle: java.net.http Package Summary
- OpenJDK: JEP 321 — HTTP Client
- Oracle: com.sun.net.httpserver Package Summary
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
KlienDenganBuilder.java
Unduh berkasimport com.sun.net.httpserver.HttpServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
public class KlienDenganBuilder {
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
// /asal membalas redirect 302 menuju /tujuan
server.createContext("/asal", exchange -> {
exchange.getResponseHeaders().set("Location", "/tujuan");
exchange.sendResponseHeaders(302, -1);
exchange.close();
});
server.createContext("/tujuan", exchange -> {
byte[] body = "sampai di tujuan".getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
server.start();
URI asal = URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/asal");
try {
HttpClient tanpaIkut = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NEVER)
.connectTimeout(Duration.ofSeconds(3))
.version(HttpClient.Version.HTTP_2)
.build();
HttpResponse<String> r1 = tanpaIkut.send(
HttpRequest.newBuilder(asal).GET().build(),
HttpResponse.BodyHandlers.ofString());
System.out.println("NEVER : status " + r1.statusCode()
+ ", location " + r1.headers().firstValue("location").orElse("-"));
HttpClient ikut = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
HttpResponse<String> r2 = ikut.send(
HttpRequest.newBuilder(asal).GET().build(),
HttpResponse.BodyHandlers.ofString());
System.out.println("NORMAL : status " + r2.statusCode()
+ ", body " + r2.body() + ", versi " + r2.version());
} finally {
server.stop(0);
}
}
}
KlienHttpMini.java
Unduh berkasimport com.sun.net.httpserver.HttpServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
public class KlienHttpMini {
public static void main(String[] args) throws Exception {
// Server HTTP lokal untuk uji tanpa jaringan eksternal
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/sapa", exchange -> {
byte[] body = "halo dari server lokal".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
server.start();
try {
HttpClient klien = HttpClient.newHttpClient();
HttpRequest permintaan = HttpRequest.newBuilder()
.uri(URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/sapa"))
.GET()
.build();
HttpResponse<String> respons = klien.send(
permintaan, HttpResponse.BodyHandlers.ofString());
System.out.println("status : " + respons.statusCode());
System.out.println("body : " + respons.body());
System.out.println("versi : " + respons.version());
System.out.println("tipe : " + respons.headers().firstValue("content-type").orElse("?"));
} finally {
server.stop(0);
}
}
}