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…

Spring Boot Declarative Web Client | How to communicate with other API in reactive manner
The result of implementation

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

References

  1. GitHub issue about Open Feign
  2. SpringDoc Documentation
  3. Spring Web Client Documentation

Subscribe to Egor Voronianskii | Java Development and whatsoever

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe