Spring Boot 不同HTTP客户端 同步&异步请求对比

开发 前端
我们通过调用 CloseableHttpClient 类上的 execute() 方法对应用程序接口进行同步调用,该方法将使用 StringEntity 实例填充的 HttpPost 对象作为输入参数。

环境:Spring Boot3.2.5

1. 简介

超文本传输协议(HTTP)是一种用于传输超媒体文档(如HTML)以及标准格式(如JSON和XML)的API数据的应用层协议。

它是应用程序之间通信时常用的协议,这些应用程序以REST API的形式发布其功能。使用Java构建的应用程序依赖某种形式的HTTP客户端来对其他应用程序进行API调用。

在选择HTTP客户端方面存在多种多样的选项。本文概述了一些主要的库,这些库被用作Java应用程序中的HTTP客户端来进行HTTP请求。

本篇文章将介绍如下几种HTTP 客户端:

  1. Java 11及以上版本编写的应用程序内置了HttpClient
  2. Apache HttpComponents项目的Apache HttpClient
  3. 由Square提供的OkHttpClient
  4. Spring WebFlux中的WebClient

为了覆盖最常见的场景,我们将查看每种类型的客户端发送异步HTTP GET请求和同步POST请求的示例。

2. 实战案例

2.1 准备接口

@RestController
@RequestMapping("/api")
public class ApiController {


  private static List<User> datas = new ArrayList<>() ;
  static {
    datas.addAll(List.of(
        new User(1L, "狗蛋"),
        new User(2L, "観月あかね")
      )) ;
  }
  @GetMapping("/list")
  public List<User> list() {
    return datas ;
  }
  @PostMapping("/save")
  public User save(@RequestBody User user) {
    datas.add(user) ;
    return user ;
  }
}

如上准备了2个接口分别是:GET请求的/list,POST请求的/save。接下来介绍的4个HTTP客户端都将围绕这2个接口进行。

2.2 Java HttpClient

原生的HttpClient作为孵化器模块在Java 9中引入,并在Java 11中作为JEP 321的一部分正式可用。HttpClient替换了自早期Java版本以来JDK中存在的旧版HttpUrlConnection类。它包括以下特性:

  1. 支持HTTP/1.1、HTTP/2和WebSocket
  2. 支持同步和异步编程模型
  3. 以响应式流的方式处理请求和响应体
  4. 支持Cookies

异步GET请求

public static void invoke() throws Exception {


  // 构建客户端
  HttpClient client = HttpClient.newBuilder()
      .version(Version.HTTP_2)
      .followRedirects(Redirect.NORMAL)
      .build() ;
  // 构造请求对象
  HttpRequest request = HttpRequest.newBuilder()
      .uri(new URI(URLConstants.LIST))
      .GET()
      // 设置超时时间
      .timeout(Duration.ofSeconds(5))
      .build() ;
  // 发送异步请求
  client.sendAsync(request, BodyHandlers.ofString())
    .thenApply(HttpResponse::body)
    .thenAccept(System.out::println)
    // 等待请求完成
    .join() ;
}

请求结果

[{"id":1,"name":"狗蛋"},{"id":2,"name":"観月あかね"}]

通过POST请求

对于 HTTP POST 和 PUT,我们会在生成器上调用 POST(BodyPublisher body) 和 PUT(BodyPublisher body) 方法。BodyPublisher 参数有几种开箱即用的实现方式,可以简化请求正文的发送,如下示例:

public static void invokePost() {
  try {
    String requestBody = prepareRequest();
    HttpClient client = HttpClient.newHttpClient();
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(URLConstants.SAVE))
        .POST(HttpRequest.BodyPublishers.ofString(requestBody))
        // 这里必须设置该header,否则会响应415状态码错误
        .header(HttpHeaders.CONTENT_TYPE, "application/json")
        .header(HttpHeaders.ACCEPT, "application/json")
        .build();


    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    System.err.printf("响应结果: %s%n", response.body()) ;
  } catch (Exception e) {
    e.printStackTrace();
  }
}
private static String prepareRequest() throws Exception {
  var objectMapper = new ObjectMapper();
  String requestBody = objectMapper.writeValueAsString(new User(666L, "莉莉"));
  return requestBody;
}

在这里,我们在 prepareRequest() 方法中创建了一个 JSON 字符串,用于通过 HTTP POST() 方法发送请求正文。

接下来,我们将使用构建器模式创建一个 HttpRequest 实例,然后同步调用 REST API。

在创建请求时,我们通过调用 POST() 方法将 HTTP 方法设置为 POST,还通过在 BodyPublisher 实例中封装 JSON 字符串来设置 API URL 和请求正文。

响应是通过使用 BodyHandler 实例从 HTTP 响应中提取的。

2.3 Apache HttpComponents

