Spring Boot Declarative Web Client | How to communicate with other API in reactive manner
In this article, I will show you how to employ a web client from Spring Boot 3. Most projects I have been working on utilize Feign…
In this article, I will show you how to employ a web client from Spring Boot 3.
Most projects I have been working on utilize Feign Declarative web clients, but Feign. But it seems like the Spring team decided to move to Spring Web Client, and they are not going to support Open Feign anymore since it required the rearchitecting of the project.
In this article, we will develop a client for Quote Garden.
What is Web Client ?
According to Spring Documentation:
Spring WebFlux includes a client to perform HTTP requests with. WebClient
has a functional, fluent API based on Reactor, see Reactive Libraries, which enables declarative composition of asynchronous logic without the need to deal with threads or concurrency. It is fully non-blocking, it supports streaming, and relies on the same codecs that are also used to encode and decode request and response content on the server side.
Implementation
Let’s start with the definition of data transfer objects.
package io.vrnsky.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
public record Quote(
@JsonProperty("_id")
String id,
@JsonProperty("quoteText")
String qouteText,
@JsonProperty("quoteAuthor")
String quoteAuthor,
@JsonProperty("quoteGenre")
String quoteGenre,
@JsonProperty("__v")
int version
) {
}
package io.vrnsky.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
public record Pagination(
@JsonProperty("currentPage")
int currentPage,
@JsonProperty("nextPage")
int nextPage,
@JsonProperty("totalPages")
long totalPages
) {
}
package io.vrnsky.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record QuoteResponse(
@JsonProperty("statusCode")
int statusCode,
@JsonProperty("message")
String message,
@JsonProperty("pagination")
Pagination pagination,
@JsonProperty("totalQuotes")
long totalQuotes,
@JsonProperty("data")
List<Quote> qoutes
) {
}
The second step we will take is to implement our declarative web client.
package io.vrnsky.client;
import io.vrnsky.dto.QuoteResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import reactor.core.publisher.Mono;
@HttpExchange(url = "https://quote-garden.onrender.com/api/v3")
public interface QuoteClient {
@GetExchange("/quotes")
Mono<QuoteResponse> getAll();
@GetExchange("/quotes")
Mono<QuoteResponse> findByParams(
@RequestParam(name = "author", required = false) String author,
@RequestParam(name = "genre", required = false ) String genre,
@RequestParam(name = "query", required = false) String query,
@RequestParam(name = "page", required = false) int page,
@RequestParam(name = "limit", required = false) int limit
);
}
The second step to start using a created web client is to create a bean of the web client and the client to another service.
package io.vrnsky.config;
import io.vrnsky.client.QuoteClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
@Configuration
public class SpringConfig {
@Bean
public WebClient quoteClient() {
return WebClient.builder()
.build();
}
@Bean
public QuoteClient quoteClient(WebClient webClient) {
HttpServiceProxyFactory httpServiceProxyFactory =
HttpServiceProxyFactory
.builderFor(WebClientAdapter.create(webClient))
.build();
return httpServiceProxyFactory.createClient(QuoteClient.class);
}
}
The next step is to implement the controller and document it so the consumers of the service can interact with it.
package io.vrnsky.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.vrnsky.client.QuoteClient;
import io.vrnsky.dto.QuoteResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
@RequiredArgsConstructor
@Tag(name = "QuoteController", description = "REST API for get quotes")
public class QuoteController {
private final QuoteClient quoteClient;
@GetMapping("/quote")
@Operation(summary = "Get all quotes")
public Mono<QuoteResponse> getAllQuotes() {
return quoteClient.getAll();
}
@GetMapping("/filteredQuotes")
@Operation(summary = "Get quotes by param")
public Mono<QuoteResponse> getFilteredQuotes(
@RequestParam(name = "author", required = false) String author,
@RequestParam(name = "genre", required = false) String genre,
@RequestParam(name = "query", required = false) String query,
@RequestParam(name = "page", required = false, defaultValue = "1") Integer page,
@RequestParam(name = "limit", required = false, defaultValue = "1") Integer limit
) {
return quoteClient.findByParams(author, genre, query, page, limit);
}
}
While writing this article, I faced a problem with SpringDoc. The API documentation wasn’t generated. My workaround was to add the baseScanPackage attribute of Spring Boot Application annotation.
package io.vrnsky.quotegardenclient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.reactive.config.EnableWebFlux;
@SpringBootApplication(scanBasePackages = "io.vrnsky")
public class QuoteGardenClientApplication {
public static void main(String[] args) {
SpringApplication.run(QuoteGardenClientApplication.class, args);
}
}
Result
Conclusion
This type of implementation gives benefits, but there are no free benefits. The benefits if you employ Web Client are:
- Reactive model
- Efficient resource utilization
- Higher throughput of your services
The catch of this benefits:
- More complex code
- The more complex interaction between service