RestClient & WebClient
Giriş — HTTP Client Evrimi
Spring uygulamaların neredeyse hepsi dış servislerle konuşur. Ödeme API'si, bildirim servisi, başka bir microservice... Bunların hepsi HTTP çağrısı demek. Spring bu iş için yıllar boyunca farklı araçlar sundu:
RestTemplate (2009): İlk ve en eski. Senkron, bloklayıcı.
WebClient (2017, Spring 5): Reactive, non-blocking. Güçlü ama öğrenme eğrisi dik.
RestClient (2023, Spring Boot 3.2): Modern, fluent API. RestTemplate'in halefi.
Bu derste üçünü de öğrenecek, ne zaman hangisini kullanacağını anlayacak ve production-ready HTTP çağrıları yazmayı göreceksin.
1. RestTemplate: Neden Maintenance Mode'da?
RestTemplate uzun yıllar Spring'in varsayılan HTTP client'ıydı. Hâlâ çalışır, hâlâ kullanılabilir. Ama Spring ekibi artık yeni özellik eklemiyor — sadece bug fix.
RestTemplate'in Limitasyonları
// Klasik RestTemplate kullanımı
RestTemplate restTemplate = new RestTemplate();
// GET — basit ama şık değil
ResponseEntity<User> response = restTemplate.getForEntity(
"https://api.example.com/users/{id}", User.class, 42);
User user = response.getBody();
// POST — verbose, okunabilirlik düşük
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(token);
HttpEntity<CreateUserRequest> request = new HttpEntity<>(body, headers);
ResponseEntity<User> result = restTemplate.postForEntity(
"https://api.example.com/users", request, User.class);Sorunlar:
Senkron ve bloklayıcı — Thread, cevap gelene kadar bekler. Yüksek trafikte thread pool tükenir.
Verbose API — Header eklemek, body ayarlamak çok fazla boilerplate.
Hata yönetimi garip — 4xx/5xx response'larda exception fırlatır (
HttpClientErrorException). Kontrol etmek zor.Fluent API yok — Method chaining desteklemez, konfigürasyon dağınık.
Modern özellikler eksik — Streaming, reactive support yok.
RestTemplate'den Göç
RestTemplate (maintenance)
├── Senkron ihtiyaç → RestClient'a geç (Spring Boot 3.2+)
└── Reactive/async ihtiyaç → WebClient'a geç⚠️ Dikkat: RestTemplate "deprecated" değil, "maintenance mode"da. Mevcut kodun çalışmaya devam eder. Ama yeni proje başlıyorsan RestTemplate kullanma. RestClient veya WebClient seç.
2. RestClient: Modern, Fluent API (Spring Boot 3.2+)
RestClient, Spring Framework 6.1 (Spring Boot 3.2) ile geldi. RestTemplate'in modern, fluent versiyonu. Senkron çalışır ama API'si çok temiz.
Temel Kullanım
// RestClient oluşturma
RestClient restClient = RestClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("Accept", "application/json")
.build();
// Veya basit oluşturma
RestClient restClient = RestClient.create("https://api.example.com");
// Mevcut RestTemplate'den oluşturma (migration için)
RestTemplate restTemplate = new RestTemplate();
RestClient restClient = RestClient.create(restTemplate);GET İsteği
// Basit GET — body olarak al
User user = restClient.get()
.uri("/users/{id}", 42)
.retrieve()
.body(User.class);
// ResponseEntity olarak al (status, headers erişimi)
ResponseEntity<User> response = restClient.get()
.uri("/users/{id}", 42)
.retrieve()
.toEntity(User.class);
System.out.println("Status: " + response.getStatusCode());
System.out.println("User: " + response.getBody());
// Liste olarak al
List<User> users = restClient.get()
.uri("/users")
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {});POST İsteği
// JSON body ile POST
CreateUserRequest request = new CreateUserRequest("Ali", "ali@test.com");
User createdUser = restClient.post()
.uri("/users")
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
.body(User.class);PUT İsteği
UpdateUserRequest updateRequest = new UpdateUserRequest("Ali Yılmaz", "ali@test.com");
User updatedUser = restClient.put()
.uri("/users/{id}", 42)
.contentType(MediaType.APPLICATION_JSON)
.body(updateRequest)
.retrieve()
.body(User.class);DELETE İsteği
// Basit DELETE
restClient.delete()
.uri("/users/{id}", 42)
.retrieve()
.toBodilessEntity(); // Body beklemiyoruz
// DELETE ile response body
ResponseEntity<Void> response = restClient.delete()
.uri("/users/{id}", 42)
.retrieve()
.toBodilessEntity();
System.out.println("Silindi: " + response.getStatusCode());PATCH İsteği
Map<String, String> patchData = Map.of("email", "yeni@test.com");
User patchedUser = restClient.patch()
.uri("/users/{id}", 42)
.contentType(MediaType.APPLICATION_JSON)
.body(patchData)
.retrieve()
.body(User.class);💡 İpucu: RestClient metod zinciri her zaman aynı sırada:
method()→.uri()→.headers()/.body()→.retrieve()→.body()/.toEntity(). Bu tutarlılık kodu çok okunabilir yapar.
Error Handling
RestClient'ın hata yönetimi RestTemplate'ten çok daha iyi:
// Varsayılan davranış: 4xx/5xx → RestClientResponseException fırlatır
// Bunu özelleştirebilirsin:
User user = restClient.get()
.uri("/users/{id}", 42)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
// 4xx hatalarında custom işlem
throw new UserNotFoundException("Kullanıcı bulunamadı: " +
new String(response.getBody().readAllBytes()));
})
.onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
// 5xx hatalarında
throw new ExternalServiceException("Servis hatası: " +
response.getStatusCode());
})
.body(User.class);// Spesifik status code kontrolü
User user = restClient.get()
.uri("/users/{id}", 42)
.retrieve()
.onStatus(status -> status.value() == 404, (request, response) -> {
throw new UserNotFoundException("Kullanıcı #42 bulunamadı");
})
.onStatus(status -> status.value() == 429, (request, response) -> {
throw new RateLimitExceededException("API rate limit aşıldı");
})
.body(User.class);Request Interceptor
Her isteğe otomatik header eklemek, loglama yapmak veya metrikleri toplamak için:
RestClient restClient = RestClient.builder()
.baseUrl("https://api.example.com")
.requestInterceptor((request, body, execution) -> {
// Her isteğe authentication header ekle
request.getHeaders().setBearerAuth(getAccessToken());
// Request loglama
long startTime = System.currentTimeMillis();
ClientHttpResponse response = execution.execute(request, body);
long duration = System.currentTimeMillis() - startTime;
System.out.printf("[HTTP] %s %s → %s (%dms)%n",
request.getMethod(), request.getURI(),
response.getStatusCode(), duration);
return response;
})
.build();Timeout Konfigürasyonu
// Apache HttpClient ile custom timeout
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.core5.util.Timeout;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.ofSeconds(5)) // Connection pool'dan almak
.setResponseTimeout(Timeout.ofSeconds(10)) // Response beklemek
.build();
var httpClient = HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.build();
var requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
RestClient restClient = RestClient.builder()
.baseUrl("https://api.example.com")
.requestFactory(requestFactory)
.build();<!-- Apache HttpClient 5 bağımlılığı -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>JDK HttpClient ile (Bağımlılık Yok)
import org.springframework.http.client.JdkClientHttpRequestFactory;
import java.net.http.HttpClient;
import java.time.Duration;
HttpClient jdkClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
var requestFactory = new JdkClientHttpRequestFactory(jdkClient);
requestFactory.setReadTimeout(Duration.ofSeconds(10));
RestClient restClient = RestClient.builder()
.baseUrl("https://api.example.com")
.requestFactory(requestFactory)
.build();Header Yönetimi ve Authentication
// Builder'da default header'lar
RestClient restClient = RestClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("X-API-Key", "my-secret-key")
.defaultHeader("Accept", "application/json")
.defaultHeader("User-Agent", "MyApp/1.0")
.build();
// İstek bazında header
User user = restClient.get()
.uri("/users/{id}", 42)
.header("X-Request-ID", UUID.randomUUID().toString())
.header("Accept-Language", "tr")
.retrieve()
.body(User.class);
// Bearer token ile
User user = restClient.get()
.uri("/users/me")
.headers(headers -> headers.setBearerAuth(jwtToken))
.retrieve()
.body(User.class);
// Basic Auth
User user = restClient.get()
.uri("/admin/users")
.headers(headers -> headers.setBasicAuth("admin", "password"))
.retrieve()
.body(User.class);Spring Bean Olarak Tanımlama
@Configuration
public class HttpClientConfig {
@Bean
public RestClient userServiceClient() {
return RestClient.builder()
.baseUrl("https://user-service.internal:8080")
.defaultHeader("X-Service-Name", "order-service")
.requestInterceptor(authInterceptor())
.build();
}
@Bean
public RestClient paymentServiceClient() {
return RestClient.builder()
.baseUrl("https://payment-service.internal:8080")
.defaultHeader("X-Service-Name", "order-service")
.requestInterceptor(authInterceptor())
.build();
}
private ClientHttpRequestInterceptor authInterceptor() {
return (request, body, execution) -> {
request.getHeaders().setBearerAuth(getServiceToken());
return execution.execute(request, body);
};
}
private String getServiceToken() {
// Service-to-service token (OAuth2 client credentials vb.)
return "service-token";
}
}// Kullanım — Qualifier ile
@Service
public class OrderService {
private final RestClient userServiceClient;
private final RestClient paymentServiceClient;
public OrderService(
@Qualifier("userServiceClient") RestClient userServiceClient,
@Qualifier("paymentServiceClient") RestClient paymentServiceClient) {
this.userServiceClient = userServiceClient;
this.paymentServiceClient = paymentServiceClient;
}
public User getUser(Long userId) {
return userServiceClient.get()
.uri("/api/users/{id}", userId)
.retrieve()
.body(User.class);
}
public PaymentResult processPayment(PaymentRequest request) {
return paymentServiceClient.post()
.uri("/api/payments")
.body(request)
.retrieve()
.body(PaymentResult.class);
}
}💡 İpucu: Her dış servis için ayrı RestClient bean'i oluştur. Her birinin kendi base URL'i, timeout'u, authentication'ı olur. Bu separation of concerns, microservices mimarisinde çok önemli.
3. WebClient: Non-Blocking, Reactive
WebClient, Spring WebFlux ile birlikte gelir. Non-blocking, reactive bir HTTP client'tır. Yüksek concurrency gerektiren senaryolarda thread'leri bloklamadan binlerce eşzamanlı HTTP çağrısı yapabilirsin.
Bağımlılık
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>💡 İpucu:
spring-boot-starter-webfluxeklemen, uygulamanı reactive yapmaz. Spring MVC ile birlikte kullanabilirsin. WebClient'ı sadece HTTP client olarak kullanıp, uygulamanı servlet stack'te tutabilirsin.
Temel Kullanım
// WebClient oluşturma
WebClient webClient = WebClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("Accept", "application/json")
.build();
// Basit oluşturma
WebClient webClient = WebClient.create("https://api.example.com");GET İsteği
// Mono — tek bir sonuç (0 veya 1)
Mono<User> userMono = webClient.get()
.uri("/users/{id}", 42)
.retrieve()
.bodyToMono(User.class);
// Bloklama ile senkron kullanım (önerilmez ama geçiş döneminde faydalı)
User user = userMono.block();
// Reactive kullanım — subscribe et
userMono.subscribe(
user -> System.out.println("Kullanıcı: " + user.getName()),
error -> System.err.println("Hata: " + error.getMessage())
);
// Flux — liste (0..N)
Flux<User> usersFlux = webClient.get()
.uri("/users")
.retrieve()
.bodyToFlux(User.class);
// Liste olarak al
List<User> users = usersFlux.collectList().block();POST İsteği
// POST — body ile
Mono<User> createdUser = webClient.post()
.uri("/users")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(new CreateUserRequest("Ali", "ali@test.com"))
.retrieve()
.bodyToMono(User.class);
// Reactive chain'de kullanım
createdUser
.doOnSuccess(user -> log.info("Kullanıcı oluşturuldu: {}", user.getId()))
.doOnError(error -> log.error("Hata: {}", error.getMessage()))
.subscribe();PUT ve DELETE
// PUT
Mono<User> updated = webClient.put()
.uri("/users/{id}", 42)
.bodyValue(updateRequest)
.retrieve()
.bodyToMono(User.class);
// DELETE
Mono<Void> deleted = webClient.delete()
.uri("/users/{id}", 42)
.retrieve()
.bodyToMono(Void.class);Error Handling
// onStatus ile hata kontrolü
Mono<User> user = webClient.get()
.uri("/users/{id}", 42)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, response -> {
return response.bodyToMono(String.class)
.flatMap(body -> Mono.error(
new UserNotFoundException("Kullanıcı bulunamadı: " + body)));
})
.onStatus(HttpStatusCode::is5xxServerError, response -> {
return Mono.error(new ExternalServiceException("Servis hatası"));
})
.bodyToMono(User.class);// onErrorResume — hata durumunda fallback
Mono<User> userWithFallback = webClient.get()
.uri("/users/{id}", 42)
.retrieve()
.bodyToMono(User.class)
.onErrorResume(WebClientResponseException.NotFound.class, ex -> {
// 404 durumunda varsayılan kullanıcı döndür
return Mono.just(User.defaultUser());
})
.onErrorResume(Exception.class, ex -> {
// Diğer hatalarda cache'ten al
return getUserFromCache(42);
});Retry Mekanizması
import reactor.util.retry.Retry;
// Basit retry
Mono<User> user = webClient.get()
.uri("/users/{id}", 42)
.retrieve()
.bodyToMono(User.class)
.retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(1)));
// 3 kez dene, aralarında 1 saniye bekle
// Exponential backoff ile retry
Mono<User> user = webClient.get()
.uri("/users/{id}", 42)
.retrieve()
.bodyToMono(User.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
.maxBackoff(Duration.ofSeconds(10)) // Max bekleme süresi
.jitter(0.5) // Rastgele jitter
.filter(ex -> ex instanceof WebClientResponseException.ServiceUnavailable)
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) ->
new ExternalServiceException(
"Servis yanıt vermiyor. " + retrySignal.totalRetries() + " deneme yapıldı.",
retrySignal.failure()
))
);Timeout Konfigürasyonu
import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import reactor.netty.http.client.HttpClient;
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // Connection timeout: 5s
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10)) // Read timeout: 10s
.addHandlerLast(new WriteTimeoutHandler(10))); // Write timeout: 10s
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.baseUrl("https://api.example.com")
.build();
// İstek bazında timeout
Mono<User> user = webClient.get()
.uri("/users/{id}", 42)
.retrieve()
.bodyToMono(User.class)
.timeout(Duration.ofSeconds(5)); // Toplam 5 saniye timeoutExchange Filter Functions
WebClient'ın interceptor'ları — her isteği ve yanıtı değiştirebilirsin:
// Loglama filter'ı
ExchangeFilterFunction logFilter = ExchangeFilterFunction.ofRequestProcessor(request -> {
System.out.printf("[→] %s %s%n", request.method(), request.url());
return Mono.just(request);
});
ExchangeFilterFunction logResponseFilter = ExchangeFilterFunction.ofResponseProcessor(response -> {
System.out.printf("[←] Status: %s%n", response.statusCode());
return Mono.just(response);
});
// Authentication filter
ExchangeFilterFunction authFilter = (request, next) -> {
ClientRequest newRequest = ClientRequest.from(request)
.header("Authorization", "Bearer " + getToken())
.build();
return next.exchange(newRequest);
};
// Hepsini birleştir
WebClient webClient = WebClient.builder()
.baseUrl("https://api.example.com")
.filter(authFilter)
.filter(logFilter)
.filter(logResponseFilter)
.build();WebClient'ı Senkron Kullanma (Spring MVC'de)
Reactive stack kullanmıyorsan ama WebClient'ın güzel API'sini istiyorsan:
@Service
public class UserService {
private final WebClient webClient;
public UserService(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder
.baseUrl("https://api.example.com")
.build();
}
// .block() ile senkron kullanım
public User getUser(Long id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(User.class)
.block(); // Bloklayıcı — reactive olmayan ortamda
}
public List<User> getAllUsers() {
return webClient.get()
.uri("/users")
.retrieve()
.bodyToFlux(User.class)
.collectList()
.block();
}
}⚠️ Dikkat:
.block()çağrısını reactive pipeline içinde (WebFlux controller, Reactor chain) asla kullanma. Deadlock oluşur..block()sadece servlet-based (Spring MVC) uygulamalarda, non-reactive context'te güvenlidir.
4. RestClient vs WebClient vs RestTemplate
Karşılaştırma Tablosu
| Özellik | RestTemplate | RestClient | WebClient |
|---|---|---|---|
| Spring sürümü | 3.0+ (eski) | 6.1+ (Boot 3.2) | 5.0+ (Boot 2.0) |
| Durum | Maintenance | Aktif geliştirme | Aktif geliştirme |
| API stili | Verbose, method-based | Fluent, builder | Fluent, builder |
| Execution | Senkron | Senkron | Async + Senkron |
| Thread modeli | Bloklayıcı | Bloklayıcı | Non-blocking |
| Reactive support | ❌ | ❌ | ✅ (Mono/Flux) |
| Error handling | Exception fırlatır | onStatus() handler | onStatus() handler |
| Streaming | Sınırlı | Sınırlı | ✅ (Flux) |
| Interceptor | ✅ | ✅ | ✅ (Filter) |
| Learning curve | Düşük | Düşük | Orta-Yüksek |
| Dependency | spring-web | spring-web | spring-webflux |
| Connection pool | Apache HC / OkHttp | Apache HC / JDK | Reactor Netty |
Ne Zaman Hangisi?
Yeni proje mi?
├── Evet
│ ├── Spring Boot 3.2+ mu?
│ │ ├── Evet
│ │ │ ├── Senkron yeterli mi? → RestClient ✓
│ │ │ └── Reactive/async gerekli mi? → WebClient ✓
│ │ └── Hayır (eski versiyon) → WebClient ✓
│ └── Spring Boot 2.x mi? → WebClient ✓
└── Mevcut proje, RestTemplate var
├── Sorun yaşıyor musun?
│ ├── Hayır → Değiştirme, çalışıyorsa dokunma
│ └── Evet → RestClient'a migration (Spring Boot 3.2+)
└── Yeni endpoint mi ekliyorsun? → RestClient kullanKarar Özeti
RestClient: Senkron HTTP çağrıları için varsayılan seçim (Spring Boot 3.2+). Basit, temiz, öğrenmesi kolay.
WebClient: Reactive stack, yüksek concurrency, streaming, non-blocking gerektiğinde. Veya Spring Boot 3.2 öncesi.
RestTemplate: Sadece legacy kod. Yenisini yazma.
💡 İpucu: RestClient, internally RestTemplate'in HTTP client infrastructure'ını kullanır. Yani performance farkı yok, sadece API farkı var. Geçiş riski minimal.
5. Inter-Service Communication
Microservices mimarisinde servisler arası iletişim kritik. Hangi client'ı kullanacağın, sistemin genel performansını etkiler.
Senkron İletişim
// Order Service → User Service çağrısı
@Service
public class OrderService {
private final RestClient userServiceClient;
private final RestClient inventoryServiceClient;
public OrderService(
@Qualifier("userServiceClient") RestClient userServiceClient,
@Qualifier("inventoryServiceClient") RestClient inventoryServiceClient) {
this.userServiceClient = userServiceClient;
this.inventoryServiceClient = inventoryServiceClient;
}
public OrderResponse createOrder(OrderRequest request) {
// 1. Kullanıcıyı doğrula
User user = userServiceClient.get()
.uri("/api/users/{id}", request.getUserId())
.retrieve()
.onStatus(status -> status.value() == 404, (req, resp) -> {
throw new UserNotFoundException("Kullanıcı bulunamadı");
})
.body(User.class);
// 2. Stok kontrolü
StockResponse stock = inventoryServiceClient.get()
.uri("/api/stock/{sku}", request.getProductSku())
.retrieve()
.body(StockResponse.class);
if (stock.getAvailable() < request.getQuantity()) {
throw new InsufficientStockException("Yetersiz stok");
}
// 3. Siparişi oluştur
Order order = new Order(user, request);
orderRepository.save(order);
// 4. Stok düş
inventoryServiceClient.post()
.uri("/api/stock/reserve")
.body(new ReserveRequest(request.getProductSku(), request.getQuantity()))
.retrieve()
.toBodilessEntity();
return OrderResponse.from(order);
}
}Paralel Çağrılar (WebClient ile)
Birden fazla servisi aynı anda çağırmak istiyorsan, WebClient ile reactive:
@Service
public class DashboardService {
private final WebClient userClient;
private final WebClient orderClient;
private final WebClient inventoryClient;
public Mono<DashboardData> getDashboard(Long userId) {
// Üç servisi paralel çağır
Mono<User> userMono = userClient.get()
.uri("/api/users/{id}", userId)
.retrieve()
.bodyToMono(User.class);
Mono<List<Order>> ordersMono = orderClient.get()
.uri("/api/orders?userId={id}", userId)
.retrieve()
.bodyToFlux(Order.class)
.collectList();
Mono<Integer> stockCountMono = inventoryClient.get()
.uri("/api/stock/count")
.retrieve()
.bodyToMono(Integer.class);
// Hepsini birleştir — paralel çalışır!
return Mono.zip(userMono, ordersMono, stockCountMono)
.map(tuple -> new DashboardData(
tuple.getT1(), // User
tuple.getT2(), // Orders
tuple.getT3() // Stock count
));
}
}Bu örnekte üç HTTP çağrısı aynı anda yapılır. Senkron olsa: 200ms + 150ms + 100ms = 450ms beklersin. Paralel olsa: max(200, 150, 100) = 200ms. Büyük fark!
⚠️ Dikkat: Microservices'te senkron çağrılar "cascading failure" riski taşır. User Service çöktüğünde Order Service de çöker. Circuit breaker pattern (Resilience4j) kullan. Bir sonraki bölümde göreceğiz.
6. Retry Pattern ve Exponential Backoff
Dış servisler her zaman erişilebilir değildir. Geçici ağ sorunları, servis restart'ları... Retry pattern bu durumlar için şart.
RestClient ile Retry (Manuel)
@Service
public class ResilientUserService {
private final RestClient restClient;
private static final int MAX_RETRIES = 3;
public User getUserWithRetry(Long userId) {
int attempt = 0;
while (attempt < MAX_RETRIES) {
try {
return restClient.get()
.uri("/api/users/{id}", userId)
.retrieve()
.body(User.class);
} catch (RestClientException e) {
attempt++;
if (attempt >= MAX_RETRIES) {
throw new ExternalServiceException(
"User servisi yanıt vermiyor. " + attempt + " deneme yapıldı.", e);
}
// Exponential backoff: 1s, 2s, 4s
long waitMs = (long) Math.pow(2, attempt - 1) * 1000;
try {
Thread.sleep(waitMs);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException(ie);
}
}
}
throw new ExternalServiceException("Beklenmeyen durum");
}
}Spring Retry ile
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>@Configuration
@EnableRetry
public class RetryConfig {}@Service
public class UserServiceWithSpringRetry {
private final RestClient restClient;
@Retryable(
retryFor = RestClientException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 10000)
)
public User getUser(Long userId) {
return restClient.get()
.uri("/api/users/{id}", userId)
.retrieve()
.body(User.class);
}
@Recover
public User getUserFallback(RestClientException e, Long userId) {
// Tüm retry'lar başarısız olursa
log.warn("User servisi erişilemez, cache'ten dönüyoruz. userId={}", userId);
return userCacheService.getCachedUser(userId)
.orElseThrow(() -> new ExternalServiceException(
"User servisi erişilemez ve cache'te yok", e));
}
}WebClient ile Retry (Reactor)
public Mono<User> getUserReactive(Long userId) {
return webClient.get()
.uri("/api/users/{id}", userId)
.retrieve()
.bodyToMono(User.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
.maxBackoff(Duration.ofSeconds(10))
.jitter(0.5)
.filter(this::isRetryable) // Sadece belirli hataları retry et
.doBeforeRetry(signal ->
log.warn("Retry #{}: {}", signal.totalRetries(),
signal.failure().getMessage()))
)
.onErrorResume(ex -> {
log.error("Tüm retry'lar başarısız", ex);
return Mono.error(new ExternalServiceException("Servis erişilemez", ex));
});
}
private boolean isRetryable(Throwable ex) {
if (ex instanceof WebClientResponseException wcre) {
// 5xx hatalarını retry et, 4xx hatalarını etme
return wcre.getStatusCode().is5xxServerError();
}
// Connection hataları retry edilebilir
return ex instanceof ConnectException ||
ex instanceof TimeoutException;
}💡 İpucu: Her hatayı retry etme! 400 Bad Request retry edilmemeli — her seferinde aynı sonucu alırsın. 500 Internal Server Error veya connection timeout retry edilebilir.
filter()ile retry edilecek hataları sınırla.
7. Gerçek Dünya Örneği: Tam Bir Service Client
Tüm best practice'leri birleştiren production-ready bir service client:
@Configuration
public class PaymentClientConfig {
@Value("${payment.service.url}")
private String paymentServiceUrl;
@Value("${payment.service.api-key}")
private String apiKey;
@Bean
public RestClient paymentClient() {
// Timeout konfigürasyonu
HttpClient jdkClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
var requestFactory = new JdkClientHttpRequestFactory(jdkClient);
requestFactory.setReadTimeout(Duration.ofSeconds(10));
return RestClient.builder()
.baseUrl(paymentServiceUrl)
.requestFactory(requestFactory)
.defaultHeader("X-API-Key", apiKey)
.defaultHeader("Content-Type", "application/json")
.defaultHeader("Accept", "application/json")
.requestInterceptor(loggingInterceptor())
.defaultStatusHandler(HttpStatusCode::is5xxServerError,
(request, response) -> {
throw new PaymentServiceUnavailableException(
"Payment servisi hatası: " + response.getStatusCode());
})
.build();
}
private ClientHttpRequestInterceptor loggingInterceptor() {
return (request, body, execution) -> {
String requestId = UUID.randomUUID().toString().substring(0, 8);
request.getHeaders().set("X-Request-ID", requestId);
long start = System.nanoTime();
try {
ClientHttpResponse response = execution.execute(request, body);
long durationMs = (System.nanoTime() - start) / 1_000_000;
log.info("[Payment] {} {} → {} ({}ms) [{}]",
request.getMethod(), request.getURI(),
response.getStatusCode(), durationMs, requestId);
return response;
} catch (IOException e) {
long durationMs = (System.nanoTime() - start) / 1_000_000;
log.error("[Payment] {} {} → ERROR ({}ms) [{}]: {}",
request.getMethod(), request.getURI(),
durationMs, requestId, e.getMessage());
throw e;
}
};
}
}@Service
public class PaymentService {
private final RestClient paymentClient;
public PaymentService(@Qualifier("paymentClient") RestClient paymentClient) {
this.paymentClient = paymentClient;
}
@Retryable(
retryFor = PaymentServiceUnavailableException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
public PaymentResult charge(PaymentRequest request) {
return paymentClient.post()
.uri("/api/v1/charges")
.body(request)
.retrieve()
.onStatus(status -> status.value() == 402, (req, resp) -> {
String body = new String(resp.getBody().readAllBytes());
throw new InsufficientFundsException("Yetersiz bakiye: " + body);
})
.onStatus(status -> status.value() == 422, (req, resp) -> {
String body = new String(resp.getBody().readAllBytes());
throw new InvalidPaymentException("Geçersiz ödeme bilgisi: " + body);
})
.body(PaymentResult.class);
}
public PaymentStatus getPaymentStatus(String paymentId) {
return paymentClient.get()
.uri("/api/v1/charges/{id}/status", paymentId)
.retrieve()
.onStatus(status -> status.value() == 404, (req, resp) -> {
throw new PaymentNotFoundException(
"Ödeme bulunamadı: " + paymentId);
})
.body(PaymentStatus.class);
}
@Retryable(
retryFor = PaymentServiceUnavailableException.class,
maxAttempts = 2,
backoff = @Backoff(delay = 500)
)
public RefundResult refund(String paymentId, RefundRequest request) {
return paymentClient.post()
.uri("/api/v1/charges/{id}/refunds", paymentId)
.body(request)
.retrieve()
.body(RefundResult.class);
}
@Recover
public PaymentResult chargeFallback(PaymentServiceUnavailableException e,
PaymentRequest request) {
log.error("Payment servisi erişilemez, ödeme kuyruğa alınıyor", e);
// Ödemeyi kuyruğa at, sonra tekrar dene
paymentQueueService.enqueue(request);
return PaymentResult.pending("Ödeme işleniyor, birazdan tamamlanacak");
}
}Yaygın Hatalar ve Çözümleri
1. .block() Reactive Chain İçinde
// YANLIŞ — Deadlock riski!
@GetMapping("/users/{id}") // WebFlux controller
public Mono<User> getUser(@PathVariable Long id) {
User user = webClient.get()
.uri("/api/users/{id}", id)
.retrieve()
.bodyToMono(User.class)
.block(); // ❌ Reactive context'te block() = DEADLOCK
return Mono.just(user);
}
// DOĞRU
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable Long id) {
return webClient.get()
.uri("/api/users/{id}", id)
.retrieve()
.bodyToMono(User.class); // ✅ Mono'yu doğrudan döndür
}2. Timeout Ayarlanmamış
// YANLIŞ — Timeout yok, dış servis yanıt vermezse sonsuza kadar bekler
RestClient restClient = RestClient.create("https://slow-api.com");
// DOĞRU — Mutlaka timeout konfigüre et
// (Örnekler yukarıda detaylı gösterildi)3. Generic Type Erasure
// YANLIŞ — Runtime'da List<User> bilgisi kaybolur
List<User> users = restClient.get()
.uri("/users")
.retrieve()
.body(List.class); // ❌ List<LinkedHashMap> döner, List<User> değil!
// DOĞRU — ParameterizedTypeReference kullan
List<User> users = restClient.get()
.uri("/users")
.retrieve()
.body(new ParameterizedTypeReference<List<User>>() {}); // ✅4. RestClient Bean Singleton Thread Safety
// RestClient thread-safe'tir — singleton bean olarak güvenle kullanabilirsin
@Bean
public RestClient apiClient() {
return RestClient.builder()
.baseUrl("https://api.example.com")
.build();
// ✅ Tüm thread'ler aynı instance'ı paylaşır, sorun yok
}Özet
RestTemplate maintenance mode'da — yeni projede kullanma, mevcut projedeki kodu hemen değiştirmen de gerekmez
RestClient (Spring Boot 3.2+), senkron HTTP çağrıları için varsayılan seçim — fluent API, temiz error handling, interceptor desteği, öğrenmesi kolay
WebClient reactive/async senaryolar için güçlü —
Mono/Fluxile non-blocking, paralel çağrılar, streaming, yüksek concurrencyHer dış servis için ayrı RestClient/WebClient bean oluştur — base URL, timeout, auth hepsi ayrı konfigüre edilir
Retry pattern production'da zorunlu —
@Retryableveya ReactorretryWhen()ile exponential backoff, sadece retryable hataları (5xx, timeout) retry etTimeout mutlaka konfigüre et — varsayılan sınırsız, dış servis yanıt vermezse thread sonsuza kadar bloklanır
AI Asistan
Sorularını yanıtlamaya hazır