HttpComponents 是 Apache 软件基金会下的一个项目,它包含了一组用于处理 HTTP 的低级 Java 组件。该项目下的组件分为:

  • HttpCore:一组低级的 HTTP 传输组件,可以用来构建自定义的客户端和服务器端 HTTP 服务。
  • HttpClient:基于 HttpCore 的一个符合 HTTP 规范的 HTTP 代理实现。它还提供了可重用的组件,用于客户端身份验证、HTTP 状态管理和 HTTP 连接管理。

引入依赖

<dependency>
  <groupId>org.apache.httpcomponents.client5</groupId>
  <artifactId>httpclient5</artifactId>
  <version>5.4</version>
</dependency>

如果是Spring Boot项目中你无需设置具体的版本,Spring Boot已经自动适配了对应的版本。

异步GET请求

public static void invoke() {


  try (CloseableHttpAsyncClient client = HttpAsyncClients.createDefault()) {
    client.start() ;
    final SimpleHttpRequest request = SimpleRequestBuilder.get()
        .setUri(URLConstants.LIST)
        .build() ;


    Future<SimpleHttpResponse> future = client.execute(request, new FutureCallback<SimpleHttpResponse>() {
      public void completed(SimpleHttpResponse result) {
        String response = new String(result.getBodyBytes(), StandardCharsets.UTF_8) ;
        System.out.printf("result: %s%n", response) ;
      }
      public void failed(Exception ex) {
        System.out.printf("error: %s%n", ex) ;
      }
      public void cancelled() {
      }
    }) ;


    HttpResponse response = future.get() ;
    System.out.printf("code: %s, reason: %s%n", response.getCode(), response.getReasonPhrase()) ;
  }
}

请求结果

code: 200, reason: OK
result: [{"id":1,"name":"狗蛋"},{"id":2,"name":"観月あかね"},{"id":666,"name":"莉莉"}]

在这里,我们通过在一个扩展的 try 块中使用默认参数实例化 CloseableHttpAsyncClient 来创建客户端。之后,我们启动客户端。

接下来,我们使用 SimpleHttpRequest 创建请求,并通过调用 execute() 方法进行异步调用,同时附加一个 FutureCallback 类来捕获和处理 HTTP 响应。

同步POST请求

public static void invokePost() throws Exception {
  StringEntity stringEntity = new StringEntity(prepareRequest()) ;
  HttpPost httpPost = new HttpPost(URLConstants.SAVE) ;


  httpPost.setEntity(stringEntity) ;
  httpPost.setHeader("Accept", "application/json") ;
  httpPost.setHeader("Content-type", "application/json") ;


  try(CloseableHttpClient httpClient = HttpClients.createDefault()) {
    String result = httpClient.execute(httpPost, new HttpClientResponseHandler<String>() {
      public String handleResponse(ClassicHttpResponse response) throws HttpException, IOException {
        System.out.printf("code: %s, reason: %s%n", response.getCode(), response.getReasonPhrase()) ;
        return EntityUtils.toString(response.getEntity()) ;
      }
    }) ;
    System.out.printf("result: %s%n", result) ;
  } catch (Exception e) {
     e.printStackTrace() ;
  } 
}
private static String prepareRequest() throws Exception {
  var objectMapper = new ObjectMapper();
  String requestBody = objectMapper.writeValueAsString(new User(666L, "Heyzo"));
  return requestBody;
}

请求结果

code: 200, reason: 
result: {"id":666,"name":"Heyzo"}

在这里,我们在 prepareRequest 方法中创建了一个 JSON 字符串,用于以 HTTP POST 方法发送请求正文。

接下来,我们用 StringEntity 类封装 JSON 字符串,并将其设置在 HttpPost 类中,从而创建请求。

我们通过调用 CloseableHttpClient 类上的 execute() 方法对应用程序接口进行同步调用,该方法将使用 StringEntity 实例填充的 HttpPost 对象作为输入参数。

2.4 OKHttpClient

OkHttpClient 是一个开源库,最初由 Square 于 2013 年发布。

引入依赖

<dependency>
  <groupId>com.squareup.okhttp3</groupId>
  <artifactId>okhttp</artifactId>
  <version>4.12.0</version>
</dependency>

目前最新的正式版本是4.12.0(5.x目前是alpha版本)。

异步GET请求

public static void invoke() throws Exception {
  OkHttpClient client = new OkHttpClient.Builder()
      .readTimeout(1000, TimeUnit.MILLISECONDS)
      .writeTimeout(1000, TimeUnit.MILLISECONDS)
      .build() ;


  Request request = new Request.Builder().url(URLConstants.LIST).get().build() ;


  Call call = client.newCall(request) ;
  call.enqueue(new Callback() {
    public void onResponse(Call call, Response response) throws IOException {
      System.out.printf("result: %s%n", response.body().string()) ;
    }
    public void onFailure(Call call, IOException e) {
    }
  }) ;
}

在这里,我们使用构建器模式来设置读写操作的超时值,从而定制客户端。

接下来,我们使用 Request.Builder 创建请求已经配置响应测试。然后,我们在客户端上进行异步 HTTP 调用,并通过附加回调处理程序接收响应。

