WebClient 和 RestClient

WebClient 和 RestClient

简介

1
Fourteen years ago, when RestTemplate was introduced in Spring Framework 3.0, we quickly discovered that exposing every capability of HTTP in a template-like class resulted in too many overloaded methods. 

十四年前,当在 Spring Framework 3.0 中引入时 RestTemplate,我们很快发现,在类似模板的类中公开HTTP的所有功能会导致太多的重载方法。

所以在 Spring Framework 中实现了如下两种客户端,用来执行 Http 请求:

  • WebClient:异步客户端
  • RestClient:同步客户端

WebClient 实现

引入 WebFlux 包:

1
2
3
4
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-web'
}

编写请求结果类:

1
2
public record JokeResponse(String id, String joke, Integer status) {
}

编写请求类:

1
2
3
4
5
6
7
8
import org.springframework.web.service.annotation.GetExchange;

public interface JokeClient {

@GetExchange("/")
JokeResponse random();

}

编写主类和 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.support.WebClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Bean
JokeClient jokeClient(WebClient.Builder builder) {
WebClient client = builder
.baseUrl("https://icanhazdadjoke.com/")
.defaultHeader("Accept", "application/json")
.build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(WebClientAdapter.create(client)).build();
return factory.createClient(JokeClient.class);
}

}

编写测试接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping
public class TestController {

@Resource
JokeClient jokeClient;

@GetMapping("/")
public String test() {
JokeResponse response = jokeClient.random();
return response.joke();
}

}

更换支持库(可选):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Bean
public JettyResourceFactory resourceFactory() {
return new JettyResourceFactory();
}

@Bean
public WebClient webClient() {

HttpClient httpClient = new HttpClient();
ClientHttpConnector connector =
new JettyClientHttpConnector(httpClient, resourceFactory());

return WebClient.builder().clientConnector(connector).build();
}

注:每种库的日志需要单独调节,支持库的清单参阅官方文档。

RestClient 实现

引入 Web 包:

1
2
3
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}

编写请求结果类:

1
2
public record JokeResponse(String id, String joke, Integer status) {
}

编写请求类:

1
2
3
4
5
6
7
8
import org.springframework.web.service.annotation.GetExchange;

public interface JokeClient {

@GetExchange("/")
JokeResponse random();

}

编写主类和 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Bean
JokeClient jokeClient(RestClient.Builder builder) {
RestClient client = builder
.baseUrl("https://icanhazdadjoke.com/")
.defaultHeader("Accept", "application/json")
.build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(RestClientAdapter.create(client)).build();
return factory.createClient(JokeClient.class);
}

}

编写测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping
public class TestController {

@Resource
JokeClient jokeClient;

@GetMapping("/")
public String test() {
JokeResponse response = jokeClient.random();
return response.joke();
}

}

更换支持库(可选):

1
2
3
4
5
6
7
8
9
@Bean
public RestClient restClient() {
JettyClientHttpRequestFactory requestFactory = new JettyClientHttpRequestFactory()
RestClient client = RestClient.builder()
.requestFactory(requestFactory)
.build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(RestClientAdapter.create(client)).build();
return client;
}

注:每种库的日志需要单独调节,支持库的清单参阅官方文档。

常见问题

获取请求状态码

可以使用如下样例获取请求结果对象,然后读取状态码等信息

1
2
3
4
5
6
7
8
9
import org.springframework.http.ResponseEntity;
import org.springframework.web.service.annotation.GetExchange;

public interface JokeClient {

@GetExchange("/")
ResponseEntity<JokeResponse> random();

}

Mock 测试

编写如下测试程序即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;


@RestClientTest(JokeClient.class)
public class JokeTest {

@Autowired
MockRestServiceServer server;

@Autowired
JokeClient jokeClient;

@Autowired
ObjectMapper objectMapper;

@Test
public void shouldReturnAllPosts() throws JsonProcessingException {
JokeResponse jokeResponse = new JokeResponse("1", "demo", 1);
this.server
.expect(requestTo("https://icanhazdadjoke.com/"))
.andRespond(withSuccess(objectMapper.writeValueAsString(jokeResponse), MediaType.APPLICATION_JSON));

JokeResponse result = jokeClient.random();
assertThat(result.joke()).isEqualTo("demo");
}
}

拦截器

可以使用拦截器的方式改变请求中的内容样例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class TokenInterceptor implements ClientHttpRequestInterceptor {

private static final Logger log = LoggerFactory.getLogger(TokenInterceptor.class);

@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
log.info("Intercepting request: " + request.getURI());
request.getHeaders().add("x-request-id", "12345");
return execution.execute(request, body);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

@Component
public class JokeClient {

private final RestClient restClient;

public JokeClient(RestClient.Builder builder, ClientHttpRequestInterceptor tokenInterceptor) {
this.restClient = builder
.baseUrl("https://icanhazdadjoke.com")
.defaultHeader("Accept", "application/json")
.requestInterceptor(tokenInterceptor)
.build();
}

public JokeResponse random() {
return restClient.get()
.uri("/")
.retrieve()
.body(JokeResponse.class);
}

}

参考资料

WebClient 文档

RestClient 文档


WebClient 和 RestClient
https://wangqian0306.github.io/2023/webclient-restclient/
作者
WangQian
发布于
2023年4月14日
许可协议