Spring之RestClient、WebClient和HTTP Interface

1 RestClient、WebClient、HTTP Interface

1.1 介绍

1.1.1 简介

Spring 框架一直提供了两种不同的客户端来执行 http 请求:

  • RestTemplate:它在 Spring 3 中被引入,提供同步的阻塞式通信。
    点击了解 Spring之RestTemplate详解
  • WebClient:它在 Spring 5Spring WebFlux 库中作为一部分被发布。它提供了流式 API,遵循响应式模型。

由于 RestTemplate 的方法暴露了太多的 HTTP 特性,导致了大量重载的方法,使用成本较高。WebClientRestTemplate 的替代品,支持同步和异步调用。它是 Spring Web Reactive 项目的一部分。

现在 Spring 6.1 M1 版本引入了 RestClient。一个新的同步 http 客户端,其工作方式与 WebClient 类似,使用与 RestTemplate 相同的基础设施。

HTTP InterfaceSpring 6 的第一个 GA 版本发布了,其中带来了一个新的特性——HTTP Interface。这个新特性,可以让开发者将 HTTP 服务,定义成一个包含特定注解标记的方法的 Java 接口,然后通过对接口方法的调用,完成 HTTP 请求。看起来很像使用 Feign 来完成远程服务调用

1.1.2 WebClient与RestTemplate

WebClient 优于 RestTemplate 的原因有几个:

  • 非阻塞 I/O:WebClient 构建在 Reactor 之上,它提供了一种非阻塞、反应式的方法来处理 I/O。这可以在高流量应用程序中实现更好的可扩展性和更高的性能。
  • 函数式风格:WebClient 使用函数式编程风格,可以使代码更易于阅读和理解。它还提供了流畅的 API,可以更轻松地配置和自定义请求。
  • 更好地支持流式传输:WebClient 支持请求和响应正文的流式传输,这对于处理大文件或实时数据非常有用。
  • 改进的错误处理:WebClient 提供比 RestTemplate 更好的错误处理和日志记录,从而更轻松地诊断和解决问题。

1.1.3 RestClient和WebClient

RestClientWebClient 都是用于在 Spring 中进行 HTTP 请求的工具,但它们有一些关键的区别:

  • 模块位置:
    RestClientSpring 6.1 M1 引入的一个新特性,它是 Spring Frameworkorg.springframework.web.client 包下的一部分,用于简化传统的 REST 客户端代码。
    WebClientSpring WebFlux 中的一个模块,属于响应式编程的一部分,位于 org.springframework.web.reactive.client 包下。它是在 Spring 5 引入的,主要用于构建响应式的、非阻塞的 Web 客户端。
  • 编程风格:
    RestClient 主要采用传统的阻塞式编程风格,适用于传统的 Servlet 环境。
  • WebClient 是响应式编程的一部分,支持非阻塞式、异步的编程风格。它更适用于构建响应式的、高并发的应用。
  • 响应式支持:
    WebClient 提供了对响应式编程的支持,可以异步地处理 HTTP 请求和响应。
    RestClient 不提供响应式编程的支持,主要采用同步的方式进行 HTTP 请求和响应。
  • 线程模型:
    RestClient 使用传统的阻塞式线程模型。
    WebClient 使用响应式线程模型,可以更好地处理并发请求。

1.2 RestClient

1.2.1 pom.xml

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0-M2</version>
        <relativePath/>
    </parent>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

在这里插入图片描述

1.2.2 创建全局 RestClient

创建 RestClient 实例有可用的静态方法:

  • create():委托给默认的 rest 客户端。
  • create(String url):接受一个默认的基础 url。
  • create(RestTemplate restTemplate):基于给定 rest 模板的配置初始化一个新的 RestClient。
  • builder():允许使用 headers、错误处理程序、拦截器等选项自定义一个 RestClient
  • builder(RestTemplate restTemplate):基于给定 RestTemplate 的配置获取一个 RestClient builder

让我们使用 builder 方法调用客户 API 来编写一个 RestClient

RestClient restClient = RestClient.builder()
  .baseUrl(properties.getUrl())
  .defaultHeader(HttpHeaders.AUTHORIZATION,
      encodeBasic("pig", "pig")
  ).build();

参数说明:

  • baseUrl:设置基础 url
  • defaultHeader:允许设置一个默认 http 请求头

1.2.3 Get接收数据 retrieve

使用客户端发送 http 请求并接收响应。
RestClient 为每种 HTTP 方法都提供了方法。例如,要搜索所有活动客户,必须执行 GET 请求。retrieve 方法获取响应并声明如何提取它。

