Spring Boot 不同HTTP客戶端 同步&異步請求對比
環境:Spring Boot3.2.5
1. 簡介
超文本傳輸協議(HTTP)是一種用于傳輸超媒體文檔(如HTML)以及標準格式(如JSON和XML)的API數據的應用層協議。
它是應用程序之間通信時常用的協議,這些應用程序以REST API的形式發布其功能。使用Java構建的應用程序依賴某種形式的HTTP客戶端來對其他應用程序進行API調用。
在選擇HTTP客戶端方面存在多種多樣的選項。本文概述了一些主要的庫,這些庫被用作Java應用程序中的HTTP客戶端來進行HTTP請求。
本篇文章將介紹如下幾種HTTP 客戶端:
- Java 11及以上版本編寫的應用程序內置了HttpClient
- Apache HttpComponents項目的Apache HttpClient
- 由Square提供的OkHttpClient
- 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類。它包括以下特性:
- 支持HTTP/1.1、HTTP/2和WebSocket
- 支持同步和異步編程模型
- 以響應式流的方式處理請求和響應體
- 支持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。