RestClient客户端

RestClient是SpringWeb 6.x(SpringBoot 3.x)中引入的新一代HTTP客户端工具类,用于和Restful API交互。在SpringBoot 2.x时代老版本的RestTemplate广泛使用,然而到了SpringBoot 3.x时代RestTemplate虽然暂未移除但已经趋于Deprecated,非必要不建议使用,如果使用了则应逐渐过渡到新客户端RestClient,它提供了更加现代化的API设计,使用起来更加直观和简洁。

注入RestClient对象

实际开发中,RestClient通常以SpringBean的形式注入其它组件中,以便复用配置好的客户端。在SpringBoot工程中,我们通常会进行如下配置。

package com.gacfox.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {
    @Bean
    public RestClient restClient() {
        return RestClient.create();
    }
}

如果需要为所有请求统一设置BaseURL、请求头等公共信息,也可以借助RestClient.Builder进行配置。

@Bean
public RestClient restClient() {
    return RestClient.builder()
            .baseUrl("http://localhost:8080")
            .build();
}

发送GET请求

假设http://localhost:8080/api/v1/getUserById是一个返回JSON数据的HTTP接口,它的返回内容对应User类。这里我们可以使用RestClient发起GET请求获取User对象,下面是一个例子。

package com.gacfox.demo.service;

import com.gacfox.demo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@Service("demoService")
public class DemoService {
    private final RestClient restClient;

    @Autowired
    public DemoService(RestClient restClient) {
        this.restClient = restClient;
    }

    public User getUserById(Long userId) {
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:8080/api/v1/getUserById")
                .queryParam("userId", userId)
                .build()
                .encode()
                .toUri();
        return restClient.get()
                .uri(uri)
                .retrieve()
                .body(User.class);
    }
}

代码中,我们使用UriComponentsBuilder构建了GET请求的URL,然后通过RestClient发起GET请求,并指定了返回值类型为User。请求发出时,RestClient会自动添加请求头Accept: application/json表示期望返回JSON数据,得到响应数据后,则会自动将JSON数据反序列化为User对象。

如果服务端未返回2xx状态码或解析返回数据时出错,RestClient将抛出对应的异常类型。

发送POST请求

发送POST请求也是类似的,下面代码我们使用POST请求发送JSON数据并接收JSON响应。

package com.gacfox.demo.service;

import com.gacfox.demo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

@Service("demoService")
public class DemoService {
    private final RestClient restClient;

    @Autowired
    public DemoService(RestClient restClient) {
        this.restClient = restClient;
    }

    public User postUser(User user) {
        return restClient.post()
                .uri("http://localhost:8080/api/v1/postUser")
                .contentType(MediaType.APPLICATION_JSON)
                .body(user)
                .retrieve()
                .body(User.class);
    }
}

代码中,我们依次指定了请求方法、URL、Content-Type请求头、请求体以及响应类型。请求参数user会被自动序列化为JSON数据,响应数据也会被自动反序列化为对应类型。

toEntity()方法与复杂泛型参数

在实际开发中,对于更复杂的业务场景,我们可能需要获取完整的HTTP响应信息(如状态码、响应头等),或者需要处理带有泛型的响应类型。RestClient通过toEntity()方法支持这两类需求。

package com.gacfox.demo.service;

import com.gacfox.demo.model.QueryParam;
import com.gacfox.demo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

import java.util.List;

@Service("demoService")
public class DemoService {
    private final RestClient restClient;

    @Autowired
    public DemoService(RestClient restClient) {
        this.restClient = restClient;
    }

    public List<User> getUserList(QueryParam queryParam) {
        ResponseEntity<List<User>> responseEntity = restClient.post()
                .uri("http://localhost:8080/api/v1/getUserList")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .body(queryParam)
                .retrieve()
                .toEntity(new ParameterizedTypeReference<>() {
                });
        return responseEntity.getBody();
    }
}

代码中,toEntity()方法的参数支持通过ParameterizedTypeReference传递泛型类型,不过由于前面我们已经声明过ResponseEntity<List<User>>,它们使用的是同一个泛型参数,编译器能自动推断,因此这里的List<User>被省略了。toEntity()方法的返回值是ResponseEntity,它包含了HTTP响应的详细信息,如响应状态码、响应头、响应体等,这里我们使用getBody()方法获取响应体,它会被自动反序列化为对应类型。

如果只关心响应体而不需要完整响应信息,也可以直接使用body()方法替代toEntity(),并传入ParameterizedTypeReference参数。

List<User> userList = restClient.post()
        .uri("http://localhost:8080/api/v1/getUserList")
        .contentType(MediaType.APPLICATION_JSON)
        .body(queryParam)
        .retrieve()
        .body(new ParameterizedTypeReference<>() {
        });

此外,和前面的方法类似,当服务端没能返回2xx状态码或响应解析出错时,RestClient会抛出对应的异常类型。

发送form-data类型的表单请求

RestClient默认发送JSON数据,但有些情况如文件上传等我们可能需要发送form-data类型的表单请求,通过手动设置Content-Type并构造MultiValueMap即可实现。

