Spring 开发者们,您是否曾经觉得需要一个 异步/非阻塞 HTTP 客户端,具有 流畅的功能风格 API,易于使用且高效?
如果是,那么我欢迎您阅读有关 WebClient 的文章,这是 Spring 5 中引入的新的响应式 HTTP 客户端。
###如何使用WebClient
WebClient 是 Spring 5 的响应式 Web 框架 Spring WebFlux 的一部分。要使用 WebClient,您需要在项目中包含 spring-webflux
模块。
在现有的 Spring Boot 项目中添加依赖
如果您已有 Spring Boot 项目,则可以通过在 pom.xml
文件中添加以下依赖项来添加 spring-webflux
模块 -
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
从头开始创建一个新项目
如果您是从头开始创建项目,那么您可以从 Spring Initializr 网站生成一个带有 spring-webflux
模块的入门项目 -
webclient-demo
。com.example.webclientdemo
。让我们让事情变得有趣并使用 WebClient 来使用 Real World API。
在本文中,我们将使用 WebClient 消费 Github’s APIs。我们将使用 WebClient 对用户的 Github 存储库执行 CRUD 操作。
您可以从头开始阅读并理解 WebClient 的点点滴滴,或者从 Github 下载包含所有示例的整个演示项目。
1.使用 create()
方法创建 WebClient
您可以使用 create()
工厂方法创建 WebClient 的实例 -
WebClient webClient = WebClient.create();
如果您仅使用来自特定服务的 API,那么您可以使用该服务的 baseUrl 初始化 WebClient,如下所示 -
WebClient webClient = WebClient.create("https://api.github.com");
2.使用 WebClient 构建器创建 WebClient
WebClient 还带有一个构建器,可为您提供一系列自定义选项,包括过滤器、默认标题、cookie、客户端连接器等 -
WebClient webClient = WebClient.builder()
.baseUrl("https://api.github.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.github.v3+json")
.defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient")
.build();
以下是如何使用 WebClient 向 Github 的 List Repositories API 发出 GET
请求 -
public Flux<GithubRepo> listGithubRepositories(String username, String token) {
return webClient.get()
.uri("/user/repos")
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToFlux(GithubRepo.class);
}
看看 API 调用是多么简单和简洁!
假设我们有一个名为 GithubRepo
的类来确认 Github 的 API 响应,上面的函数将返回 GithubRepo
对象的 Flux
。
请注意,我使用 Github 的基本身份验证机制来调用 API。它需要您的 github 用户名和您可以从 https://github.com/settings/tokens 生成的个人访问令牌。
retrieve()
方法是获取响应正文的最简单方法。但是,如果您想对响应有更多的控制,那么您可以使用 exchange()
方法,该方法可以访问整个 ClientResponse
包括所有标题和正文 -
public Flux<GithubRepo> listGithubRepositories(String username, String token) {
return webClient.get()
.uri("/user/repos")
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.exchange()
.flatMapMany(clientResponse -> clientResponse.bodyToFlux(GithubRepo.class));
}
您可以在请求 URI 中使用参数并在 uri()
函数中单独传递它们的值。所有参数都用花括号括起来。在发出请求之前,参数将被 WebClient 自动替换 -
public Flux<GithubRepo> listGithubRepositories(String username, String token) {
return webClient.get()
.uri("/user/repos?sort={sortField}&direction={sortDirection}",
"updated", "desc")
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToFlux(GithubRepo.class);
}
您还可以使用 UriBuilder
像这样获得对请求 URI 的完全编程控制 -
public Flux<GithubRepo> listGithubRepositories(String username, String token) {
return webClient.get()
.uri(uriBuilder -> uriBuilder.path("/user/repos")
.queryParam("sort", "updated")
.queryParam("direction", "desc")
.build())
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToFlux(GithubRepo.class);
}
如果你有 Mono
或 Flux
形式的请求体,那么你可以直接将它传递给 WebClient 中的 body()
方法,否则你可以创建一个来自对象的 Mono/Flux 并像这样传递它 -
public Mono<GithubRepo> createGithubRepository(String username, String token,
RepoRequest createRepoRequest) {
return webClient.post()
.uri("/user/repos")
.body(Mono.just(createRepoRequest), RepoRequest.class)
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToMono(GithubRepo.class);
}
如果您有实际值而不是 Publisher
(Flux
/Mono
),则可以使用 syncBody()
快捷方法来传递请求正文 -
public Mono<GithubRepo> createGithubRepository(String username, String token,
RepoRequest createRepoRequest) {
return webClient.post()
.uri("/user/repos")
.syncBody(createRepoRequest)
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToMono(GithubRepo.class);
}
最后,您可以使用 BodyInserters
类提供的各种工厂方法来构造一个 BodyInserter
对象并将其传递到 body()
方法中。 BodyInserters
类包含从 Object
、Publisher
、Resource
、FormData
、[ [$33$]] 等等 -
public Mono<GithubRepo> createGithubRepository(String username, String token,
RepoRequest createRepoRequest) {
return webClient.post()
.uri("/user/repos")
.body(BodyInserters.fromObject(createRepoRequest))
.header("Authorization", "Basic " + Base64Utils
.encodeToString((username + ":" + token).getBytes(UTF_8)))
.retrieve()
.bodyToMono(GithubRepo.class);
}
WebClient 支持使用 ExchangeFilterFunction
进行请求过滤。您可以使用过滤器功能以任何方式拦截和修改请求。例如,您可以使用过滤器函数为每个请求添加一个 Authorization
标头,或者记录每个请求的详细信息。
ExchangeFilterFunction
有两个参数 -
ClientRequest
和ExchangeFilterFunction
。它可以修改ClientRequest
并调用过滤器链中的下一个ExchangeFilterFucntion
进行下一个过滤器或直接返回修改后的ClientRequest
来阻塞过滤器链。
在上面的所有示例中,我们都包含一个 Authorization
标头,用于使用 Github API 进行基本身份验证。由于这是所有请求的共同点,因此您可以在创建 WebClient
时在过滤器函数中添加此逻辑。
ExchaneFilterFunctions
API 已经为基本身份验证提供了过滤器。你像这样使用它 -
WebClient webClient = WebClient.builder()
.baseUrl(GITHUB_API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, GITHUB_V3_MIME_TYPE)
.filter(ExchangeFilterFunctions
.basicAuthentication(username, token))
.build();
现在,您不需要在每个请求中添加 Authorization
标头。过滤器功能将拦截每个 WebClient 请求并添加此标头。
让我们看一个自定义 ExchangeFilterFunction
的例子。我们将编写一个过滤器函数来拦截和记录每个请求 -
WebClient webClient = WebClient.builder()
.baseUrl(GITHUB_API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, GITHUB_V3_MIME_TYPE)
.filter(ExchangeFilterFunctions
.basicAuthentication(username, token))
.filter(logRequest())
.build();
这里是 logRequest()
过滤器函数的实现 -
private ExchangeFilterFunction logRequest() {
return (clientRequest, next) -> {
logger.info("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers()
.forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value)));
return next.exchange(clientRequest);
};
}
ExchangeFilterFunction API 提供了两个名为 ofRequestProcessor()
和 ofResponseProcessor()
的工厂方法,用于创建分别拦截请求和响应的过滤器函数。
我们在上一节中创建的 logRequest()
过滤器函数可以使用 ofRequestProcessor()
工厂方法创建,如下所示 -
private ExchangeFilterFunction logRequest() {
ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
logger.info("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers()
.forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value)));
return Mono.just(clientRequest);
});
}
如果你想拦截WebClient的响应,你可以使用ofResponseProcessor()
方法来创建一个这样的过滤函数——
private ExchangeFilterFunction logResposneStatus() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
logger.info("Response Status {}", clientResponse.statusCode());
return Mono.just(clientResponse);
});
}
每当收到状态码为 4xx 或 5xx 的响应时,WebClient 中的 retrieve()
方法就会抛出一个 WebClientResponseException
。
您可以使用 onStatus()
方法进行自定义,如下所示 -
public Flux<GithubRepo> listGithubRepositories() {
return webClient.get()
.uri("/user/repos?sort={sortField}&direction={sortDirection}",
"updated", "desc")
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse ->
Mono.error(new MyCustomClientException())
)
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
Mono.error(new MyCustomServerException())
)
.bodyToFlux(GithubRepo.class);
}
请注意,与 retrieve()
方法不同,exchange()
方法不会在 4xx 或 5xx 响应的情况下抛出异常。您需要自己检查状态代码并以您想要的方式处理它们。
@ExceptionHandler
处理 WebClientResponseExceptions您可以在控制器中使用 @ExceptionHandler
来处理 WebClientResponseException
并像这样向客户端返回适当的响应 -
@ExceptionHandler(WebClientResponseException.class)
public ResponseEntity<String> handleWebClientResponseException(WebClientResponseException ex) {
logger.error("Error from WebClient - Status {}, Body {}", ex.getRawStatusCode(), ex.getResponseBodyAsString(), ex);
return ResponseEntity.status(ex.getRawStatusCode()).body(ex.getResponseBodyAsString());
}
WebTestClient 包含类似于 WebClient 的请求方法。此外,它还包含检查响应状态、标头和正文的方法。您还可以将 AssertJ
之类的断言库与 WebTestClient 结合使用。
查看以下示例,了解如何使用 WebTestClient 执行 Rest API 测试 -
package com.example.webclientdemo;
import com.example.webclientdemo.payload.GithubRepo;
import com.example.webclientdemo.payload.RepoRequest;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class WebclientDemoApplicationTests {
@Autowired
private WebTestClient webTestClient;
@Test
@Order(1)
public void testCreateGithubRepository() {
RepoRequest repoRequest = new RepoRequest("test-webclient-repository", "Repository created for testing WebClient");
webTestClient.post().uri("/api/repos")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(repoRequest), RepoRequest.class)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody()
.jsonPath("$.name").isNotEmpty()
.jsonPath("$.name").isEqualTo("test-webclient-repository");
}
@Test
@Order(2)
public void testGetAllGithubRepositories() {
webTestClient.get().uri("/api/repos")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBodyList(GithubRepo.class);
}
@Test
@Order(3)
public void testGetSingleGithubRepository() {
webTestClient.get()
.uri("/api/repos/{repo}", "test-webclient-repository")
.exchange()
.expectStatus().isOk()
.expectBody()
.consumeWith(response ->
Assertions.assertThat(response.getResponseBody()).isNotNull());
}
@Test
@Order(4)
public void testEditGithubRepository() {
RepoRequest newRepoDetails = new RepoRequest("updated-webclient-repository", "Updated name and description");
webTestClient.patch()
.uri("/api/repos/{repo}", "test-webclient-repository")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(Mono.just(newRepoDetails), RepoRequest.class)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody()
.jsonPath("$.name").isEqualTo("updated-webclient-repository");
}
@Test
@Order(5)
public void testDeleteGithubRepository() {
webTestClient.delete()
.uri("/api/repos/{repo}", "updated-webclient-repository")
.exchange()
.expectStatus().isOk();
}
}
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://www.callicoder.com/spring-5-reactive-webclient-webtestclient-examples/
内容来源于网络,如有侵权,请联系作者删除!