通过POST请求

public static void invokePost() throws Exception {
  OkHttpClient client = new OkHttpClient.Builder()
      .readTimeout(1000, TimeUnit.MILLISECONDS)
      .writeTimeout(1000, TimeUnit.MILLISECONDS)
      .build() ;
  // 1.准备发送的请求数据
  String requestBody = prepareRequest() ;
  // 2.创建 Request Body
  RequestBody body = RequestBody.create(requestBody, MediaType.parse("application/json")) ;
  // 3.创建HTTP请求
  Request request = new Request.Builder().url(URLConstants.SAVE).post(body).build() ;


  // 4.同步调用发送请求
  Response response = client.newCall(request).execute() ;
  System.out.printf("result: %s%n", response.body().string()) ;
}

请求结果

result: {"id":666,"name":"Heyzo"}

在这里,通过 prepareRequest() 方法中创建了一个 JSON 字符串,用于以 HTTP POST 方法发送请求正文。

接下来,使用 Request.Builder 创建请求。

然后,在通过 OkHttpClient#newCall() 方法对 API 进行同步调用。

当我们创建一个单一的 OkHttpClient 实例并在应用程序中的所有 HTTP 调用中重复使用它时,OkHttp 的性能最佳。安卓应用程序中常用的 HTTP 客户端(如 Retrofit 和 Picasso)都使用 OkHttp。

2.5 Spring WebClient

Spring WebClient 是 Spring 5 在 Spring WebFlux 项目中引入的异步、反应式 HTTP 客户端,用于取代旧版 RestTemplate,在使用 Spring Boot 框架构建的应用程序中进行 REST API 调用。它支持同步、异步和流场景。

引入依赖

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

说明:如果你项目中也引入了starter-web模块,那么创业的应该还是基于servlet技术栈。我们这里引入webflux就是单纯的使用WebClient。

异步GET请求

public static void invoke() {
  WebClient client = WebClient.create() ;
  client.get()
    .uri(URLConstants.LIST)
    .retrieve()
    .bodyToMono(String.class)
    .subscribe(System.err::println) ;
}

请求结果

[{"id":1,"name":"狗蛋"},{"id":2,"name":"観月あかね"},{"id":666,"name":"莉莉"}]

首先使用默认设置创建客户端WebClient。然后,调用客户端上的 get() 方法来发送 HTTP GET 请求,调用 uri 来设置 API访问接口。

通过POST请求

public static void invokePost() throws Exception {
  WebClient client = WebClient.create();
  String result = client
      .post()
      .uri(URLConstants.SAVE)
      .contentType(MediaType.APPLICATION_JSON)
      .body(BodyInserters.fromValue(prepareRequest()))
      .retrieve()
      .bodyToMono(String.class)
      .block() ;
  System.out.printf("result: %s%n", result) ;
}

请求结果

result: {"id":666,"name":"Heyzo"}

在这里,我们在 prepareRequest() 方法中创建了一个 JSON 字符串,然后通过 HTTP POST 方法将该字符串作为请求体发送。

然后,通过retrieve()方法获取响应结果。

最后我们通过block()阻塞订阅当前的Mono。

3. 如何选择

总结如下几点:

  • 如果你不想添加任何外部库,对于Java 11及以上的应用程序,Java原生的HttpClient是首选。
  • 对于Spring Boot应用程序,特别是当我们使用响应式API时,Spring WebClient是更推荐的选择。
  • 当我们需要对HTTP客户端进行最大程度的自定义和配置灵活性时,可以使用Apache HttpClient。由于其在社区中的广泛使用,与其他库相比,它在网上有最丰富的文档资料。
  • 当我们使用外部客户端库时,推荐使用Square的OkHttpClient。正如我们在前面的例子中所见,它功能丰富、高度可配置,并且拥有比其他库更容易使用的API。
责任编辑:武晓燕 来源: Spring全家桶实战案例源码
相关推荐

2010-05-31 10:11:32

瘦客户端

2009-11-07 14:57:00

2021-05-21 10:48:09

http语言开发

2009-11-09 15:49:01

WCF异步调用

2010-02-23 09:58:21

WCF客户端验证

2021-10-18 05:00:38

语言GoRequestHTTP

2020-03-24 15:15:29

HttpClientOkHttpJava

2011-08-03 10:39:06

IOS程序 HTTP 请求

2024-07-31 15:57:41

2024-10-15 10:28:43

2021-04-09 10:05:20

OneDrive微软同步客户端

2020-01-02 16:30:02

Spring BootJava异步请求

2024-09-05 09:41:57

2023-09-19 22:41:30

控制器HTTP

2021-09-22 15:46:29

虚拟桌面瘦客户端胖客户端

2011-08-17 10:10:59

2011-07-21 11:30:19

网关选择产品

2024-05-31 14:04:18

2009-12-22 18:43:00

WCF异步调用

2009-08-21 14:33:15

C#异步传输字符串
点赞
收藏

51CTO技术栈公众号