Spring WebClient: Yeni Nesil Reactive HTTP Client
WebClient, HTTP isteklerini gerçekleştirmek için non-blocking ve asenkron işlemleri destekleyen reactive bir HTTP client’dır. Reactive işlemlerin yanında senkron ve blocking istekleri de destekler. Spring WebFlux ile birlikte gelen WebClient uçtan uca tüm isteklerimizi gerçekleştirebileceğimiz senkron/asenkron yapıya sahip bir HTTP client yapısı sunar.
Spring Webflux ile reactive uygulamalar tasarlayabileceğimizi bir önceki yazımda anlatmıştım. Bir uygulamanın reactive olması için uçtan uca işlemlerin reactive yapıya uygun olması gerektiğini tekrar hatırlarsak, başka servislere atacağımız Rest isteklerde ve testlerimizde kullanacağımız Http isteklerinin de reactive bir http client altyapısına sahip olması gerektiğini söyleyebiliriz. Bu serinin, WebClient’dan da örnekler içeren projesine Github üzerinden ulaşabilirsiniz.
Hatırlatma:
Mono: 0 ya da 1 tane event içerebilen Publisher’lar için kullanılır.
Flux: 0..N tane event’ı içeren publisher’lar için kullanılır.
Neden RestTemplate yerine tercih edilsin?
Belki duymuşsunuzdur ancak RestTemplate
ömrünün sonuna doğru yaklaşıyor. Spring’in de son dökümanındaki notuna göre RestTemplate bakım moduna alınıyor ve artık yeni özellikler gelmeyeceği belirtiliyor.
WebClient RestTemplate’e alternatif olarak geliştirilen senkron, asenkron ve non-blocking operasyonları destekler ve streaming özellikleri barındırır, modern ve yenilikçi metotlar sunarak reactive programlamayı destekler.
Tanımlamalarımızdan da yola çıkarak ikisinin arasında büyük farklar olduğunu hızlıca görebilsek de bir özet geçmekte fayda var. Özellikle rest template’in bakım moduna alınacağını duyduktan sonra alternatiflerine bakmakta fayda var. WebClient en güçlü alternatif olarak karşımıza çıkıyor.
Webclient vs RestTemplate
Her istek için RestTemplate
yeni bir Thread oluşturur ve bu isteğe yanıt gelene kadar kullanır. Bir istek gönderdikten sonra RestTemplate, daha önce tanımlanmış bir timout’a ulaşılana kadar response bekler. Bu işlem Thread’i bloklar. Uygulamada çok sayıda istek atılıyor ise bununla orantılı olarak çok sayıda thread kullanılacak ve connection oluşacaktır. Bu da maliyet ve server’a yük olarak geri dönecektir ve bu sayıların artması performansta problemlere neden olabilir.
RestTemplate’in aksine, WebClient asenkron ve non-blockingdir. Spring WebFlux’ın desteklediği gibi reactive programlamayı destekler ve event-driven mimarıyı örnek alır. WebClient ile bir istek atıldığında isteği atan thread bloklanmadan hayatına devam eder, dolayısıyla blocking bir yapıda çalışmaz, böylece asenkron bir yapı sunar. Önemli bir not ise WebClient block()
metoduyla birlikte RestTemplate benzeri senkron işlemleri de desteklemektedir.
“Tamam da hangisini kullanayım?” diyenler için bir kısa açıklama geçelim. RestTemplate benim de uğraştığım çoğu projede çok başarılı şekilde görevini yerine getirdi ve çoğu yerde de görevini yerine getirmeye devam edecek. Dolayısıyla hemen RestTemplate’i başka bir HttpClient ile değiştirmeliyim diye bir algıya kapılmaya gerek yok. Ancak özellikle yeni projelerde RestTemplate yerine alternatifleri tercih edilebilir. Spring’in önerdiği alterenatif şu an için WebClient olarak gözüküyor. Ancak Feign, OKHttp, Retrofit 2 gibi alternatifleride göz ardı etmemek gerekiyor.
WebClient Kullanımı
WebClient Spring Web Flux’ın bir parçası olduğu için projemize “spring-boot-starter-webflux
” bağımlılığını eklemeliyiz.
implementation(“org.springframework.boot:spring-boot-starter-webflux:2.6.2”) //Gradle için
WebClient’ı oluşturmanın üç farklı yolu vardır. Bunlardan ilki ve en kolayı create()
factory metoduyla default ayarlarda bir WebClient instance oluşturmaktır.
WebClient webClient = WebClient.create();
Biraz daha esnek olarak yine create factoy metodunu bu kez URL ile birlikte WebClient instance’ı oluşturabiliriz.
WebClient webClient = WebClient.create("http://localhost:8081");
Bu yöntemdekine benzer şekilde eğer tek bir servis ile iletişim kurmak istiyorsak bir Bean
oluşturabilir ve kullandığımız yerlerde artık URL bilgisini vermeden işlemlerimize devam edebiliriz.
Daha esnek ve kullanışlı bir yol ise WebClient.Builder
ile birlikte oluşturmaktır. Böylece bütün temel ayarları builder ile bir defa tanımlayabilir ve sonrasında build edilmiş instance’ı kullanabiliriz. Her seferinde yeniden ayarları düzenlememize gerek kalmadan uygulamamızı geliştirmeye devam edebiliriz.
Hadi şimdi bunun kullanımına göz atalım ve genel configurasyonlar ile birlikte WebClient.Builder
üzerinden bir instance oluşturalım.
@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl("http://localhost:8081")
.defaultHeaders(header -> header.setBasicAuth(user, password))
.defaultCookie(DEFAULT_COOKIE, COOKIE_VALUE)
.defaultUriVariables(
Collections.singletonMap("url","gokhana.dev"))
.build();
}
WebClient get()
, post()
, put()
, patch()
, delete()
, options()
veya head()
yöntemleri destekler.
İsteklerimizi hazırlamadan önce birkaç yapıya göz atmamız gerekiyor.
uri()
ile birlikte path’i, path variable’ları ve request parametrelerini belirtebiliriz.headers()
ile birlikte isteği atarken header’ları gönderebiliriz.cookies()
ile birlikte Cookie’lerimizi tanımlayabiliriz.retrieve()
metodunu isteği gerçekleştirmek ve server’dan aldığımız response’u veya hataları okumak için kullanabilirz.exchange()
ile birlikte daha kontrollü bir response işleme yapısı kurabiliriz.exchangeToFlux()
veyaexchangeToMono()
metotlarını kullanarak direkt Flux ya da Mono objelerine dönüşüm sağlayabiliriz.retryWhen()
ile birlikte retry mekanizması kurabiliriz.
Son olarak attığımız işleme gelen cevabıbodyToMono()
ile Mono’ya ve bodyToFlux()
ile Flux’a dönüştürme işlemleri gerçekleştirebiliriz.
WebClient ile Asenkron İstekler Göndermek
WebClient Bean’imizi oluşturduğumuza göre onu kullanarak hızlıca asenkron istek atabilen bir metot yazabiliriz.
Mono<User> response = webClient
.post()
.uri(uriBuilder ->
uriBuilder.pathSegment("users", "{userId}").build(userId))
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromPublisher(Mono.just(userObj)))
.retrieve()
.bodyToMono(User.class)
.retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(3)));
WebClient ile Senkron İstekler Göndermek
Bir işlemi senkron gerçekleştirebilmek için response’da dönen objeyi .toEntity()
metoduyla belirtebilir .block()
metoduyla isteğin blocking ve senkron olmasını sağlayabiliriz.
Mono<User> response = webClient
.get()
.uri(uriBuilder ->
uriBuilder.pathSegment("users", "{userId}").build(userId))
.header("Authorization", "Bearer token")
.retrieve()
.toEntity(User.class)
.block();
Örnekte gösterilen yapı ile beraber restTemplate’in yerine de kullanılabilir bir yapı elde etmiş oluyoruz.
WebClient ile Error Handling
Hataları kontrol edip, kendi custom exception’larımızı da yaratabileceğimiz yöntemlerden birisi .onStatus()
ile hata durumlarını handle etmektir. .onStatus()
metodu ile birlikte client ya da server tarafından oluşan hataları handle edebiliriz.
Mono<User> response = webClient
.post()
.body(BodyInserters.fromPublisher(Mono.just(userObj)))
.retrieve()
.onStatus(HttpStatus::is5xxServerError,
error -> Mono.error(new RuntimeException("Error message")))
.bodyToMono(User.class);
doOnError()
Mono bir hatayla tamamlandığında tetiklenir.onErrorResume()
herhangi bir hata oluştuğunda devreye girmek ve işlemin devam edebilmesi sağlamak için kullanılabilir.
public Mono<User> getUser() {
return webClient()
.get()
.retrieve()
.bodyToMono(User.class)
.doOnError(error -> log.error("There is an error while sending request {}", error.getMessage()))
.onErrorResume(error -> Mono.just(new User()))
}
Ve son olarak çok tercih edilmese de bodyToMono
’da error handling yapabiliyoruz.
Mono<String> response = webClient
.get()
.exchangeToMono(result -> {
if (!result.statusCode().is2xxSuccessful()) {
return Mono.just("Error response");
}
return result.bodyToMono(String.class);
});
WebClient ile birlikte senkron, asenkron operasyonların yanında error handling gibi konulara değinmiş olduk. Bir sonraki adım ise uygulama geliştirirken olmazsa olmazlarımızdan reactive uygulamalarda test yazma konusunu anlamak olmalı.
Test Yazarken WebTestClient Kullanımı
Özellikle reactive yapıya sahip uygulamalarımızın rest servislerini test edebilmek için WebClient en uygun araçlardan biridir.
WebTestClient
, WebFlux endpointlerini test etmek için kullanılır ve WebClient gibi davranır. İsteği gerçekleştirmek için exchange()
yöntemini kullanmamız gerekiyor. exchange()
yöntemi, response’u doğrulamak için expectStatus
, expectHeader
ve expectBody
gibi bazı işimize yarayacak metotları içerir.
@Test
public void testUserCreate() {
UserRequest request = new UserRequest(1,"gokhanadev");
webTestClient.post().uri("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(request), UserRequest.class)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody()
.jsonPath("$.name").isNotEmpty()
.jsonPath("$.name").isEqualTo("gokhanadev");
}
WebTestClient
ile oluşturduğumuz istekler tamamen WebClient gibi reactive çalışır.
Sonuç
Uygulamanızda sadece WebClient kullanmak uygulamanızın reactive olduğu anlamına gelmez. Bir uygulamanın reactive özellikleri taşıyabilmesi için uçtan uca asenkron ve non-blocking operasyonları içerebilmesi, eğer kullanıyor iseniz veritabanınızın da reactive desteği bulunması gerekir. WebClient Http isteklerinizi reactive olarak atabilmenizi sağlayan ve RestTemplate’e alternatif olarak sunulan en önemli Spring WebFlux kütüphanelerinden biridir.
WebClient ve WebTestClient örneklerini içeren reactive-rest-api-demo adlı Github repomu inceleyebilirsiniz.