Tampilan
Kenapa penting
Hampir tidak ada backend yang hidup sendirian. Layanan checkout memanggil layanan stok, gateway pembayaran, dan penyedia ongkos kirim; layanan notifikasi memanggil API WhatsApp atau email. Setiap panggilan keluar adalah titik rawan: jaringan lambat, server lawan error, atau format response berubah. Developer junior biasanya bisa membuat panggilan HTTP "jalan di laptop", tetapi yang membedakan engineer siap kerja adalah client yang punya timeout, menerjemahkan error dengan benar, bisa dilacak lewat log, dan teruji tanpa bergantung pada server sungguhan. Bab ini menyiapkan fondasi itu sebelum kamu belajar resilience di bab berikutnya.
Konsep inti
RestClient: client HTTP sinkron yang modern
RestClient (paket org.springframework.web.client) adalah API fluent untuk memanggil HTTP secara blocking. Kamu membuatnya sekali lewat builder, lalu memakainya bersama-sama — objek RestClient aman dipakai banyak thread.
java
// Potongan — konfigurasi bean di aplikasi Spring Boot 4
@Configuration
class InventoryClientConfig {
@Bean
RestClient inventoryRestClient(RestClient.Builder builder) {
return builder
.baseUrl("https://inventory.internal.example.com")
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader("X-Client-Name", "checkout-service")
.build();
}
}Di Spring Boot, minta RestClient.Builder lewat injeksi (bukan RestClient.builder() statis) supaya kamu mendapat builder yang sudah dikonfigurasi Boot: message converter JSON (Jackson 3), observability, dan pengaturan client dari application.yaml. Builder ini prototype — setiap injeksi mendapat salinan baru, jadi aman dimodifikasi.
Memanggil endpoint lalu mengubah body menjadi objek cukup dengan retrieve().body(...):
java
// Potongan
record StockResponse(String sku, int quantity) {}
StockResponse stock = inventoryRestClient.get()
.uri("/stocks/{sku}", sku) // variabel URI di-encode otomatis
.retrieve()
.body(StockResponse.class);Butuh status dan header juga? Pakai .toEntity(StockResponse.class) yang mengembalikan ResponseEntity. Untuk mengirim data, gunakan post(), .contentType(MediaType.APPLICATION_JSON), dan .body(objek).
Menerjemahkan error dengan onStatus
Secara default retrieve() melempar subclass RestClientException untuk status 4xx/5xx — misalnya HttpClientErrorException.NotFound untuk 404. Membiarkan exception teknis ini bocor ke lapisan service membuat kode domain bergantung pada detail HTTP. Lebih baik terjemahkan di batas client:
java
// Potongan
StockResponse stock = inventoryRestClient.get()
.uri("/stocks/{sku}", sku)
.retrieve()
.onStatus(status -> status.value() == 404, (request, response) -> {
throw new ProductNotFoundException(sku);
})
.onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
throw new InventoryUnavailableException("Layanan stok error: " + response.getStatusCode());
})
.body(StockResponse.class);Pembedaan 4xx dan 5xx penting untuk bab berikutnya: 5xx dan timeout sering bersifat sementara (layak di-retry), sedangkan 4xx biasanya kesalahan permintaanmu sendiri (retry tidak akan menolong).
Timeout itu wajib, bukan opsional
Ada dua timeout yang perlu kamu kenal:
| Timeout | Artinya | Contoh nilai |
|---|---|---|
| Connect timeout | Batas waktu membangun koneksi TCP ke server | 1–2 detik |
| Read timeout | Batas waktu menunggu data response setelah terhubung | Sesuai SLA lawan, mis. 2–5 detik |
Tanpa timeout, satu layanan lawan yang macet membuat thread-mu menunggu terlalu lama. Request masuk berikutnya ikut tertahan, pool thread habis, dan aplikasimu ikut tumbang — padahal masalahnya ada di layanan lain. Program Java murni berikut mensimulasikan layanan stok yang butuh 2 detik, sementara client hanya mau menunggu 500 ms:
java
import com.sun.net.httpserver.HttpServer;
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.net.http.HttpTimeoutException;
import java.time.Duration;
public class TimeoutDemo {
public static void main(String[] args) throws Exception {
// Server palsu: layanan stok yang lambat (2 detik)
HttpServer server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/stocks/SKU-1", exchange -> {
try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
byte[] body = "{\"sku\":\"SKU-1\",\"qty\":7}".getBytes();
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
exchange.close();
});
server.start();
String url = "http://localhost:" + server.getAddress().getPort() + "/stocks/SKU-1";
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(1))
.build();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.timeout(Duration.ofMillis(500)) // batas menunggu response
.build();
long start = System.nanoTime();
try {
client.send(request, HttpResponse.BodyHandlers.ofString());
} catch (HttpTimeoutException e) {
long ms = (System.nanoTime() - start) / 1_000_000;
System.out.println("Gagal cepat: " + e.getMessage() + " (" + (ms < 1000 ? "< 1 detik" : ms + " ms") + ")");
} finally {
server.stop(0);
}
}
}text
Gagal cepat: request timed out (< 1 detik)Gagal cepat jauh lebih baik daripada menunggu tanpa batas: kamu bisa mengembalikan error yang jelas, mencoba lagi, atau memakai fallback. Di Spring, timeout disetel pada request factory di bawah RestClient:
java
// Potongan — timeout eksplisit di kode
HttpClient jdkClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(1))
.build();
JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory(jdkClient);
factory.setReadTimeout(Duration.ofSeconds(3));
RestClient client = builder
.baseUrl("https://inventory.internal.example.com")
.requestFactory(factory)
.build();Di Spring Boot 4 kamu juga bisa menyetelnya lewat properti, berlaku untuk semua client yang dibuat Boot:
yaml
spring:
http:
clients:
connect-timeout: 1s
read-timeout: 3sHTTP interface client: deklaratif dengan @HttpExchange
Menulis get().uri(...).retrieve() berulang-ulang cepat membosankan. Spring menyediakan HTTP interface client: kamu mendeklarasikan interface Java, Spring membuat implementasinya (proxy) di atas RestClient.
java
// Potongan — paket org.springframework.web.service.annotation
@HttpExchange("/stocks")
public interface InventoryApi {
@GetExchange("/{sku}")
StockResponse getStock(@PathVariable String sku);
@PostExchange("/reservations")
ReservationResponse reserve(@RequestBody ReservationRequest request);
}Spring Framework 7 menambahkan HTTP service registry: interface dikelompokkan dalam group (biasanya satu group per host tujuan), lalu didaftarkan dengan @ImportHttpServices (paket org.springframework.web.service.registry). Setiap interface otomatis menjadi bean yang bisa kamu injeksi.
java
// Potongan
@Configuration
@ImportHttpServices(group = "inventory", types = InventoryApi.class)
class HttpClientsConfig {
}Spring Boot 4 lalu menghubungkan group ke URL dan timeout lewat properti spring.http.serviceclient.<group>:
yaml
spring:
http:
serviceclient:
inventory:
base-url: https://inventory.internal.example.com
connect-timeout: 1s
read-timeout: 3sButuh kustomisasi lebih jauh per group (interceptor, header default)? Daftarkan bean RestClientHttpServiceGroupConfigurer dan gunakan groups.filterByName("inventory").forEachClient(...). Sebelum Spring Framework 7, kamu harus membuat proxy manual dengan HttpServiceProxyFactory — kamu masih akan menemukannya di kode lama.
RestClient, RestTemplate, atau WebClient?
| Client | Gaya | Kapan dipakai |
|---|---|---|
RestClient | Sinkron, fluent | Default untuk aplikasi Spring MVC (termasuk dengan virtual thread) |
RestTemplate | Sinkron, template method | Kode lama; tidak disarankan untuk kode baru |
WebClient | Reaktif (Mono/Flux) | Aplikasi Spring WebFlux atau kebutuhan streaming reaktif |
Dengan virtual thread (yang sudah kamu pelajari di M10), panggilan blocking lewat RestClient tetap efisien, sehingga kamu tidak perlu beralih ke reaktif hanya demi skalabilitas I/O.
Correlation id: benang merah antar-layanan
Saat checkout gagal, kamu perlu melihat log layanan order, stok, dan pembayaran untuk request yang sama. Correlation id adalah ID unik (misalnya UUID) yang dibuat di pintu masuk dan diteruskan lewat header ke setiap panggilan keluar. Cara paling rapi adalah ClientHttpRequestInterceptor agar tidak ada panggilan yang lupa:
java
// Potongan
class CorrelationIdInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
String correlationId = MDC.get("correlationId"); // diisi filter di pintu masuk
if (correlationId != null) {
request.getHeaders().set("X-Correlation-Id", correlationId);
}
return execution.execute(request, body);
}
}
// saat membangun client:
builder.requestInterceptor(new CorrelationIdInterceptor());Tracing otomatis
Di M12 kamu akan memakai Micrometer Tracing, yang meneruskan header standar W3C traceparent secara otomatis untuk client yang dibuat dari builder Boot. Correlation id manual tetap berguna untuk memahami konsepnya dan untuk sistem yang belum memakai tracing.
Menguji client tanpa server sungguhan
Test yang memanggil server sungguhan lambat dan rapuh. MockRestServiceServer (dari spring-test) mencegat request dari RestClient.Builder dan mencocokkannya dengan ekspektasi:
java
// Potongan — test unit client
class InventoryClientTest {
@Test
void getStock_mengembalikanJumlahStok() {
RestClient.Builder builder = RestClient.builder().baseUrl("https://inventory.test");
MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build();
InventoryClient client = new InventoryClient(builder.build());
server.expect(requestTo("https://inventory.test/stocks/SKU-1"))
.andExpect(method(HttpMethod.GET))
.andRespond(withSuccess("""
{"sku":"SKU-1","quantity":7}
""", MediaType.APPLICATION_JSON));
assertThat(client.getStock("SKU-1").quantity()).isEqualTo(7);
server.verify();
}
}Untuk menguji perilaku jaringan nyata (delay, koneksi diputus, timeout), alat seperti WireMock menjalankan server HTTP palsu sungguhan di port lokal. Gunakan MockRestServiceServer untuk test cepat logika mapping, dan WireMock saat kamu perlu membuktikan timeout benar-benar bekerja.
Contoh di dunia backend
Layanan checkout memeriksa stok sebelum membuat pesanan. Client dibungkus kelas kecil yang menyembunyikan detail HTTP dari service domain:
java
// Potongan — client stok di layanan checkout
@Component
public class InventoryClient {
private final RestClient restClient;
public InventoryClient(RestClient inventoryRestClient) {
this.restClient = inventoryRestClient;
}
public StockResponse getStock(String sku) {
try {
return restClient.get()
.uri("/stocks/{sku}", sku)
.retrieve()
.onStatus(status -> status.value() == 404, (req, res) -> {
throw new ProductNotFoundException(sku);
})
.onStatus(HttpStatusCode::is5xxServerError, (req, res) -> {
throw new InventoryUnavailableException("Stok error " + res.getStatusCode());
})
.body(StockResponse.class);
} catch (ResourceAccessException e) {
// Timeout atau koneksi gagal dibungkus RestClient menjadi ResourceAccessException
throw new InventoryUnavailableException("Layanan stok tidak merespons", e);
}
}
}
@Service
public class CheckoutService {
private final InventoryClient inventoryClient;
public CheckoutService(InventoryClient inventoryClient) {
this.inventoryClient = inventoryClient;
}
public void ensureAvailable(String sku, int requested) {
StockResponse stock = inventoryClient.getStock(sku);
if (stock.quantity() < requested) {
throw new OutOfStockException(sku, requested, stock.quantity());
}
}
}Perhatikan pembagiannya: CheckoutService hanya mengenal exception domain (ProductNotFoundException, InventoryUnavailableException, OutOfStockException). @RestControllerAdvice yang kamu pelajari di level Spring Boot lalu memetakannya ke 404, 503, atau 409 untuk klien. Di bab berikutnya, InventoryUnavailableException inilah yang akan kita retry dan lindungi dengan circuit breaker.
Pertanyaan interview
1. Kenapa timeout harus disetel eksplisit pada setiap HTTP client?
Karena tanpa batas yang jelas, panggilan ke layanan yang macet bisa menahan thread jauh lebih lama dari yang wajar. Thread yang tertahan menumpuk, pool habis, dan aplikasimu ikut tidak responsif — kegagalan menjalar (cascading failure). Connect timeout membatasi waktu membangun koneksi, read timeout membatasi waktu menunggu response.
2. Apa beda RestClient, RestTemplate, dan WebClient?
Ketiganya client HTTP Spring. RestClient adalah API sinkron fluent modern dan pilihan default untuk aplikasi MVC. RestTemplate adalah API sinkron lama yang dipertahankan untuk kode yang ada. WebClient adalah client reaktif berbasis Mono/Flux untuk stack WebFlux. HTTP interface client bisa berjalan di atas RestClient maupun WebClient.
3. Bagaimana kamu melacak satu request yang melewati tiga layanan?
Buat correlation id (atau trace id) di pintu masuk, simpan di MDC agar tercetak di setiap baris log, dan teruskan lewat header pada setiap panggilan keluar memakai interceptor. Di production, Micrometer Tracing mengotomatiskannya dengan header W3C traceparent dan mengirim span ke backend tracing.
Jebakan umum
- Memakai
RestClient.create()ataunew RestTemplate()apa adanya tanpa timeout — aman di laptop, bencana saat layanan lawan macet. - Membuat
RestClientbaru di setiap pemanggilan method. Buat sekali sebagai bean lalu pakai ulang; objeknya thread-safe. - Membiarkan
HttpClientErrorExceptionbocor sampai ke controller sehingga klien menerima 500 untuk kasus yang seharusnya 404 atau 503. - Menggabungkan URL dengan konkatenasi string (
"/stocks/" + sku). Pakai variabel URI{sku}agar nilai di-encode dengan benar. - Mencatat (log) seluruh header atau body request — token
Authorizationdan data pribadi ikut terekam. - Mempercayai correlation id dari klien untuk keputusan keamanan. Itu hanya label pelacakan.
Ringkasan
RestClientadalah client HTTP sinkron yang direkomendasikan; buat sekali lewatRestClient.Builderdari Boot dan pakai ulang.retrieve().body(...)mengubah response menjadi objek;onStatusmenerjemahkan 4xx/5xx menjadi exception domain.- Connect timeout dan read timeout wajib disetel — lewat request factory atau properti
spring.http.clients.*. - HTTP interface client (
@HttpExchange,@GetExchange) +@ImportHttpServicesmembuat client deklaratif per group, dengan konfigurasispring.http.serviceclient.<group>.*. - Teruskan correlation id lewat interceptor dan uji client dengan
MockRestServiceServer(atau WireMock untuk perilaku jaringan).