WebClient WebFlux: RestTemplate වෙනුවට Reactive API Calls SC Guide

WebClient WebFlux: RestTemplate වෙනුවට Reactive API Calls SC Guide

ඉතින් කොහොමද යාලුවනේ! WebClient එකෙන් Reactive API Calls ගහමුද?

අද අපි කතා කරන්නේ Spring Boot ලෝකේ හරිම වැදගත් සහ ජනප්‍රිය මාතෘකාවක් ගැන. මේ දවස් වල හැමතැනම කියවෙනවා WebFlux, Reactive Programming ගැන. ඉතින්, ඔයාලා තාමත් RestTemplate එක පාවිච්චි කරනවා නම්, මේක කියෙව්වට පස්සේ අනිවාර්යෙන්ම WebClient එකට මාරු වෙයි!

RestTemplate එක ගැන අලුතෙන් කියන්න දෙයක් නෑනේ. ගොඩක් කල් Spring Boot එක්ක වැඩ කරපු අයට ඒක හොඳට පුරුදුයි. ඒත්, ඒක Blocking වෙන නිසා, විශේෂයෙන්ම High-Performance, Scalable Applications හදනකොට පොඩි ප්‍රශ්නයක් වෙනවා. Thread එකක් API Call එක ඉවර වෙනකම් Block වෙලා තියෙන එක, System Resources නිකරුණේ නාස්ති කරනවා වගේ වැඩක්. මේකට තමයි WebFlux ආවේ Reactive Programming එක්ක.

අද අපි බලමු RestTemplate එක පැත්තකට දාලා, WebClient එක පාවිච්චි කරලා කොහොමද Non-Blocking, Reactive API Calls කරන්නේ කියලා. අපි සරල උදාහරණයක් අරගෙන External API එකකට Call එකක් ගහන හැටිත් කතා කරමු. එහෙනම්, පටන් ගමුද?


මොකක්ද මේ WebClient කියන්නේ?

සරලවම කිව්වොත්, WebClient කියන්නේ Spring WebFlux Framework එකේ තියෙන Non-Blocking, Reactive HTTP Client එකක්. මේක Spring 5 එක්ක ආපු අලුත් එකක්, RestTemplate එකට වඩා ගොඩක් වාසි තියෙනවා.

  • Non-Blocking: මේක තමයි ප්‍රධානම වාසිය. WebClient එක API call එකක් ගහනකොට thread එක Block කරන් ඉන්නේ නෑ. ඒ වෙනුවට, ඒ call එක asynchronous විදියට handle කරනවා. ඒ කියන්නේ, එක thread එකකට එක පාරට ගොඩක් API calls handle කරන්න පුළුවන්. මේක නිසා High Concurrency තියෙන Systems වල Performance එක අනිවාර්යෙන්ම වැඩි වෙනවා.
  • Reactive: WebClient එක Reactor Project එකේ Mono සහ Flux කියන Types එක්ක තමයි වැඩ කරන්නේ. Mono එකක් කියන්නේ එක item එකක් එනවා කියලා බලාපොරොත්තු වෙන එකක්, Flux එකක් කියන්නේ item කිහිපයක් එන්න පුළුවන් කියලා බලාපොරොත්තු වෙන එකක්. මේවා Stream Processing වලට හරිම ගැළපෙනවා.
  • Functional API: WebClient එකේ API එක හරිම Clean, Fluent, Functional විදියට තමයි හදලා තියෙන්නේ. ඒක නිසා Code එක කියවන්න, තේරුම් ගන්න හරිම ලේසියි.
  • Spring WebFlux Ecosystem: WebFlux Framework එකත් එක්ක seamlessly integrate වෙන නිසා, Security, Tracing වගේ දේවල් එක්ක වැඩ කරන එක හරිම ලේසියි.

ඉතින්, ඔයාලා High-Scalability ඕන කරන Microservices හදනවා නම්, WebClient කියන්නේ අනිවාර්යෙන්ම ඉගෙන ගන්න ඕන දෙයක්.


