Spring Declarative HTTP Client using @HttpExchange

Starting Spring 6 and Spring Boot 3, Spring framework supports proxying a remote HTTP service as a Java interface with annotated methods for HTTP exchanges. Similar libraries, like OpenFeign and Retrofit, can still be used, but HttpServiceProxyFactory adds native support to Spring framework.

1. What is a Declarative Http Interface?

A declarative HTTP interface is a Java interface that helps reduce the boilerplate code, generates a proxy implementing this interface, and performs the exchanges at the framework level.

For example, if we want to consume an API at URL https://server-address.com/api/resource/id then we need to create and configure either a RestTemplate or WebClient bean and use its exchange methods for invoking the API, parsing response and handling the errors.

Most often, the code to create and configure the beans and invoke remote APIs is very similar and thus can be abstracted by the framework, so we do not need to write this code again and again in every application. We can simply express the remote API details using the annotations on an interface and let the framework create an implementation under the hood.

For example, if we want to consume a HTTP GET /users API then we can simply write:

public interface UserClient {

  @GetExchange("/users")
  Flux<User> getAll();
}

Spring will provide the interface and exchange implementations in runtime, and we only need to invoke the getAll() method.

@Autowired
UserClient userClient;

userClient.getAll().subscribe(
    data -> log.info("User: {}", data)
);

2. Maven

The declarative HTTP interface functionality is part of the spring-web dependency that is transitively pulled in when we include either spring-boot-starter-web or spring-boot-starter-webflux. If we want to add the reactive support then include the later dependency.

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

<!-- For reactive support -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

3. Creating an HTTP Service Interface

In Spring, an HTTP service interface is a Java interface with @HttpExchange methods. The annotated method is treated as an HTTP endpoint, and the details are defined statically through annotation attributes as well as through the input method argument types.

3.1. Exchange Methods

We can use the following annotations to mark a method as HTTP service endpoint:

  • @HttpExchange: is the generic annotation to specify an HTTP endpoint. When used at the interface level, it applies to all methods.
  • @GetExchange: specifies @HttpExchange for HTTP GET requests.
  • @PostExchange: specifies @HttpExchange for HTTP POST requests.
  • @PutExchange: specifies @HttpExchange for HTTP PUT requests.
  • @DeleteExchange: specifies @HttpExchange for HTTP DELETE requests.
  • @PatchExchange: specifies @HttpExchange for HTTP PATCH requests.

3.2. Method Arguments

The exchange methods support the following method parameters in the method signature:

  • URI: sets the URL for the request.
  • @PathVariable: replaces a value with a placeholder in the request URL.
  • @RequestBody: provides the body of the request.
  • @RequestParam: add the request parameter(s). When “content-type” is set to “application/x-www-form-urlencoded“, request parameters are encoded in the request body. Otherwise, they are added as URL query parameters.
  • @RequestHeader: adds the request header names and values.
  • @RequestPart: can be used to add a request part (form field, resource or HttpEntity etc).
  • @CookieValue: adds cookies to the request.
@PutExchange
void update(@PathVariable Long id, @RequestBody User user);

3.3. Return Values

An HTTP exchange method can return values that are:

  • either blocking or reactive (Mono/Flux).
  • only the specific response information, such as status code and/or response headers.
  • void if the method is treated as execute only.

For a blocking exchange method, we should generally return ResponseEntity, and for reactive methods, we can return the Mono/Flux types.

//Blocking
@GetExchange("/{id}")
User getById(...);

//Reactive
@GetExchange("/{id}")
Mono<User> getById(...);

4. Building HttpServiceProxyFactory

The HttpServiceProxyFactory is a factory to create a client proxy from an HTTP service interface. Use its HttpServiceProxyFactory.builder(client).build() method to get an instance of the proxy bean.

import com.fasterxml.jackson.databind.ObjectMapper;
import com.howtodoinjava.app.web.UserClient;
import lombok.SneakyThrows;
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 WebConfig {

  @Bean
  WebClient webClient(ObjectMapper objectMapper) {
    return WebClient.builder()
        .baseUrl("https://jsonplaceholder.typicode.com/")
        .build();
  }

  @SneakyThrows
  @Bean
  UserClient postClient(WebClient webClient) {
    HttpServiceProxyFactory httpServiceProxyFactory =
        HttpServiceProxyFactory.builderFor(WebClientAdapter.create(webClient))
            .build();
    return httpServiceProxyFactory.createClient(UserClient.class);
  }
}

Notice we have set the remote API’s base URL in the WebClient bean, so we need to use only the relative paths in the exchange methods.

5. HTTP Request Headers

We can set the request headers in all outgoing requests in two ways: headers specific to a request or global headers send with every outgoing request automatically.

5.1. Headers Specific to a Request

To send a header, specific to a request, we must add it to the method signature using the @RequestHeader annotation as follows:

@GetExchange("/")
Flux<User> getAll(@RequestHeader("X-LOCAL-HEADER") String headerName);

Next, we need to set the header value when we execute the request.

String headerValue = UUID.randomUUID().toString();

