Memilih REST sinkron berarti menerima satu kenyataan: kode kita kini bergantung pada proses lain yang berjalan di mesin lain, di luar kendali kita. Panggilan bisa berhasil, bisa dijawab dengan error dari server, atau bisa gagal lebih dini karena jaringan putus sebelum kata pertama sampai. Chapter ini membangun panggilan antarservice yang bertanggung jawab dengan java.net.http.HttpClient — HTTP client bawaan JDK yang sudah kita kenal di Section Networking — lalu memperlakukan kegagalan bukan sebagai kejadian langka, melainkan sebagai salah satu hasil normal setiap panggilan. Sebuah server tiruan di alamat loopback memainkan peran svc-inventori, lengkap dengan satu endpoint yang sengaja sakit.
HttpClient Warga JDK #
HttpClient tinggal di java.net.http sejak Java 11 dan tidak butuh dependensi apa pun — satu alasan kuat memilihnya untuk panggilan antarservice di aplikasi yang ingin tetap ramping. Objek HttpRequest dibangun lewat builder dan bersifat immutable: satu request bisa dikirim berkali-kali, dan satu HttpClient boleh dipakai bersama banyak request — kelas tersebut aman lintas thread.
Dua mode pengiriman tersedia. Method send(...) memblokir hingga jawaban tiba — sederhana dan cukup untuk sebagian besar kasus. Method sendAsync(...) mengembalikan CompletableFuture untuk aliran non-blokir ala reactive. Sejak Java 21, ada jalan ketiga: send(...) di atas virtual threads (JEP 444) memberi gaya penulisan blokir yang tetap hemat sumber daya — ribuan panggilan menunggu tanpa ribuan thread OS.
Panggilan dengan Tiga Nasib #
Program berikut menyiapkan svc-inventori dengan dua endpoint — /stok/A1 sehat, /stok/B2 selalu menjawab 503 — lalu memanggil keduanya, dan terakhir memanggil endpoint sehat setelah server dimatikan:
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
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;
/** svc-pesanan memanggil svc-inventori lewat REST dan menangani kegagalannya. */
public class PanggilInventori {
public static void main(String[] args) throws Exception {
// "svc-inventori" lokal: satu endpoint sehat, satu endpoint gagal
HttpServer inventori = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
inventori.createContext("/stok/A1", exchange -> {
byte[] body = "{\"kode\":\"A1\",\"jumlah\":7}".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
inventori.createContext("/stok/B2", exchange -> {
byte[] body = "{\"error\":\"gudang sibuk\"}".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(503, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
inventori.start();
String dasar = "http://127.0.0.1:" + inventori.getAddress().getPort();
HttpClient klien = HttpClient.newHttpClient();
try {
System.out.println("skenario 1 (sukses) : " + panggil(klien, dasar + "/stok/A1"));
System.out.println("skenario 2 (503) : " + panggil(klien, dasar + "/stok/B2"));
} finally {
inventori.stop(0); // service inventori "dimatikan"
}
// Klien baru (pool koneksi baru) menabrak port yang sudah tidak dilayani
HttpClient klienBaru = HttpClient.newHttpClient();
System.out.println("skenario 3 (mati) : " + panggil(klienBaru, dasar + "/stok/A1"));
}
/** Satu panggilan GET dengan penanganan status respons dan kegagalan koneksi. */
static String panggil(HttpClient klien, String url) {
HttpRequest permintaan = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Accept", "application/json")
.GET()
.build();
try {
HttpResponse<String> respons =
klien.send(permintaan, HttpResponse.BodyHandlers.ofString());
if (respons.statusCode() == 200) {
return "200 -> " + respons.body();
}
return respons.statusCode() + " -> stok tidak diketahui, jangan lempar mentah";
} catch (IOException e) {
return "koneksi gagal (" + e.getClass().getSimpleName() + ") -> fallback cache";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "terputus";
}
}
}Output yang Dihasilkan:
skenario 1 (sukses) : 200 -> {"kode":"A1","jumlah":7}
skenario 2 (503) : 503 -> stok tidak diketahui, jangan lempar mentah
skenario 3 (mati) : koneksi gagal (ConnectException) -> fallback cacheMari kita bedah bagian-bagian kuncinya:
HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0): server uji yang hanya mendengarkan alamat loopback, dengan port0agar sistem operasi memilih port bebas. Trik ini memungkinkan kita menguji perilaku HTTP nyata tanpa layanan eksternal dan tanpa membuka port ke jaringan luar.- Dua
createContext(...): setiap path punya handler sendiri. Handler/stok/B2sengaja mengirim status 503 dengan body JSON error — mensimulasikan service tetangga yang kelebihan beban, kasus paling sering di produksi. panggil(klien, url): satu method untuk seluruh skenario. Cabang pertama memeriksarespons.statusCode() == 200sebelum memakai body; cabang kedua memetakan status apa pun yang bukan 200 menjadi keputusan domain (“stok tidak diketahui”), bukan exception yang lepas mentah ke pemanggil.catch (IOException e): kegagalan sebelum respons terbentuk — koneksi ditolak, timeout, koneksi diputus — masuk kategori ini.ConnectExceptionyang tampil pada skenario 3 adalah turunanIOException; kami mencetak nama kelasnya agar jenis kegagalannya terlihat, lalu memilih fallback.catch (InterruptedException e):send(...)yang diblokir bisa dibangunkan lewat interrupt. Kebiasaan yang benar: pulihkan status interrupt denganThread.currentThread().interrupt()sebelum kembali.- Skenario 3 memakai
HttpClientbaru: klien pertama masih menyimpan koneksi keep-alive lama ke server; klien baru dipakai agar panggilan ketiga benar-benar menabrak port yang sudah tidak dilayani siapa pun.
Memetakan Status ke Keputusan #
Status HTTP hanyalah angka sampai kode kita memberinya arti. Pemetaan yang sehat di sisi klien:
| Kondisi | Contoh | Arti untuk klien | Tindakan wajar |
|---|---|---|---|
| 2xx | 200, 201 | permintaan berhasil | baca body, lanjutkan |
| 4xx | 404, 409, 422 | permintaan kita keliru | perbaiki permintaan; jangan di-retry |
| 5xx | 500, 503 | server tetangga bermasalah | retry terbatas, fallback, catat |
| Timeout | HttpTimeoutException | tidak ada jawaban tepat waktu | retry terbatas, fallback |
| Koneksi gagal | ConnectException | service tidak dapat dihubungi | fallback, laporan kesehatan |
Pembeda baris 4xx dan 5xx penting: me-retry permintaan yang salah format hanya membakar waktu dan menambah beban tetangga. Retry masuk akal untuk kegagalan yang transien — gangguan sesaat yang kemungkinan hilang bila dicoba lagi.
Kontrak yang Tidak Boleh Disepelekan #
Body JSON pada skenario 1 dibaca sebagai string mentah; chapter JSON over HTTP (Section Networking) sudah membahas membangun dan mem-parsing JSON manual. Pada proyek nyata, pustaka seperti Jackson menangani pemetaan ke record — yang wajib dijaga adalah kontraknya: field yang diharapkan divalidasi sebelum dipakai, karena respons tetangga bisa berubah bentuk antar rilis. Uji kontrak (consumer-driven contract test) mencegah kejutan itu.
Tiga nasib panggilan di atas — sukses, ditolak, koneksi mati — adalah bahan baku pola yang lebih besar: bagaimana sistem tetap melayani pengguna ketika tetangga sedang sakit lama. Chapter berikutnya membahas resilience patterns: fallback, bulkhead, dan load shedding yang menjaga kegagalan tetap terkurung.
Referensi #
- Oracle: Kelas HttpClient (JDK 25)
- OpenJDK: JEP 444 — Virtual Threads
- microservices.io: Remote Procedure Invocation
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PanggilInventori.java
Unduh berkasimport com.sun.net.httpserver.HttpServer;
import java.io.IOException;
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;
/** svc-pesanan memanggil svc-inventori lewat REST dan menangani kegagalannya. */
public class PanggilInventori {
public static void main(String[] args) throws Exception {
// "svc-inventori" lokal: satu endpoint sehat, satu endpoint gagal
HttpServer inventori = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
inventori.createContext("/stok/A1", exchange -> {
byte[] body = "{\"kode\":\"A1\",\"jumlah\":7}".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
inventori.createContext("/stok/B2", exchange -> {
byte[] body = "{\"error\":\"gudang sibuk\"}".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(503, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
});
inventori.start();
String dasar = "http://127.0.0.1:" + inventori.getAddress().getPort();
HttpClient klien = HttpClient.newHttpClient();
try {
System.out.println("skenario 1 (sukses) : " + panggil(klien, dasar + "/stok/A1"));
System.out.println("skenario 2 (503) : " + panggil(klien, dasar + "/stok/B2"));
} finally {
inventori.stop(0); // service inventori "dimatikan"
}
// Klien baru (pool koneksi baru) menabrak port yang sudah tidak dilayani
HttpClient klienBaru = HttpClient.newHttpClient();
System.out.println("skenario 3 (mati) : " + panggil(klienBaru, dasar + "/stok/A1"));
}
/** Satu panggilan GET dengan penanganan status respons dan kegagalan koneksi. */
static String panggil(HttpClient klien, String url) {
HttpRequest permintaan = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Accept", "application/json")
.GET()
.build();
try {
HttpResponse<String> respons =
klien.send(permintaan, HttpResponse.BodyHandlers.ofString());
if (respons.statusCode() == 200) {
return "200 -> " + respons.body();
}
return respons.statusCode() + " -> stok tidak diketahui, jangan lempar mentah";
} catch (IOException e) {
return "koneksi gagal (" + e.getClass().getSimpleName() + ") -> fallback cache";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "terputus";
}
}
}