让我们从使用完整正文作为 String 的简单情况开始。

String data = restClient.get()
  .uri("?name={name}&type={type}", "lengleng", "1")
  .accept(MediaType.APPLICATION_JSON)
  .retrieve()
  .body(String.class);

logger.info(data);

uri 方法可以设置 http 参数第一个参数(一个字符串模板)是附加到 RestClient 中定义的 base url 的查询字符串。第二个参数是模板的 uri 变量(varargs)。

我们还指定媒体类型为 JSON。输出显示在控制台中:

[
  {
    "id":1,
    "name":"lengleng",
    "type":"1"
  }
]

如果需要检查响应状态码或响应头怎么办,toEntity 方法会返回一个 ResponseEntity

ResponseEntity response = restClient.get()
  .uri("?name={name}&type={type}", "lengleng", "1")
  .accept(MediaType.APPLICATION_JSON)
  .retrieve()
  .toEntity(String.class);

logger.info("Status " + response.getStatusCode());
logger.info("Headers " + response.getHeaders());

1.2.4 结果转换 Bean

RestClient 还可以将响应主体转换为 JSON 格式。Spring 将自动默认注册 MappingJackson2HttpMessageConverterMappingJacksonHttpMessageConverter,如果在类路径中检测到 Jackson 2 库或 Jackson 库。但是可以注册自己的消息转换器并覆盖默认设置。

在我们的例子中,响应可以直接转换为记录。例如,检索特定客户的 API:

ReqUserResponse customer = restClient.get()
  .uri("/{name}","lengleng")
  .accept(MediaType.APPLICATION_JSON)
  .retrieve()
  .body(ReqUserResponse.class);

logger.info("res name: " + customer.personInfo().name());

要搜索客户,我们只需要使用 List 类,如下所示:

List<ReqUserResponse> customers = restClient.get()
  .uri("?type={type}", "1")
  .accept(MediaType.APPLICATION_JSON)
  .retrieve()
  .body(List.class);

logger.info("res size " + customers.size());

1.2.5 Post发布数据

要发送 post 请求,只需调用 post 方法。下一段代码片段创建一个新客户。

ReqUserResponse customer = new ReqUserResponse(
  "lengleng-plus",
  "1"
);

ResponseEntity<Void> response = restClient.post()
  .accept(MediaType.APPLICATION_JSON)
  .body(customer)
  .retrieve()
  .toBodilessEntity();

if (response.getStatusCode().is2xxSuccessful()) {
  logger.info("Created " + response.getStatusCode());
  logger.info("New URL " + response.getHeaders().getLocation());
}

响应代码确认客户已成功创建:

Created 201 CREATED
New URL http://localhost:8080/api/v1/customers/11

要验证客户是否已添加,可以通过 postman 检索以上 URL:

{
  "id": 2,
  "name": "lengleng-plus",
  "type": "1"
}

当然,可以使用与前一节类似的代码通过 RestClient 获取它。

1.2.6 Delete删除数据

调用 delete 方法发出 HTTP delete 请求尝试删除资源非常简单。

ResponseEntity<Void> response = restClient.delete()
  .uri("/{id}",2)
  .accept(MediaType.APPLICATION_JSON)
  .retrieve()
  .toBodilessEntity();

logger.info("Deleted with status " + response.getStatusCode());

值得一提的是,如果操作成功,响应主体将为空。对于这种情况,toBodilessEntity 方法非常方便。要删除的客户 ID 作为 uri 变量传递。

Deleted with status 204 NO_CONTENT

1.2.7 处理错误

如果我们尝试删除或查询一个不存在的客户会发生什么?客户端点将返回一个 404 错误代码以及消息详细信息。然而,每当接收到客户端错误状态码(400-499)或服务器错误状态码(500-599)时,RestClient 将抛出 RestClientException 的子类。

要定义自定义异常处理程序,有两种选项适用于不同的级别:

RestClient 中使用 defaultStatusHandler 方法(对其发送的所有 http 请求)

RestClient restClient = RestClient.builder()
  .baseUrl(properties.getUrl())
  .defaultHeader(HttpHeaders.AUTHORIZATION,
                 encodeBasic("pig","pig"))
  .defaultStatusHandler(
    HttpStatusCode::is4xxClientError,
    (request, response) -> {
      logger.error("Client Error Status " + response.getStatusCode());
      logger.error("Client Error Body "+new String(response.getBody().readAllBytes()));
  })
  .build();

在运行删除命令行运行程序后,控制台的输出如下:

Client Error Status 404 NOT_FOUND
Client Error Body {"status":404,"message":"Entity Customer for id 2 was not found.","timestamp":"2023-07-23T09:24:55.4088208"}

