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-Type为multipart/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。