New RestClient in Spring 6.1

noelopez

Noe Lopez

Posted on July 23, 2023

New RestClient in Spring 6.1

Introduction to HTTP clients in Spring

Spring framework has offered two different options to perform http requests:

  1. RestTemplate: It was introduced in Spring 3 over a decade ago. It is an implementation of the Template pattern providing synchronous blocking communication.
  2. WebClient: It was released in Spring 5 as part of Spring WebFlux library. It provides a fluent API and it follows a reactive model.

RestRemplate approach exposed too many HTTP features leading to a big number of overloaded methods. It employs the one thread per request paradigm from the Jakarta Servlet API.

WebClient is the replacement for RestTemplate supporting both synchronous and asynchronous calls. It is part of the Spring Web Reactive project.

Now Spring 6.1 M1 version presents RestClient. A new synchronous http client which works in a similar way to WebClient, using the same infrastructure as RestTemplate.

Setup project

We will be using Spring Boot 3.2 and the Spring web dependency. You can go to the Spring Initializr page and generate a new project selecting Spring Web dependency. With maven the pom.xml will contain

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

That's it. Spring Reactive Web dependency is not needed at all.

Preparing the project

As this is a simple project to get familiar with RestClient, we are going to make http calls to the customer web service from previous articles. Also, the embedded tomcat will be disabled as there is no need to have a web container running. To do so, application.properties file will contain the property

spring.main.web-application-type=none
Enter fullscreen mode Exit fullscreen mode

Then, a CommandLineRunner class will do all the job. The basic structure of the class can be seen below

@Configuration
public class Initializer implements CommandLineRunner {
    private Logger logger = 
        LoggerFactory.getLogger(Initializer.class);

    private ClientProperties properties;

    public Initializer(ClientProperties properties) {
        this.properties = properties;
    }

    public void run(String... args) throws Exception {
    }
}
Enter fullscreen mode Exit fullscreen mode

In the run method is where neccessary objects are constructed in order to interact with the customer endpoint.

Creating a RestClient

To create an instance of the RestClient we have available convinient static methods:

  1. create() method delegates in a default rest client.
  2. create(String url) accepts a default base url.
  3. create(RestTemplate restTemplate) initializes a new RestClient based on the configuration of the given rest template.
  4. builder() allows to customise a RestClient with headers, error handlers, interceptors and more options.
  5. builder(RestTemplate restTemplate) obtain a RestClient builder based on the configuration of the given RestTemplate.

Let's write a RestClient with the builder method to call the customer API.

RestClient restClient = RestClient.builder()
    .baseUrl(properties.getUrl())
    .defaultHeader(HttpHeaders.AUTHORIZATION,
        encodeBasic(properties.getUsername(), 
                    properties.getPassword())
    ).build();
Enter fullscreen mode Exit fullscreen mode

Let's take a closer look at the above code:

  • baseUrl method is self-explanatory. it sets the base url for the client
  • defaultHeader allows to set an http header. There is another method named defaultHeaders which takes Consumer as argument for multiple headers. we are setting the Authorization header to pass the credentials.
  • properties is a simple Configuration Properties class to store the rest API data needed for the requests. And it is constructor-injected in the CommmandLineRunner class.
@Configuration
@ConfigurationProperties(prefix = "application.rest.v1.customer")
public class ClientProperties {
    String url;
    String username;
    String password;
    // getter/setter omitted
}
Enter fullscreen mode Exit fullscreen mode

Three new key-values are added to the application properties file

application.rest.v1.customer.url=http://localhost:8080/api/v1/customers
application.rest.v1.customer.username=user1234
application.rest.v1.customer.password=password5678
Enter fullscreen mode Exit fullscreen mode

Finally, the encodeBasic routine just for reference

private String encodeBasic(String username, String password) {
    return "Basic "+Base64
        .getEncoder()
        .encodeToString((username+":"+password).getBytes());
}
Enter fullscreen mode Exit fullscreen mode

Receiving Data

Next step is to use the client to send http requests and receive the response. RestClient offers methods for each HTTP method. For instance, to search all active customers a GET request must be done. The retrieve method fetches the responce and declares how to extract it.