另一种选择是为删除操作实现 onstatus 方法。它优先于 RestClient 默认处理程序行为。

ResponseEntity response = restClient.delete()
  .uri("/{id}",2)
  .accept(MediaType.APPLICATION_JSON)
  .retrieve()
  .onStatus(HttpStatusCode::is4xxClientError,
    (req, res) -> logger.error("Couldn't delete "+res.getStatusText())
  )
  .toBodilessEntity();

if (response.getStatusCode().is2xxSuccessful())
  logger.info("Deleted with status " + response.getStatusCode());

现在控制台中的消息将是:

Couldn't delete Not Found

1.2.8 Exchange 方法

当响应必须根据响应状态进行不同解码时,exchange 方法很有用。使用 exchange 方法时,状态处理程序将被忽略。

在这个虚构的示例代码中,响应基于状态映射到实体:

SimpleResponse simpleResponse = restClient.get()
  .uri("/{id}",4)
  .accept(MediaType.APPLICATION_JSON)
  .exchange((req,res) ->
    switch (res.getStatusCode().value()) {
      case 200 -> SimpleResponse.FOUND;
      case 404 -> SimpleResponse.NOT_FOUND;
      default -> SimpleResponse.ERROR;
    }
  );

1.3 WebClient

1.3.1 创建网络客户端

import io.netty.channel.ChannelOption;
import io.netty.channel.ConnectTimeoutException;
import io.netty.handler.timeout.ReadTimeoutException;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.TimeoutException;
import jakarta.annotation.PostConstruct;
import java.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientRequestException;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;


HttpClient httpClient =
        HttpClient.create()
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout)
            .responseTimeout(Duration.ofMillis(requestTimeout))
            .doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(readTimeout)));

   WebClient client =
        WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).build();

1.3.2 同步发送请求(与RestTemplate一样)

如果想坚持使用发送 HTTP 请求并等待响应的老方法,也可以使用 WebClient 实现如下所示的相同功能:

public String postSynchronously(String url, String requestBody) {
  LOG.info("Going to hit API - URL {} Body {}", url, requestBody);
  String response = "";
  try {
    response =
        client
            .method(HttpMethod.POST)
            .uri(url)
            .accept(MediaType.ALL)
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(requestBody)
            .retrieve()
            .bodyToMono(String.class)
            .block();

  } catch (Exception ex) {
    LOG.error("Error while calling API ", ex);
    throw new RunTimeException("XYZ service api error: " + ex.getMessage());
  } finally {
    LOG.info("API Response {}", response);
  }

  return response;
}

block()用于同步等待响应,这可能并不适合所有情况,可能需要考虑subscribe()异步使用和处理响应。

1.3.3 异步发送请求

有时我们不想等待响应,而是希望异步处理响应,这可以按如下方式完成:

import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

public static Mono<String> makePostRequestAsync(String url, String postData) {
    WebClient webClient = WebClient.builder().build();
    return webClient.post()
            .uri(url)
            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
            .body(BodyInserters.fromFormData("data", postData))
            .retrieve()
            .bodyToMono(String.class);
}

要使用此函数,只需传入要向其发送 POST 请求的 URL 以及要在请求正文中以 URL 编码字符串形式发送的数据。该函数将返回来自服务器的响应,或者如果请求由于任何原因失败,则返回一条错误消息。

注意:在此示例中,WebClient 是使用默认配置构建的。可能需要根据不同要求进行不同的配置。
另请注意,block() 用于同步等待响应,这可能并不适合所有情况。可能需要考虑subscribe()异步使用和处理响应。

要使用响应,可以订阅Mono并异步处理响应。下面是一个例子:

makePostRequestAsync( "https://example.com/api" , "param1=value1¶m2=value2" ) 
.subscribe(response -> { 
    // 处理响应
    System.out.println ( response ); 
}, error -> { 
    / / 处理错误
    System.err.println ( error .getMessage ());     
    }
);

subscribe()用于异步处理响应,可以提供两个 lambda 表达式作为 subscribe() 的参数。如果请求成功并收到响应作为参数,则执行第一个 lambda 表达式;如果请求失败并收到错误作为参数,则执行第二个 lambda 表达式

1.3.4 处理4XX和5XX错误

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

public static Mono<String> makePostRequestAsync(String url, String postData) {
    WebClient webClient = WebClient.builder()
            .baseUrl(url)
            .build();
    return webClient.post()
            .uri("/")
            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
            .body(BodyInserters.fromFormData("data", postData))
            .retrieve()
            .onStatus(HttpStatus::is4xxClientError, clientResponse -> Mono.error(new RuntimeException("Client error")))
            .onStatus(HttpStatus::is5xxServerError, clientResponse -> Mono.error(new RuntimeException("Server error")))
            .bodyToMono(String.class);
}

