Method send() yang kita pakai sepanjang Section ini nyaman untuk satu permintaan, tetapi ada harga tersembunyi: thread pemanggil berhenti total selama menunggu jawaban. Jika satu layanan perlu memanggil lima API lain untuk merangkai satu halaman, pola sinkron menghabiskan jumlah waktu yang sama dengan jumlah penundaan para API itu — dijumlahkan berurutan. Mode asinkron HttpClient membalik keadaan tersebut: sendAsync() mengembalikan CompletableFuture<HttpResponse<T>> seketika, thread bebas mengerjakan hal lain, dan hasil diolah lewat rantai operasi ketika data tiba. Lima panggilan yang berjalan bersamaan hanya selambat panggilan terlambatnya. Chapter ini menguasai pola tersebut — pengiriman paralel, transformasi hasil, dan perantaian permintaan yang bergantung — dengan tetap menjaga output yang deterministik.
sendAsync dan CompletableFuture #
CompletableFuture adalah janji hasil di masa depan: objek yang kelak terisi ketika operasi selesai, lengkap dengan method untuk mengolah isinya. Ketika sendAsync(req, BodyHandlers.ofString()) dipanggil, HttpClient mengirim permintaan dari thread eksekutor internalnya dan mengembalikan future kosong; begitu respons lengkap tiba, future terisi dengan HttpResponse<String>. Operasi lanjutan dirangkai tanpa memblokir: thenApply(...) mengubah isi future, thenCompose(...) merangkai future baru yang bergantung pada hasil sebelumnya, dan exceptionally(...) menangkap kegagalan.
Program berikut mengirim tiga permintaan secara bersamaan lewat satu client, memverifikasi penyelesaiannya dengan allOf().join(), lalu mencetak hasil sesuai urutan pengiriman:
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.util.List;
import java.util.concurrent.CompletableFuture;
public class SapaAsync {
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/sapa/", exchange -> {
String nama = exchange.getRequestURI().getPath().substring("/sapa/".length());
byte[] body = ("halo " + nama).getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
server.start();
try {
HttpClient klien = HttpClient.newHttpClient();
List<String> nama = List.of("Budi", "Siti", "Dewi");
// Tiga permintaan dikirim tanpa menunggu satu sama lain
CompletableFuture<String>[] tugas = new CompletableFuture[nama.size()];
for (int i = 0; i < nama.size(); i++) {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("http://127.0.0.1:" + server.getAddress().getPort()
+ "/sapa/" + nama.get(i)))
.GET().build();
tugas[i] = klien.sendAsync(req, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body);
}
CompletableFuture.allOf(tugas).join();
System.out.println("permintaan tuntas : " + tugas.length);
for (int i = 0; i < tugas.length; i++) {
System.out.println("hasil " + (i + 1) + " : " + tugas[i].join());
}
} finally {
server.stop(0);
}
}
}Output yang Dihasilkan:
permintaan tuntas : 3
hasil 1 : halo Budi
hasil 2 : halo Siti
hasil 3 : halo DewiMari kita telaah baris-baris kode di atas:
- Baris 36-38 (
klien.sendAsync(req, ...)): pemanggilan seketika mengembalikanCompletableFuture<HttpResponse<String>>— threadmaintidak menunggu sama sekali pada titik ini. Loop berikutnya langsung mengirim permintaan kedua dan ketiga, sehingga ketiganya mengapung bersamaan di eksekutor client. - Baris 38 (
.thenApply(HttpResponse::body)): begitu future respons terisi, isiannya ditransformasi menjadi body-nya saja.thenApplymenghasilkan future baru bertipeCompletableFuture<String>tanpa memblokir siapa pun. - Baris 42 (
CompletableFuture.allOf(tugas).join()):allOfmenggabungkan seluruh future menjadi satu future “semua selesai”;join()adalah satu-satunya titik menunggu di program ini. Dari sini, semua hasil pasti sudah tersedia. - Baris 44-46 (
tugas[i].join()): pemanggilanjoin()pada future yang sudah terisi langsung mengembalikan isinya tanpa menunggu. Pencetakan mengikuti indeks array, bukan urutan kedatangan, sehingga output selalu terurutBudi,Siti,Dewimeskipun jaringan menyajikan balasan dalam urutan mana pun.
Merangkai Permintaan yang Bergantung #
Kasus kedua yang sering muncul: permintaan berikutnya butuh hasil permintaan sebelumnya — ambil token dulu, atau cari ID dulu, baru panggil detailnya. Pola naif dengan join() di tengah jalan memblokir thread di tiap persimpangan; thenCompose merapatkan persimpangan itu menjadi satu rantai future yang mengalir tanpa blok. Program berikut mengambil nama kota dari /kota, lalu memanggil /sapa/<kota> dengan hasil tersebut:
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.util.concurrent.CompletableFuture;
public class RantaiAsync {
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/kota", exchange -> {
byte[] body = "Bandung".getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
server.createContext("/sapa/", exchange -> {
String nama = exchange.getRequestURI().getPath().substring("/sapa/".length());
byte[] body = ("halo " + nama).getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
server.start();
try {
HttpClient klien = HttpClient.newHttpClient();
String dasar = "http://127.0.0.1:" + server.getAddress().getPort();
// Permintaan kedua bergantung pada hasil permintaan pertama
CompletableFuture<String> hasil =
klien.sendAsync(
HttpRequest.newBuilder(URI.create(dasar + "/kota")).GET().build(),
HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenCompose(kota -> klien.sendAsync(
HttpRequest.newBuilder(URI.create(dasar + "/sapa/" + kota))
.GET().build(),
HttpResponse.BodyHandlers.ofString()))
.thenApply(HttpResponse::body);
System.out.println("hasil rantai : " + hasil.join());
} finally {
server.stop(0);
}
}
}Output yang Dihasilkan:
hasil rantai : halo BandungRantainya layak dibaca dari bawah ke atas struktur logikanya: sendAsync("/kota") menghasilkan respons, thenApply(HttpResponse::body) mengekstrak teks Bandung, lalu thenCompose menerima teks itu dan memulai permintaan kedua ke /sapa/Bandung — hasilnya future baru yang digabung kembali menjadi satu rantai tunggal, dan thenApply terakhir mengambil body akhir. Thread main hanya berhenti sekali, di hasil.join() paling akhir; selama dua permintaan berjalan, thread tersebut sepenuhnya bebas. Jika salah satu mata rantai gagal — koneksi ditolak, status tidak terduga — kegagalan mengalir sepanjang rantai dan muncul sebagai CompletionException pada join(), cukup ditangkap satu kali di ujung.
Eksekutor dan Batas Parallelisme #
Secara bawaan, sendAsync berjalan pada eksekutor internal milik HttpClient, dan callback thenApply/thenCompose dieksekusi pada thread yang menyelesaikan future tersebut. Aplikasi yang butuh kendali — membatasi parallelisme, mengukur konteks, atau memisahkan I/O dari pemrosesan — menyediakan eksekutornya sendiri lewat builder:
ExecutorService eksekutor = Executors.newFixedThreadPool(8);
HttpClient klien = HttpClient.newBuilder()
.executor(eksekutor)
.build();
// seluruh sendAsync memakai eksekutor tersebutPola ini berpadu baik dengan pengaturan di Chapter Concurrency: ukuran pool disesuaikan beban, dan penutupan ExecutorService menjadi bagian dari siklus hidup aplikasi. Untuk mayoritas kasus, eksekutor bawaan sudah memadai — kendali penuh baru dibutuhkan saat profil beban mulai terukur.
Dengan mode sinkron dan asinkron di tangan, perangkat komunikasi HttpClient lengkaplah sudah. Satu wilayah terakhir yang menentukan layak atau tidaknya semua ini masuk produksi: keamanan di level aplikasi — memvalidasi alamat dari input pengguna, menutup celah SSRF, dan menjaga rahasia tetap rahasia. Chapter penutup Section ini menghimpun semuanya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
RantaiAsync.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.util.concurrent.CompletableFuture;
public class RantaiAsync {
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/kota", exchange -> {
byte[] body = "Bandung".getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
server.createContext("/sapa/", exchange -> {
String nama = exchange.getRequestURI().getPath().substring("/sapa/".length());
byte[] body = ("halo " + nama).getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
server.start();
try {
HttpClient klien = HttpClient.newHttpClient();
String dasar = "http://127.0.0.1:" + server.getAddress().getPort();
// Permintaan kedua bergantung pada hasil permintaan pertama
CompletableFuture<String> hasil =
klien.sendAsync(
HttpRequest.newBuilder(URI.create(dasar + "/kota")).GET().build(),
HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenCompose(kota -> klien.sendAsync(
HttpRequest.newBuilder(URI.create(dasar + "/sapa/" + kota))
.GET().build(),
HttpResponse.BodyHandlers.ofString()))
.thenApply(HttpResponse::body);
System.out.println("hasil rantai : " + hasil.join());
} finally {
server.stop(0);
}
}
}
SapaAsync.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.util.List;
import java.util.concurrent.CompletableFuture;
public class SapaAsync {
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/sapa/", exchange -> {
String nama = exchange.getRequestURI().getPath().substring("/sapa/".length());
byte[] body = ("halo " + nama).getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
server.start();
try {
HttpClient klien = HttpClient.newHttpClient();
List<String> nama = List.of("Budi", "Siti", "Dewi");
// Tiga permintaan dikirim tanpa menunggu satu sama lain
CompletableFuture<String>[] tugas = new CompletableFuture[nama.size()];
for (int i = 0; i < nama.size(); i++) {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("http://127.0.0.1:" + server.getAddress().getPort()
+ "/sapa/" + nama.get(i)))
.GET().build();
tugas[i] = klien.sendAsync(req, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body);
}
CompletableFuture.allOf(tugas).join();
System.out.println("permintaan tuntas : " + tugas.length);
for (int i = 0; i < tugas.length; i++) {
System.out.println("hasil " + (i + 1) + " : " + tugas[i].join());
}
} finally {
server.stop(0);
}
}
}