Let's start with a simple case by getting the full body as String.

String data = restClient.get()
    .uri("?status={STATUS}&vip={vip}","activated", true)
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .body(String.class);

logger.info(data);
Enter fullscreen mode Exit fullscreen mode

The uri method can setup the http parameters to filter by status and vip. The first argument (a String template) is the query string appended to the base url defined in the RestClient. The second argument are uri variables (varargs) of the template.

We also specify the Media type as JSON. The output is displayed in the console:

[{"id":6,"status":"ACTIVATED","personInfo":{"name":"name 6 surname 6","email":"organisation6@email.com","dateOfBirth":"19/07/1976"},"detailsInfo":{"info":"Customer info details 6","vip":true}}]
Enter fullscreen mode Exit fullscreen mode

What if we need to check the response status code or response headers? No worries, the method toEntity returns a ResponseEntity.

ResponseEntity response = restClient.get()
    .uri("?status={STATUS}&vip={vip}","activated", true)
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .toEntity(String.class);

logger.info("Status " + response.getStatusCode());
logger.info("Headers " + response.getHeaders());
Enter fullscreen mode Exit fullscreen mode

Converting JSON

RestClient can also convert a response body in JSON format. Spring will automatically register by default MappingJackson2HttpMessageConverter or MappingJacksonHttpMessageConverter if Jackson 2 library or Jackson library are detected in the classpath. But you can register your own message converters and override the default settings.

In our case, the response can be directly converted to a record. For example, to retrieve a particular customer from the API:

CustomerResponse customer = restClient.get()
    .uri("/{id}",3)
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .body(CustomerResponse.class);

logger.info("Customer name: " + customer.personInfo().name());
Enter fullscreen mode Exit fullscreen mode

And output to extract the customer name

Customer name: name 3 surname 3
Enter fullscreen mode Exit fullscreen mode

To search for customers we just need to use the List class as demonstrated in the following code

List<CustomerResponse> customers = restClient.get()
    .uri("?status={STATUS}&vip={vip}","activated", true)    
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .body(List.class);

logger.info("Customers size " + customers.size());
Enter fullscreen mode Exit fullscreen mode

The record classes for the customer response are shown for reference

public record CustomerResponse(
    long id, 
    String status, 
    CustomerPersonInfo personInfo, 
    CustomerDetailsInfo detailsInfo) {}

public record CustomerPersonInfo(
    String name, String email, String dateOfBirth) {}

public record CustomerDetailsInfo(String info, boolean vip) {}
Enter fullscreen mode Exit fullscreen mode

Posting Data

To send a post request just call the post method. The next code snippet creates a new customer.

CustomerRequest customer = new CustomerRequest(
                "John Smith",
                "john.smith@mycompany.com",
                LocalDate.of(1998, 10, 25),
                "Customer detailed info here",
                true
);

ResponseEntity<Void> response = restClient.post()
                .accept(MediaType.APPLICATION_JSON)
                .body(customer)
                .retrieve()
                .toBodilessEntity();

if (response.getStatusCode().is2xxSuccessful()) {
    logger.info("Created " + response.getStatusCode());
    logger.info("New URL " + response.getHeaders().getLocation());
}
Enter fullscreen mode Exit fullscreen mode

The response code confirms the customer was created successfully

Created 201 CREATED
New URL http://localhost:8080/api/v1/customers/11
Enter fullscreen mode Exit fullscreen mode

To verify the customer was added, the above URL can be retreived via postman

{
    "id": 11,
    "status": "ACTIVATED",
    "personInfo": {
        "name": "John Smith",
        "email": "john.smith@mycompany.com",
        "dateOfBirth": "25/10/1998"
    },
    "detailsInfo": {
        "info": "Customer detailed info here",
        "vip": true
    }
}
Enter fullscreen mode Exit fullscreen mode

Of course it could be fetched using the RestClient with code similar to the previous section.

The record class for the customer request is shown for reference

public record CustomerRequest(
    String name,
    String email,
    LocalDate dateOfBirth,
    String info, 
    Boolean vip) { }
