logo-cover-Spring 5 WebClient和WebTestClient使用教程

1.引言

Spring开发人员,您是否曾经觉得需要一个易于使用且高效的流畅功能样式 API 的异步/非阻塞 HTTP客户端?

如果是,那么我欢迎您阅读关于WebClient的文章,WebClient是Spring 5中引入的新的被动HTTP客户端。

 

2.如何使用WebClient

WebClient是Spring 5的反应性Web框架Spring WebFlux的一部分。要使用WebClient,您需要将spring-webflux模块包含在您的项目中。

在现有的Spring Boot项目中添加依赖项

如果您有一个现有的Spring Boot项目,则可以spring-webflux通过在该pom.xml文件中添加以下依赖项来添加该模块-

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

请注意,您需要Spring Boot 2.xx版本才能使用Spring WebFlux模块。

从Scratch创建一个新项目

如果您从头开始创建项目,那么您可以使用Spring Initializr网站的spring-webflux模块生成初始项目-

  1. 转到http://start.spring.io
  2. 选择弹簧引导版本2.xx的
  3. 在依赖项部分添加反应性Web依赖项。
  4. 如果需要,请更改工件的详细信息,然后单击生成工程下载项目。

3.使用WebClient消费远程API

让我们做一些有趣的事情,并使用WebClient来使用Real World API。

在本文中,我们将使用WebClient来使用Github的API。我们将使用WebClient在用户的Github存储库上执行CRUD操作。

创建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 GETGithub的List Repositories API发出请求-

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响应,上面的函数会返回一个FluxGithubRepo对象。

请注意,我使用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请求中传递Request Body

如果你有一个Mono或一个形式的请求体Flux,那么你可以直接将它传递给body()WebClient中的方法,否则你可以从一个对象中创建一个单声道/通量并像这样传递 -

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);
}
如果您具有实际值而不是PublisherFluxMono),则可以使用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类包含的方法来创建一个BodyInserterObjectPublisherResourceFormDataMultipartData等-
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

ExchaneFilterFunctionsAPI已经为基本认证提供了一个过滤器。你可以像这样使用它 -

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的响应retrieve(),WebClient中的方法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执行其他API测试 -

提示:项目源码下载