WebClient එක Setup කරගමු!

හරි, දැන් අපි බලමු WebClient එක පාවිච්චි කරන්න මොනවද ඕන කරන්නේ කියලා. මුලින්ම, ඔයාගේ Spring Boot Project එකට WebFlux Dependency එක add කරගන්න ඕන. `pom.xml` එකේ මේ වගේ එකක් තියෙන්න ඕන:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

මේ Dependency එක add කරාට පස්සේ ඔයාට WebClient එක Inject කරලා පාවිච්චි කරන්න පුළුවන්. සාමාන්‍යයෙන්, අපි WebClient instance එකක් Bean එකක් විදියට Configure කරලා, ඒක අවශ්‍ය තැනට Inject කරගන්නවා. මේක කරන්න හොඳම ක්‍රමය තමයි WebClient.Builder එක පාවිච්චි කරන එක.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient(WebClient.Builder builder) {
        // Base URL එකක් සෙට් කරන්න පුළුවන්
        // builder.baseUrl("https://api.example.com")
        // .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        // .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
        return builder.build();
    }
}

මේ විදියට `WebClientConfig` එකක් හැදුවම, ඔයාට WebClient එක ඕන Service එකකට, Controller එකකට Inject කරගන්න පුළුවන්.

import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;

@Service
public class MyApiService {

    private final WebClient webClient;

    public MyApiService(WebClient webClient) {
        this.webClient = webClient;
    }

    // ... දැන් මේ webClient එක පාවිච්චි කරන්න පුළුවන්
}

දැන් අපි Ready! External API Call එකක් ගහන්න.


WebClient එකෙන් External API Call එකක් ගහමු!