Enter fullscreen mode Exit fullscreen mode

Deleting Data

Making an HTTP delete request to try to remove a resource is as simple as invoking the delete method.

ResponseEntity<Void> response = restClient.delete()
    .uri("/{id}",2)
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .toBodilessEntity();

logger.info("Deleted with status " + response.getStatusCode());
Enter fullscreen mode Exit fullscreen mode

It is worth mentioning that the response body will be empty if the operation succeeded. For this situation the method toBodilessEntity comes in handy. The customer id to be deleted is passed as uri variables.

Deleted with status 204 NO_CONTENT
Enter fullscreen mode Exit fullscreen mode

Handling Errors

What happens if we try to delete or retrieve a non-existing customer? The customer endpoint will return a 404 error code along with a message details. However, RestClient will throw a subclass of RestClientException whenever a client error status (400-499) or server error status (500-599) are received.

To define our custom exception handlers there are two options that work at different levels:

  1. In the RestClient with defaultStatusHandler method (for all http request sent with it)
  2. For each http request with the onstatus method after the call to retreive method (this method returns a ResponseSpec interface).

The first is presented in this code snippet

RestClient restClient = RestClient.builder()
    .baseUrl(properties.getUrl())
    .defaultHeader(HttpHeaders.AUTHORIZATION,
                   encodeBasic(properties.getUsername(), 
                   properties.getPassword()))
    .defaultStatusHandler(
        HttpStatusCode::is4xxClientError,
        (request, response) -> {
             logger.error("Client Error Status " + 
             response.getStatusCode());
             logger.error("Client Error Body "+new 
                 String(response.getBody().readAllBytes()));
    })
    .build();
Enter fullscreen mode Exit fullscreen mode

And the console after running the delete command line runner:

Client Error Status 404 NOT_FOUND
Client Error Body {"status":404,"message":"Entity Customer for id 2 was not found.","timestamp":"2023-07-23T09:24:55.4088208"}
Enter fullscreen mode Exit fullscreen mode

The other option is implementing the onstatus method for the delete operation. It takes precedence over the RestClient defaultt handler behaviour. Hence, it is overriden as proved in the below code lines

ResponseEntity response = restClient.delete()
    .uri("/{id}",2)
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .onStatus(HttpStatusCode::is4xxClientError,
         (req, res) -> 
         logger.error("Couldn't delete "+res.getStatusText())
    )
    .toBodilessEntity();

    if (response.getStatusCode().is2xxSuccessful())
        logger.info("Deleted with status " + 
                     response.getStatusCode());
Enter fullscreen mode Exit fullscreen mode

Now the message in the console will be

 Couldn't delete Not Found
Enter fullscreen mode Exit fullscreen mode

Exchange Method

The exchange method is useful for situations where the response must be decoded differently depending on the response status. Status handlers are ignored when the exchange method is employed.

In this fictitious sample code, the response is mapped to an entity based on the status

SimpleResponse simpleResponse = restClient.get()
    .uri("/{id}",4)
    .accept(MediaType.APPLICATION_JSON)
    .exchange((req,res) -> 
        switch (res.getStatusCode().value()) {
            case 200 -> SimpleResponse.FOUND;
            case 404 -> SimpleResponse.NOT_FOUND;
            default -> SimpleResponse.ERROR;
        }
    );
Enter fullscreen mode Exit fullscreen mode

Summary

In this article we have covered the main features of the new RestClient shipped with Spring 6.1 M1+. Now you should be in a position to perform the most common tasks when calling Rest APIs as well as customise error handling and use the low level http request/response with exchange.

As you can see this new API is easier to manage than the old RestTemplate. It also avoids adding the reactive web libraries in case your project only uses the WebClient API.

Thanks for reading the article and stay tuned for more articles on Java and Spring.

πŸ’– πŸ’ͺ πŸ™… 🚩
noelopez
Noe Lopez

Posted on July 23, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

New RestClient in Spring 6.1
spring New RestClient in Spring 6.1

July 23, 2023

Spring - @RequestParam
spring Spring - @RequestParam

April 17, 2022