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:

text
status : 200
body   : halo dari server lokal
versi  : HTTP_1_1
tipe   : text/plain; charset=utf-8

Mari kita telaah baris-baris kode di atas:

  1. Baris 14-24 (HttpServer.create(...)): server uji dibuat pada 127.0.0.1 port 0 — sistem memilih port bebas, lalu nomornya dibaca lewat server.getAddress().getPort(). Handler /sapa membalas status 200 dengan body teks pendek.
  2. Baris 28 (HttpClient.newHttpClient()): client dengan konfigurasi default — HTTP/2 bila memungkinkan (turun ke 1.1 otomatis), redirect tidak diikuti, tanpa connect timeout.
  3. Baris 29-33 (HttpRequest.newBuilder().uri(...).GET().build()): permintaan dibangun bertahap lalu dijadikan objek final dengan build(). Objek HttpRequest tidak bisa diubah setelah dibangun, sehingga aman dibagikan antar thread.
  4. 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 satu String.
  5. Baris 38-41: statusCode() mengembalikan status HTTP, body() mengembalikan teks hasil, version() melaporkan protokol yang benar-benar terpakai, dan headers().firstValue(...) mengambil header pertama dengan nama tersebut dalam bentuk Optional<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 builderPeran
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:

text
NEVER  : status 302, location /tujuan
NORMAL : status 200, body sampai di tujuan, versi HTTP_1_1

Hasilnya 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

KlienDenganBuilder.java

Unduh berkas
java
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);
        }
    }
}

KlienHttpMini.java

Unduh berkas
java
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);
        }
    }
}