හරි, දැන් තමයි වැඩේට බහින්නේ! අපි Public API එකක් වන JSONPlaceholder (https://jsonplaceholder.typicode.com) එකෙන් Posts ගන්න හැටි බලමු. අපි `GET /posts/{id}` endpoint එකට Call එකක් ගහමු.

Data Model එක හදාගමු

API එකෙන් එන Response එක Map කරගන්න Model Class එකක් ඕනනේ. මේ වගේ Class එකක් හදාගමු:

import lombok.Data; // Lombok පාවිච්චි කරනවා නම්

@Data // Getters, Setters, toString, equals, hashCode auto generate වෙන්න
public class Post {
    private Long id;
    private Long userId;
    private String title;
    private String body;
}

GET Request එකක් ගහමු

දැන් අපි බලමු `WebClient` එක පාවිච්චි කරලා `GET` Request එකක් කොහොමද කරන්නේ කියලා. අපි කලින් හදපු `MyApiService` එකට මේ method එක add කරමු:

import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Service
public class MyApiService {

    private final WebClient webClient;

    public MyApiService(WebClient webClient) {
        this.webClient = webClient;
    }

    public Mono<Post> getPostById(Long id) {
        return webClient.get() // GET request එකක්
                .uri("/posts/{id}", id) // API endpoint එක සහ path variable එක
                .retrieve() // response එක handle කරන්න
                .bodyToMono(Post.class); // response body එක Post object එකකට map කරන්න (single object නිසා Mono)
    }

    public Mono<String> getPostByIdRaw(Long id) {
        return webClient.get()
                .uri("https://jsonplaceholder.typicode.com/posts/{id}", id)
                .retrieve()
                .bodyToMono(String.class); // Raw String response එකක් විදියට ගන්න
    }

    // List එකක් ගන්නවා නම් (උදා: /posts)
    public Flux<Post> getAllPosts() {
        return webClient.get()
                .uri("/posts") // List of posts
                .retrieve()
                .bodyToFlux(Post.class); // response body එක Post objects ගොඩකට map කරන්න (list නිසා Flux)
    }
}

කලින් `WebClientConfig` එකේ `baseUrl` එක සෙට් නොකළා නම්, `uri` එකට Full URL එක දෙන්න පුළුවන්, දෙවැනි `getPostByIdRaw` method එකේ වගේ.

Method එකේ Flow එක තේරුම් ගනිමු:

  1. webClient.get(): මේකෙන් කියවෙන්නේ අපි `GET` Request එකක් කරන්න යනවා කියලා. `post()`, `put()`, `delete()` වගේ Methods තියෙනවා වෙනත් HTTP Methods වලට.
  2. .uri("/posts/{id}", id): මේකෙන් අපි Request එක යවන්න ඕන URI එක specify කරනවා. Path variables වගේ දේවල් දෙන්න පුළුවන්.
  3. .retrieve(): මේකෙන් Request එක Execute කරලා Response එක Retrieve කරන්න සූදානම් වෙනවා. මේකෙන් ලැබෙන්නේ `ResponseSpec` එකක්.
  4. .bodyToMono(Post.class): මේකෙන් කියවෙන්නේ Response Body එක `Post.class` එකට Map කරන්න කියලා. මේක `Mono` එකක් Return කරන්නේ Single Object එකක් නිසා. List එකක් නම් `bodyToFlux()` පාවිච්චි කරනවා.

POST Request එකක් ගහමු

දැන් බලමු `POST` Request එකක් කොහොමද කරන්නේ කියලා. අපි අලුත් `Post` එකක් JSONPlaceholder එකට add කරන්න උත්සාහ කරමු.

import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Service
public class MyApiService {

    private final WebClient webClient;

    public MyApiService(WebClient webClient) {
        this.webClient = webClient;
    }

    // ... කලින් methods ...

    public Mono<Post> createPost(Post newPost) {
        return webClient.post() // POST request එකක්
                .uri("/posts") // API endpoint එක
                .bodyValue(newPost) // Request body එකට යවන්න ඕන object එක
                .retrieve() // response එක handle කරන්න
                .bodyToMono(Post.class); // response body එක Post object එකකට map කරන්න
    }
}

මෙහිදී, `bodyValue(newPost)` කියන method එකෙන් අපි request body එකට යවන්න ඕන Object එක දෙනවා. WebClient එක automatically ඒක JSON/XML format වලට convert කරලා යවනවා.

Controller එකකින් Call කරන හැටි

හරි, දැන් අපි මේ Service එක Controller එකක් ඇතුළේ පාවිච්චි කරන හැටි බලමු. මේක හරිම සරලයි:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/api/posts")
public class PostController {

    private final MyApiService myApiService;

    public PostController(MyApiService myApiService) {
        this.myApiService = myApiService;
    }

    @GetMapping("/{id}")
    public Mono<Post> getPost(@PathVariable Long id) {
        return myApiService.getPostById(id);
    }

    @PostMapping
    public Mono<Post> createPost(@RequestBody Post post) {
        return myApiService.createPost(post);
    }
}

දැන් ඔයාට ඔයාගේ Spring Boot Application එක Run කරලා, Postman හෝ Insomnia වගේ tool එකකින් මේ Endpoints call කරලා බලන්න පුළුවන්. `GET /api/posts/1` Call එකක් ගහලා බලන්න, JSONPlaceholder එකෙන් Post එකක් එයි.


Error Handling සහ Advanced Options

WebClient එකේදී Error Handling එකත් හරිම වැදගත්. Network issues, HTTP Status errors (4xx, 5xx) වගේ දේවල් handle කරන්න වෙනවනේ.

HTTP Status Errors Handle කරමු

WebClient එක `retrieve()` කලාට පස්සේ `onStatus()` method එක පාවිච්චි කරලා HTTP Status Codes වලට අනුව Errors handle කරන්න පුළුවන්.

import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;

// ...
public Mono<Post> getPostByIdWithErrorHandling(Long id) {
    return webClient.get()
            .uri("/posts/{id}", id)
            .retrieve()
            .onStatus(HttpStatus::is4xxClientError, clientResponse ->
                Mono.error(new RuntimeException("Client Error: " + clientResponse.statusCode())) // 4xx error නම්
            )
            .onStatus(HttpStatus::is5xxServerError, clientResponse ->
                Mono.error(new RuntimeException("Server Error: " + clientResponse.statusCode())) // 5xx error නම්
            )
            .bodyToMono(Post.class)
            .onErrorResume(WebClientResponseException.class, ex -> {
                // WebClientResponseException එකක් ආවොත් handle කරන්න
                System.err.println("WebClient error: " + ex.getRawStatusCode() + " " + ex.getResponseBodyAsString());
                return Mono.error(new RuntimeException("Failed to fetch post: " + ex.getMessage()));
            })
            .doOnError(throwable -> System.err.println("General error: " + throwable.getMessage())); // වෙනත් error handle කරන්න
}

මෙහිදී, `onStatus` වලින් අපි HTTP Status codes චෙක් කරලා, අපිට ඕන විදියට Custom Exception එකක් throw කරනවා. `onErrorResume` කියන Reactive operator එකෙන් අපිට Pipeline එකේදී එන Error එකක් අල්ලලා ඒකට Response එකක් දෙන්න පුළුවන්. `doOnError` එකෙන් අපිට Error එකක් ආවොත් මොනවහරි Log කරන්න පුළුවන්.

Timeout එකක් දාමු

API Call එකක් වැඩි වෙලාවක් ගත්තොත් Time out වෙන්න සෙට් කරන්නත් පුළුවන්. මේක WebClient Builder එකේදී Configure කරන්න පුළුවන්.

import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient(WebClient.Builder builder) {
        HttpClient httpClient = HttpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // Connection Timeout 5 seconds
                .responseTimeout(Duration.ofSeconds(5)) // Response Timeout 5 seconds
                .doOnConnected(conn -> conn
                        .addHandlerLast(new ReadTimeoutHandler(5, TimeUnit.SECONDS)) // Read Timeout 5 seconds
                        .addHandlerLast(new WriteTimeoutHandler(5, TimeUnit.SECONDS))); // Write Timeout 5 seconds

        return builder
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .build();
    }
}