userClient.getAll(headerValue).subscribe(
    data -> log.info("User: {}", data)
);

5.2. Set Static Global Header to WebClient Bean

To set the request headers for every outgoing request, such as trace ID or authentication, we can set the headers in the WebClient bean itself.

In the following example, we are setting the basic authentication with each outgoing request. Additionally, we have set the enableLoggingRequestDetails(true) to verify the headers in outgoing requests. Comment this code in production.

@Bean
WebClient webClient(ObjectMapper objectMapper) {

  return WebClient.builder()
    .exchangeStrategies(ExchangeStrategies.builder().codecs(c ->
        c.defaultCodecs().enableLoggingRequestDetails(true)).build()
    )
    .defaultHeaders(header -> header.setBasicAuth("username", "password"))   //Globally Set Header - Once
    .baseUrl("https://jsonplaceholder.typicode.com/")
    .build();
}

Now, when we execute the getAll() API written, we can see that both, local and global, headers are present in the request.

HTTP GET https://jsonplaceholder.typicode.com/users/

headers=[Authorization:"Basic dXNlcm5hbWU6cGFzc3dvcmQ=", 
  Content-Type:"application/json", 
  Accept:"application/json", 
  X-LOCAL-HEADER:"0d4cb598-c53c-43dc-ac66-b13d78df8191"]

5.3. Set Dynamic Global Header with ExchangeFilterFunction

If we want to set a global header whose value changes for each outgoing request, we can use the ExchangeFilterFunction. The ExchangeFilterFunction represents a filter that, once registered, is executed for each request-response interaction with the server.

It should be noted ClientRequest object is immutable and we cannot modify this once it has been fully initialized. So in the ExchangeFilterFunction.filter(), we need to create a new ClientRequest with the details from the existing request, add our new headers with dynamic values and build a new request for further processing.

@Component
public class DynamicHeaderFilter implements ExchangeFilterFunction {

  @Override
  public Mono<ClientResponse> filter(ClientRequest clientRequest, ExchangeFunction nextFilter) {

    // Create a new ClientRequest with the additional headers
    ClientRequest modifiedRequest = ClientRequest
        .from(clientRequest)
        .header("X-REQUEST-TIMESTAMP", LocalDateTime.now().toString())
        .build();

    return nextFilter.exchange(modifiedRequest);
  }
}

Next, register the DynamicHeaderFilter with the WebClient so it is invoked for each outgoing request.

@Autowired
DynamicHeaderFilter dynamicHeaderFilter;

@Bean
WebClient webClient(ObjectMapper objectMapper) {
  return WebClient.builder()
      ...
      .filter(dynamicHeaderFilter)
      ...
      .build();
}

Let us test the above filter with a simple request.

HTTP GET https://jsonplaceholder.typicode.com/users/

headers=[
  Authorization:"Basic dXNlcm5hbWU6cGFzc3dvcmQ=", 
  Content-Type:"application/json", 
  Accept:"application/json", 
  X-LOCAL-HEADER:"9d212fbc-4a2b-4646-a168-c60b8442ef89", 
  X-REQUEST-TIMESTAMP:"2023-09-03T12:54:56.583565800"]

6. HTTP Service Interface Example

The following is an example of HTTP interface that interacts with https://jsonplaceholder.typicode.com/users/ endpoint and performs various operations.

import com.howtodoinjava.app.model.User;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.DeleteExchange;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;
import org.springframework.web.service.annotation.PutExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@HttpExchange(url = "/users", accept = "application/json", contentType = "application/json")
public interface UserClient {

  @GetExchange("/")
  Flux<User> getAll();

  @GetExchange("/{id}")
  Mono<User> getById(@PathVariable("id") Long id);

  @PostExchange("/")
  Mono<ResponseEntity<Void>> save(@RequestBody User user);

  @PutExchange("/{id}")
  Mono<ResponseEntity<Void>> update(@PathVariable Long id, @RequestBody User user);

  @DeleteExchange("/{id}")
  Mono<ResponseEntity<Void>> delete(@PathVariable Long id);
}

Note that we have created a record of User type to hold the user information.

public record User(Long id, String name, String username, String email) {}

Now we can inject the UserClient bean into application classes and invoke methods to get the API responses.

@Autowired
UserClient userClient;

//Get All Users
userClient.getAll().subscribe(
    data -> log.info("User: {}", data)
);

//Get User By Id
userClient.getById(1L).subscribe(
    data -> log.info("User: {}", data)
);

//Create a New User
userClient.save(new User(null, "Lokesh", "lokesh", "admin@email.com"))
    .subscribe(
        data -> log.info("User: {}", data)
    );


//Delete User By Id
userClient.delete(1L).subscribe(
    data -> log.info("User: {}", data)
);

7. Conclusion

In this Spring tutorial, we learned to create and use the declarative HTTP client interface using examples. We learned to create the exchange methods in the interface and then invoke them using the proxy implementation created by the Spring framework. We also learned to create the interface proxy using the HttpServiceProxyFactory and WebClient beans.

Happy Learning !!

Sourcecode on Github

Comments

Subscribe
Notify of
guest
13 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.