package com.gacfox.demo.service;

import com.gacfox.demo.model.ApiResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClient;

@Service("demoService")
public class DemoService {
    private final RestClient restClient;

    @Autowired
    public DemoService(RestClient restClient) {
        this.restClient = restClient;
    }

    public void upload(Long userId, byte[] imageData) {
        ByteArrayResource byteArrayResource = new ByteArrayResource(imageData) {
            @Override
            public String getFilename() {
                return "avatar.jpg";
            }
        };
        MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
        formData.add("userId", String.valueOf(userId));
        formData.add("image", byteArrayResource);
        restClient.post()
                .uri("http://localhost:8080/api/v1/upload")
                .contentType(MediaType.MULTIPART_FORM_DATA)
                .accept(MediaType.APPLICATION_JSON)
                .body(formData)
                .retrieve()
                .body(new ParameterizedTypeReference<ApiResult<Void>>() {});
    }
}

代码中,我们设置发送数据的Content-Typemultipart/form-data,然后构造了一个MultiValueMap类型的表单数据,其中userId字段是普通文本字段,image字段是文件字段,注意image字段我们使用ByteArrayResource类型重新封装byte[]并重写了getFilename()方法,这样服务端才能正确解析。

处理文件下载

RestClient处理文件下载非常简单,我们只需要将响应体接收类型设置为byte[],这样就可以以二进制数据的形式读取服务端返回的内容。

package com.gacfox.demo.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

@Service("demoService")
public class DemoService {
    private final RestClient restClient;

    @Autowired
    public DemoService(RestClient restClient) {
        this.restClient = restClient;
    }

    public byte[] download() {
        return restClient.get()
                .uri("http://localhost:8080/api/v1/download")
                .retrieve()
                .body(byte[].class);
    }
}

客户端拦截器

RestClient支持配置拦截器,这需要实现ClientHttpRequestInterceptor接口,并编写intercept方法。下面例子中我们实现了一个例子拦截器,其中打印了一些请求时的详细信息。

package com.gacfox.demo.interceptor.client;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.IOException;

@Slf4j
public class LogInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        log.info("=== REST Client Request ===");
        log.info("URI     : {}", request.getURI());
        log.info("Method  : {}", request.getMethod());
        log.info("Headers : {}", request.getHeaders());
        return execution.execute(request, body);
    }
}

拦截器需要在RestClient创建时传入,下面是一个例子。

package com.gacfox.demo.config;

import com.gacfox.demo.interceptor.client.LogInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {
    @Bean
    public RestClient restClient() {
        return RestClient.builder()
                .requestInterceptor(new LogInterceptor())
                .build();
    }
}

客户端配置

RestClient底层的配置信息可以在创建Bean时通过ClientHttpRequestFactory指定,下面是JavaConfig的例子,代码中我们配置了RestClient的连接超时和读超时时间。

package com.gacfox.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {
    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setReadTimeout(15000);
        factory.setConnectTimeout(5000);
        return factory;
    }

    @Bean
    public RestClient restClient(ClientHttpRequestFactory factory) {
        return RestClient.builder()
                .requestFactory(factory)
                .build();
    }
}

自定义HTTP客户端

RestClient底层实现了自动配置,它会自动按顺序搜索工程中是否引入了Apache HttpClient、Jetty HttpClient、Reactor Netty HttpClient、JDK HttpClient,如果工程没有什么特殊配置,一般来说都会使用JDK HttpClient。如果我们希望对底层客户端有更多精细控制,推荐引入Apache HttpClient。

<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
</dependency>

如下JavaConfig配置了RestClient使用Apache HttpClient作为底层实现。

package com.gacfox.demo.config;

import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
import org.apache.hc.core5.pool.PoolReusePolicy;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {

    @Bean
    public RestClient restClient(RestClient.Builder builder) {

        ConnectionConfig connectionConfig = ConnectionConfig.custom()
                .setConnectTimeout(Timeout.ofSeconds(5))
                .setSocketTimeout(Timeout.ofSeconds(15))
                .setTimeToLive(TimeValue.ofMinutes(10))
                .setValidateAfterInactivity(TimeValue.ofSeconds(5))
                .build();

        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(Timeout.ofSeconds(5))
                .setResponseTimeout(Timeout.ofSeconds(15))
                .setRedirectsEnabled(true)
                .build();

        PoolingHttpClientConnectionManager connectionManager =
                PoolingHttpClientConnectionManagerBuilder.create()
                        .setDefaultConnectionConfig(connectionConfig)
                        .setMaxConnTotal(200)
                        .setMaxConnPerRoute(50)
                        .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.STRICT)
                        .setConnPoolPolicy(PoolReusePolicy.LIFO)
                        .build();

        CloseableHttpClient httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .setDefaultRequestConfig(requestConfig)
                .evictExpiredConnections()
                .evictIdleConnections(TimeValue.ofSeconds(30))
                .build();

        return builder
                .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient))
                .build();
    }
}

代码中我们创建了CloseableHttpClient客户端,并为其配置了超时、连接池等信息,随后我们使用该客户端创建了RestClient

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。