在此示例中,该onStatus()方法被调用两次,一次针对 4xx 客户端错误,一次针对 5xx 服务器错误。onStatus() 每次调用都采用两个参数:

  • aPredicate确定错误状态代码是否与条件匹配
  • aFunction用于返回Mono,即要传播到订阅者的错误信息。

如果状态代码与条件匹配,Mono则会发出相应的状态代码,并且Mono链会因错误而终止。在此示例中,Mono 将发出一条 RuntimeException 错误消息,指示该错误是客户端错误还是服务器错误。

1.3.5 根据错误状态采取行动

要根据Monosubscribe()方法中的错误采取操作,可以在subscribe函数中处理响应的lambda表达式之后添加另一个lambda表达。如果在处理Monumber的过程中出现错误,则执行第二个lambda表达式。

下面是如何使用makePostRequestAsync函数和处理subscribe方法中的错误的更新示例:

makePostRequestAsync("https://example.com/api", "param1=value1&param2=value2")
.subscribe(response -> {
    // handle the response
    System.out.println(response);
}, error -> {
    // handle the error
    System.err.println("An error occurred: " + error.getMessage());
    if (error instanceof WebClientResponseException) {
        WebClientResponseException webClientResponseException = (WebClientResponseException) error;
        int statusCode = webClientResponseException.getStatusCode().value();
        String statusText = webClientResponseException.getStatusText();
        System.err.println("Error status code: " + statusCode);
        System.err.println("Error status text: " + statusText);
    }
});

subscribe方法中的第二个lambda表达式检查错误是否是WebClientResponseException的实例,这是WebClient在服务器有错误响应时抛出的特定类型的异常。如果它是WebClientResponseException的实例,则代码将从异常中提取状态代码和状态文本,并将它们记录到日志中。

1.3.6 处理成功响应和错误的完整代码

responseMono.subscribe(
response -> {
  // handle the response
  LOG.info("SUCCESS API Response {}", response);
},
error -> {
  // handle the error
  LOG.error("An error occurred: {}", error.getMessage());
  LOG.error("error class: {}", error.getClass());

  // Errors / Exceptions from Server
  if (error instanceof WebClientResponseException) {
    WebClientResponseException webClientResponseException =
        (WebClientResponseException) error;
    int statusCode = webClientResponseException.getStatusCode().value();
    String statusText = webClientResponseException.getStatusText();
    LOG.info("Error status code: {}", statusCode);
    LOG.info("Error status text: {}", statusText);
    if (statusCode >= 400 && statusCode < 500) {
      LOG.info(
          "Error Response body {}", webClientResponseException.getResponseBodyAsString());
    }

    Throwable cause = webClientResponseException.getCause();
    LOG.error("webClientResponseException");
    if (null != cause) {
      LOG.info("Cause {}", cause.getClass());
      if (cause instanceof ReadTimeoutException) {
        LOG.error("ReadTimeout Exception");
      }
      if (cause instanceof TimeoutException) {
        LOG.error("Timeout Exception");
      }
    }
  }

  // Client errors i.e. Timeouts etc - 
  if (error instanceof WebClientRequestException) {
    LOG.error("webClientRequestException");
    WebClientRequestException webClientRequestException =
        (WebClientRequestException) error;
    Throwable cause = webClientRequestException.getCause();
    if (null != cause) {
      LOG.info("Cause {}", cause.getClass());
      if (cause instanceof ReadTimeoutException) {
        LOG.error("ReadTimeout Exception");
      }
      
      if (cause instanceof ConnectTimeoutException) {
        LOG.error("Connect Timeout Exception");
      }
    }
  }
});

1.3.7 超时

我们可以在每个请求中设置超时,如下所示:

return webClient
    .method(this.httpMethod)
    .uri(this.uri)
    .headers(httpHeaders -> httpHeaders.addAll(additionalHeaders))
    .bodyValue(this.requestEntity)
    .retrieve()
    .bodyToMono(responseType)
    .timeout(Duration.ofMillis(readTimeout))  // request timeout for this request
    .block();

但是,我们无法在每个请求中设置连接超时,这是WebClient 的属性,只能设置一次。如果需要,我们始终可以使用新的连接超时值创建一个新的 Web 客户端实例。

连接超时、读取超时和请求超时的区别如下:
图片

1.4 HTTP Interface

1.4.1 示例

