Spring 5 WebClient 和 WebTestClient 教程与示例

x33g5p2x  于2021-10-20 转载在 Spring  
字(10.9k)|赞(0)|评价(0)|浏览(1561)

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 模块的入门项目 -

  1. 前往http://start.spring.io
  2. ArtifactGroup 设置为 webclient-demo
  3. Package 设置为 com.example.webclientdemo
  4. 添加 Reactive WebValidation 依赖项。
    1、点击Generate生成并下载工程。

使用 WebClient 使用远程 API

让我们让事情变得有趣并使用 WebClient 来使用 Real World API。

在本文中,我们将使用 WebClient 消费 Github’s APIs。我们将使用 WebClient 对用户的 Github 存储库执行 CRUD 操作。
您可以从头开始阅读并理解 WebClient 的点点滴滴,或者从 Github 下载包含所有示例的整个演示项目。

创建一个 WebClient 实例

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 发出请求并检索响应

以下是如何使用 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 生成的个人访问令牌。

使用 exchange() 方法检索响应

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 中使用参数并在 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

您还可以使用 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);
}

在 WebClient 请求中传递请求正文

如果你有 MonoFlux 形式的请求体,那么你可以直接将它传递给 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 类包含从 ObjectPublisherResource、​​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 有两个参数 -

  1. ClientRequest
  2. 过滤器链中的下一个 ExchangeFilterFunction

它可以修改ClientRequest并调用过滤器链中的下一个ExchangeFilterFucntion进行下一个过滤器或直接返回修改后的ClientRequest来阻塞过滤器链。

1. 使用过滤器功能添加基本身份验证

在上面的所有示例中,我们都包含一个 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 请求并添加此标头。

2. 使用过滤器功能记录所有请求

让我们看一个自定义 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);
    };
}
3. 使用 ofRequestProcessor() 和 ofResponseProcessor() 工厂方法来创建过滤器

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);
    });
}

处理 WebClient 错误

每当收到状态码为 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());
}

使用 Spring 5 WebTestClient 测试 Rest API

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();
	}
}

相关文章