Spring web frameworks - Spring WebFlux

https://docs.spring.io/spring-framework/reference/web/webflux.html

Reading material

A great article: https://docs.spring.io/spring-framework/reference/web/webflux/new-framework.html

https://docs.spring.io/spring-framework/reference/web/webflux.html https://www.baeldung.com/spring-webflux-concurrency

Overview

The original web framework included in the Spring Framework, Spring Web MVC, was purpose-built for the Servlet API and Servlet containers. The reactive-stack web framework, Spring WebFlux, was added later in version 5.0.

It is

  1. fully non-blocking,
  2. supports Reactive Streams back pressure, and
  3. runs on such servers as Netty, Undertow, and Servlet containers.

Usage of spring WebClient and setting up test cases for it

Maven dependency

In order to use WebClients in a Spring Boot project include a starter dependency for Spring WebFlux.

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

This dependency implicitly sets all the required dependencies including underlying Netty server.

Usage

If you have to use WebClient in a client class in the application, do it this way:

@Component
public class ClientClassName {
    private WebClient webClient;

    @Value("$endpointUrl")
    private String endpointUrl;

    @Value("$username")
    private String username;

    @Value("$password")
    private String password;

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

    @PostConstruct
    private void init() {
        webClient = WebClient.builder()
                             .baseUrl(endpointUrl)
                             .defaultHeaders(header -> header.setBasicAuth(username, password))
                             .build();
    }

    public TestResponse callExternalService(TestRequest testRequest) {

       TestResponse resp = null;

       try {
           resp = webClient.post()
                           .body(Mono.just(testRequest), TestRequest.class)
                           .retrieve()
                           .bodyToMono(TestResponse.class)
                           .toFuture()
                           .get();
       } catch (Exception1 | Exception2 ex) {
           handleException(ex);
       }

        return resp;
    }
}

How to define WebClient bean?

If you are not using the bean in individual client files, you have to add custom configuration in the WebClients defined in each of the classes. If you are defining custom WebClient in a class and not using the WebClient bean (like the example shown above), the configuration that you put on the bean will not be applicable in those individual classes.

@Configuration
public class WebClientConfig {
    @Bean
    public WebClient configureWebClient() {
        return WebClient.builder().build();
    }
}

How to set up test cases for ClientClassName?

@RequiredArgsConstructor
public class ClientClassNameTests {
    ClientClassName clientClassName;

    public static MockWebServer mockWebServer;

    @BeforeAll
    static void setUp() throws IOException {
        mockWebServer = new MockWebServer();
        mockWebServer.start();
    }

    @BeforeEach
    void initialize() {
        String baseUrl = String.format("http://localhost:%s", mockWebServer.getPort());
        clientClassName = new ClientClassName(WebClient.builder().baseUrl(baseUrl).build());
    }

    @AfterAll
    static void tearDown() throws IOException {
        mockWebServer.shutdown();
    }

    @Test
    void test1() throws JsonProcessingException {
        TestResponse mockedTestResponse = new TestResponse();
        // set dummy values to mockedTestResponse here

        mockWebServer.enqueue(new MockResponse()
                                               .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                                               .setBody(MAPPER.writeValueAsString(mockedTestResponse))
        );

        TestRequest testReq = new TestRequest();
        // set dummy values to testReq here

        TestResponse actualResult = clientClassName.callExternalService(testReq);

        RecordedRequest request = mockWebServer.takeRequest();


        assertAll(() -> {

            // verify the response
            assertNotNull(actualResult);
            assertThat(actualResult).usingRecursiveComparison().isEqualTo(mockedTestResponse));

            // verify the request
            assertThat(request.getMethod()).isEqualTo("POST");
            assertThat(request.getPath()).isEqualTo("/Accounts/ACd936ed6d/Messages.json");
        });
    }
}

Required dependencies in pom.xml for the test case

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.0.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>mockwebserver</artifactId>
    <version>4.0.1</version>
    <scope>test</scope>
</dependency>

Issues or exceptions

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/codec/CodecConfigurer.DefaultCodecs.html#maxInMemorySize-int-

org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144
    at org.springframework.core.io.buffer.LimitedDataBufferList.raiseLimitException(LimitedDataBufferList.java:101)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException

Option 1:

You can increase the MaxInMemorySize like this:

public WebClient webClient() {
    final int size = 16 * 1024 * 1024;
    final ExchangeStrategies strategies = ExchangeStrategies.builder()
        .codecs(codecs -> codecs.defaultCodecs().maxInMemorySize(size))
        .build();
    return WebClient.builder()
        .exchangeStrategies(strategies)
        .build();
}

Option 2:

I suppose this issue is about adding a new spring.codec.max-in-memory-size configuration property in Spring Boot. Add it to the application.yml file like:

spring:
  codec:
    max-in-memory-size: 10MB

Need further research

3 techniques to stream JSON in Spring WebFlux - https://nurkiewicz.com/2021/08/json-streaming-in-webflux.html

Processing streaming data with Spring WebFlux - https://medium.com/@nithinmallya4/processing-streaming-data-with-spring-webflux-ed0fc68a14de

Making Fully Asynchronous Requests - https://reflectoring.io/spring-webclient/ (Making Fully Asynchronous Requests)

References:

https://www.arhohuttunen.com/spring-boot-webclient-mockwebserver/ https://www.baeldung.com/spring-mocking-webclient

Items TODO

  1. https://www.baeldung.com/spring-webflux-concurrency

Tags

  1. Spring web frameworks - Spring WebFlux - retry logic