1.4.1.1 创建服务端

首先创建一个简单的 HTTP 服务,这一步可以创建一个简单的 Spring Boot 工程来完成。
先创建一个实体类:

public class User implements Serializable {

    private int id;
    private String name;
    // 省略构造方法、Getter和Setter
    @Override
    public String toString() {
        return id + ":" + name;
    }
}

再写一个简单的 Controller:

@GetMapping("/users")
public List<User> list() {
    return IntStream.rangeClosed(1, 10)
            .mapToObj(i -> new User(i, "User" + i))
            .collect(Collectors.toList());
}

确保启动服务之后,能够从http://localhost:8080/users地址获取到一个包含十个用户信息的用户列表。

1.4.1.2 SpringBoot工程

下面我们新建一个 SpringBoot 工程
在这里插入图片描述

这里需要注意,Spring Boot 的版本至少需要是 3.0.0,这样它以来的 Spring Framework 版本才是 6.0 的版本,才能够包含 HTTP Interface 特性,另外,Spring Framework 6.0Spring Boot 3.0 开始支持的 Java 版本最低是 17,因此,需要选择至少是 17 的 Java 版本。
另外,需要依赖 Spring WebSpring Reactive Web 依赖

由于Http Interface需要依赖webflux来实现,我们还需添加它的依赖。

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

创建好新的 SpringBoot 工程后,首先需要定义一个 HTTPInterface 接口。最简单的定义如下即可:
需要通过@HttpExchange声明一个Http服务,使用@GetExchange注解表示进行GET请求;

@HttpExchange
public interface UserApiService {
    @GetExchange("/users")
    List<User> getUsers();
}

然后,我们可以写一个测试方法。

@Test
public void getUsers() {
   WebClient client = WebClient.builder().baseUrl("http://localhost:8080/").build();
   HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build();
   UserApiService service = factory.createClient(UserApiService.class);
   List<User> users = service.getUsers();
   for (User user : users) {
      System.out.println(user);
   }
}

1.4.2 深入分析

1.4.2.1 GetExchange(HttpExchange)

上文例子中的 GetExchange 注解代表这个方法代替执行一个 HTTP Get 请求,与此对应,Spring 还包含了其他类似的注解:
在这里插入图片描述

这些注解定义在spring-web模块的org.springframework.web.service.annotation包下,除了 HttpExchange 之外,其他的几个都是 HttpExchange 的特殊形式,这一点与 Spring MVC 中的 RequestMapping/GetMapping 等注解非常相似。

以下是 HttpExchange 的源码:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
@Reflective(HttpExchangeReflectiveProcessor.class)
public @interface HttpExchange {

    @AliasFor("url")
    String value() default "";

    @AliasFor("value")
    String url() default "";

    String method() default "";

    String contentType() default "";

    String[] accept() default {};

}

在上面的例子中,我们只指定了请求的资源路径。

1.4.2.2 UserApiService 实例创建

在上面例子中,我们定义的 HTTP Interface 接口是 UserApiService,在测试方法中,我们通过 HttpServiceProxyFactory 创建了 UserApiService 的实例,这是参考了 Spring 的官方文档的写法。
也可以将创建的过程写到一个 @Bean 方法中,从而可以将创建好的实例注入到其他的组件中。

我们再定义 UserApiService 的时候,只是声明了一个接口,那具体的请求操作是怎么发出的呢,我们可以通过 DEBUG 模式看得出来,这里创建的 UserApiService 的实例,是一个代理对象:
在这里插入图片描述

目前,Spring 还没有提供更方便的方式来创建这些代理对象,不过,之后的版本肯定会提供,如果感兴趣的话,可以从 HttpServiceProxyFactorycreateClient方法的源码中看到一些与创建 AOP 代理相似的代码,因此,我推测 Spring 之后可能会增加类似的注解来方便地创建代理对象。

1.4.3 其他特性

除了上述例子中的简单使用之外,添加了 HttpExchange 的方法还支持各种类型的参数,这一点也与 Spring MVCController 方法类似,方法的返回值也可以是任意自定义的实体类型(就像上面的例子一样),此外,还支持自定义的异常处理。

上面例子中为什么需要 Spring Reactive Web 的依赖
引入了 Spring Reactive Web 的依赖,在创建代理的service对象的时候,使用了其中的 WebClient 类型。这是因为,HTTP Interface 目前只内置了 WebClient 的实现,它属于 Reactive Web 的范畴。Spring 在会在后续版本中推出基于 RestTemplate 的实现。

posted @ 2023-09-06 15:25  上善若泪  阅读(537)  评论(0编辑  收藏  举报