DEV Community

Guillaume Le Floch
Guillaume Le Floch

Posted on

Easy HTTP Client with Spring 6.x

TLDR;

Spring 6 brings HttpServiceProxyFactory class allowing you to easily generate HTTP client based on interfaces and annotations.

Introduction

Before Spring 6.x, creating an HTTP client consisted of implementing each calls using either the old RestTemplate, the reactive WebClient, or the new RestClient introduced in Spring 6.1.
This leads to a lot of boilerplate code.

Nevertheless, some alternatives exist and aim to ease the development of rest client such as OpenFEIGN or the Eclipse MicroProfile RestClient specification.

In its last major release, Spring introduced the HttpServiceProxyFactory class as well as a new blocking rest client RestClient (in 6.1).

Thanks to this factory, you can declare your client using a simple interface annotated with @HttpExchange annotations (similar to Spring Data Repositories). The factory will then take care of generating an implementation of your interface.

Giving it a try

Let's consider the following beer API (based on Brewery OpenDB)

GET /api/v1/beer 
GET /api/v1/beer/{beerId}
POST /api/v1/beer
Enter fullscreen mode Exit fullscreen mode

In order to create a client, we need to create an interface and specify annotation in order to declare:

  • Path
  • HTTP verb
  • Headers
  • Cookies
  • Path and Query parameters
package org.dev.example;

import org.springframework.web.service.annotation.*;

public interface BeerClient {

    @GetExchange("/api/v1/beer")
    public List<Beer> listBeers();

    @GetExchange("/api/v1/beer/{beerId}")
    public Beer getBeer(@PathVariable String beerId);

    @PostExchange("/api/v1/beer")
    public Beer createBeer(@RequestBody Beer beer);
}
Enter fullscreen mode Exit fullscreen mode

It is possible to "simplify" paths using @HttpExchange directly on the interface

package org.dev.example;

import org.springframework.web.service.annotation.*;

@HttpExchange(url = "api/v1/beer", contentType = "application/json")
public interface BeerClient {

    @GetExchange
    public List<Beer> listBeers();

    @GetExchange("/{beerId}")
    public Beer getBeer(@PathVariable String beerId);

    @PostExchange
    public Beer createBeer(@RequestBody Beer beer);
}
Enter fullscreen mode Exit fullscreen mode

Once the interface is created, creating a new client consists of using HttpServiceProxyFactory, setting the http client we would like to use and then creating the client implementation.

public BeerClient buildBeerRestClient(WebClient webClient) {
    HttpServiceProxyFactory clientFactory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient)).build();
    return clientFactory.createClient(BeerClient.class);
}
Enter fullscreen mode Exit fullscreen mode

This can be integrated in a bean using a simple configuration class:

@Configuration
public class BeerClientConfiguration() {

    @Bean
    public BeerClient buildBeerClient(WebClient.Builder webClientBuilder) {
        WebClient webClient = webClientBuilder.build();
        HttpServiceProxyFactory clientFactory = 
HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient)).build();
        return clientFactory.createClient(BeerClient.class);
    }
}
Enter fullscreen mode Exit fullscreen mode

This bean can then be injected as any other bean.

Customizing the WebClient

Usually, in most client, we need to inject some headers, add authentication tokens, set loggers, ...

This can be done directly in the WebClient or RestClient (introduced in Spring 6.1) builders using defaultHeaders(...) method or using filters (interceptors when using RestClient or RestTemplate). For example, you can easily register a filter that add an authentication header based on the currently logged in user.

What about non blocking types ?

Types such as Mono or Flux, can be directly specified as return type in the interface such as

package org.dev.example;

import org.springframework.web.service.annotation.*;
import reactor.core.publisher.*;

@HttpExchange(url = "api/v1/beer", contentType = "application/json")
public interface BeerClient {

    @GetExchange
    public Flux<Beer> listBeers();

    @GetExchange("/{beerId}")
    public Mono<Beer> getBeer(@PathVariable String beerId);

    @PostExchange
    public Mono<Beer> createBeer(@RequestBody Beer beer);
}

Enter fullscreen mode Exit fullscreen mode

The HttpServiceProxyFactory also supports returning ResponseEntity type (which can also be combined with Mono and Flux).

What about testing ?

Testing can be done in different ways:

  • By overriding the exchangeFunction of the WebClient used by the HttpServiceProxyFactory
  • If your client is exposed as bean, it can be mocked as any other bean by your favorite mocking library.

Top comments (0)