මේකෙන් අපි Netty HttpClient එක Configure කරලා Connection, Read, Write, Response Timeout දානවා. මේක Production Environments වලදී හරිම වැදගත්.


අවසාන වශයෙන් (Conclusion)

ඉතින් යාලුවනේ, අද අපි WebClient ගැන ගොඩක් දේවල් ඉගෙන ගත්තා නේද? WebClient කියන්නේ Spring Boot Application වල Reactive, Non-Blocking External API calls කරන්න තියෙන සුපිරිම Tool එකක්. RestTemplate එකට වඩා මේක කොච්චර Effective ද කියලා දැන් ඔයාලට තේරෙනවා ඇති.

Microservices හදනකොට, Performance එකයි Scalability එකයි ගැන හිතනකොට WebClient එකට මාරු වෙන එක හරිම බුද්ධිමත් තීරණයක්. ඒකෙන් ඔයාගේ Application එකේ Resource Utilization එක වැඩි වෙනවා වගේම, Response Times අඩු කරගන්නත් පුළුවන්.

මම හිතනවා මේ ලිපිය ඔයාලට WebClient එක තේරුම් ගන්න හොඳ මඟ පෙන්වීමක් වුණා කියලා. දැන් ඉතින් පරණ RestTemplate එක පැත්තකට දාලා, WebClient එක පාවිච්චි කරන්න පටන් ගන්න කාලේ හරි! පොඩි Project එකක් හදලා මේක Try කරලා බලන්න. එතකොට තව හොඳට තේරෙයි.

මේ ගැන ඔයාලට තියෙන අදහස්, ප්‍රශ්න පහලින් comment කරන්න. තව මොන වගේ topics ගැනද දැනගන්න කැමති කියලත් කියන්න. එහෙනම්, තවත් අලුත් ලිපියකින් හමුවෙමු! Happy Coding!