spring boot,

中文版

Spring Boot 3.2 New Feature: RestClient

qihaiyan qihaiyan Follow Dec 03, 2023 · 7 mins read

Spring Boot 3.2 introduces the new RestClient for calling HTTP APIs, with a fluent API style that supports chained calls.

The complete code is available in the example project https://github.com/qihaiyan/springcamp/tree/main/spring-data-jdbc-client.

1. Overview

RestClient is a synchronous API-calling tool similar to RestTemplate. While RestTemplate follows the template design pattern, RestClient adopts a fluent API style — simple, flexible, easy to read and maintain.

2. Adding RestClient

First add the spring-boot-starter-web dependency.

Add one line to build.gradle:

implementation 'org.springframework.boot:spring-boot-starter-web'

Configuring RestClient:

@Configuration
public class RestClientConfig {
    public CloseableHttpClient httpClient() {
        Registry<ConnectionSocketFactory> registry =
                RegistryBuilder.<ConnectionSocketFactory>create()
                        .register("http", PlainConnectionSocketFactory.getSocketFactory())
                        .register("https", SSLConnectionSocketFactory.getSocketFactory())
                        .build();
        PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager(registry);

        poolingConnectionManager.setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(Timeout.ofSeconds(2)).build());
        poolingConnectionManager.setDefaultConnectionConfig(ConnectionConfig.custom().setConnectTimeout(Timeout.ofSeconds(2)).build());

        // set total amount of connections across all HTTP routes
        poolingConnectionManager.setMaxTotal(200);
        // set maximum amount of connections for each http route in pool
        poolingConnectionManager.setDefaultMaxPerRoute(200);

        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionKeepAlive(TimeValue.ofSeconds(10))
                .setConnectionRequestTimeout(Timeout.ofSeconds(2))
                .setResponseTimeout(Timeout.ofSeconds(2))
                .build();

        return HttpClients.custom()
                .setDefaultRequestConfig(requestConfig)
                .setConnectionManager(poolingConnectionManager)
                .setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy())
                .build();
    }

    @Slf4j
    static class CustomClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
        @Override
        @NonNull
        public ClientHttpResponse intercept(HttpRequest request, @NonNull byte[] bytes, @NonNull ClientHttpRequestExecution execution) throws IOException {
            log.info("HTTP Method: {}, URI: {}, Headers: {}", request.getMethod(), request.getURI(), request.getHeaders());
            request.getMethod();
            if (request.getMethod().equals(HttpMethod.POST)) {
                log.info("HTTP body: {}", new String(bytes, StandardCharsets.UTF_8));
            }

            ClientHttpResponse response = execution.execute(request, bytes);
            ClientHttpResponse responseWrapper = new BufferingClientHttpResponseWrapper(response);

            String body = StreamUtils.copyToString(responseWrapper.getBody(), StandardCharsets.UTF_8);
            log.info("RESPONSE body: {}", body);

            return responseWrapper;
        }
    }

    static class BufferingClientHttpResponseWrapper implements ClientHttpResponse {

        private final ClientHttpResponse response;
        private byte[] body;

        BufferingClientHttpResponseWrapper(ClientHttpResponse response) {
            this.response = response;
        }

        @NonNull
        public HttpStatusCode getStatusCode() throws IOException {
            return this.response.getStatusCode();
        }

        @NonNull
        public String getStatusText() throws IOException {
            return this.response.getStatusText();
        }

        @NonNull
        public HttpHeaders getHeaders() {
            return this.response.getHeaders();
        }

        @NonNull
        public InputStream getBody() throws IOException {
            if (this.body == null) {
                this.body = StreamUtils.copyToByteArray(this.response.getBody());
            }
            return new ByteArrayInputStream(this.body);
        }

        public void close() {
            this.response.close();
        }
    }

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder
                .requestFactory(() -> new HttpComponentsClientHttpRequestFactory(httpClient()))
                .interceptors(new CustomClientHttpRequestInterceptor())
                .build();
    }

    @Bean
    public RestClient restClient(RestTemplate restTemplate) {
        return RestClient.builder(restTemplate).requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient())).build();
    }
}

In the configuration we still define a RestTemplate and use it to initialize the RestClient, so that we can keep RestTemplate’s log-printing feature (see https://github.com/qihaiyan/springcamp/tree/main/spring-rest-template-log)

If you don’t want to keep using RestTemplate, the initialization code can be changed to

RestClient.builder().requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient())).build();

We also configure a requestFactory for the RestClient, so it can call APIs over persistent connections.

3. Calling GET APIs

Calling a GET API and returning a String:

restClient.get()
                .uri("https://httpbin.org/get")
                .retrieve()
                .body(String.class)

Calling a GET API and returning an object:

restClient.get()
                .uri("https://httpbin.org/get")
                .retrieve()
                .body(MyData.class);

Calling a GET API and returning a List:

List<String> list = restClient.get()
                .uri("http://someservice/list")
                .retrieve()
                .body(new ParameterizedTypeReference<>() {});

4. Calling POST APIs

MyData postBody = new MyData("test", "test RestClient");
        ResponseEntity<String> respObj = restClient.post()
                .uri("https://httpbin.org/post")
                .contentType(MediaType.APPLICATION_JSON)
                .body(postBody)
                .retrieve()
                .toEntity(String.class);

5. Calling APIs with Exchange

When you need finer-grained control over the API response, you can use the exchange method. For example, have the restClient return an empty string when the API responds with 4xx, and the normal result otherwise:

restClient.get()
                .uri("https://httpbin.org/get")
                .accept(MediaType.APPLICATION_JSON)
                .exchange((request, response) -> {
                    if (response.getStatusCode().is4xxClientError()) {
                        log.info("status 4xx");
                        return "";
                    } else {
                        log.info("response: {}", response);
                        return response;
                    }
                });

6. Error Handling

When the API returns an error, you can check for it in the onStatus method and act accordingly:

restClient.get()
                .uri("https://httpbin.org/status/404")
                .retrieve()
                .onStatus(status -> status.value() == 404, (request, response) -> {
                    log.info("status 404");
                })
                .toBodilessEntity();

The toBodilessEntity method is a way of ignoring the API response; use it when you don’t need to read the API response result.

qihaiyan
Written by qihaiyan
业精于勤而荒于嬉,行成于思而毁于随