Spring Boot 不同HTTP客戶端 同步&異步請求對比
環(huán)境:Spring Boot3.2.5
1. 簡介
超文本傳輸協(xié)議(HTTP)是一種用于傳輸超媒體文檔(如HTML)以及標(biāo)準(zhǔn)格式(如JSON和XML)的API數(shù)據(jù)的應(yīng)用層協(xié)議。
它是應(yīng)用程序之間通信時常用的協(xié)議,這些應(yīng)用程序以REST API的形式發(fā)布其功能。使用Java構(gòu)建的應(yīng)用程序依賴某種形式的HTTP客戶端來對其他應(yīng)用程序進(jìn)行API調(diào)用。
在選擇HTTP客戶端方面存在多種多樣的選項(xiàng)。本文概述了一些主要的庫,這些庫被用作Java應(yīng)用程序中的HTTP客戶端來進(jìn)行HTTP請求。
本篇文章將介紹如下幾種HTTP 客戶端:
- Java 11及以上版本編寫的應(yīng)用程序內(nèi)置了HttpClient
- Apache HttpComponents項(xiàng)目的Apache HttpClient
- 由Square提供的OkHttpClient
- Spring WebFlux中的WebClient
為了覆蓋最常見的場景,我們將查看每種類型的客戶端發(fā)送異步HTTP GET請求和同步POST請求的示例。
2. 實(shí)戰(zhàn)案例
2.1 準(zhǔn)備接口
@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 ;
}
}
如上準(zhǔn)備了2個接口分別是:GET請求的/list,POST請求的/save。接下來介紹的4個HTTP客戶端都將圍繞這2個接口進(jìn)行。
2.2 Java HttpClient
原生的HttpClient作為孵化器模塊在Java 9中引入,并在Java 11中作為JEP 321的一部分正式可用。HttpClient替換了自早期Java版本以來JDK中存在的舊版HttpUrlConnection類。它包括以下特性:
- 支持HTTP/1.1、HTTP/2和WebSocket
- 支持同步和異步編程模型
- 以響應(yīng)式流的方式處理請求和響應(yīng)體
- 支持Cookies
異步GET請求
public static void invoke() throws Exception {
// 構(gòu)建客戶端
HttpClient client = HttpClient.newBuilder()
.version(Version.HTTP_2)
.followRedirects(Redirect.NORMAL)
.build() ;
// 構(gòu)造請求對象
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(URLConstants.LIST))
.GET()
// 設(shè)置超時時間
.timeout(Duration.ofSeconds(5))
.build() ;
// 發(fā)送異步請求
client.sendAsync(request, BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
// 等待請求完成
.join() ;
}
請求結(jié)果
[{"id":1,"name":"狗蛋"},{"id":2,"name":"観月あかね"}]
通過POST請求
對于 HTTP POST 和 PUT,我們會在生成器上調(diào)用 POST(BodyPublisher body) 和 PUT(BodyPublisher body) 方法。BodyPublisher 參數(shù)有幾種開箱即用的實(shí)現(xiàn)方式,可以簡化請求正文的發(fā)送,如下示例:
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))
// 這里必須設(shè)置該header,否則會響應(yīng)415狀態(tài)碼錯誤
.header(HttpHeaders.CONTENT_TYPE, "application/json")
.header(HttpHeaders.ACCEPT, "application/json")
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.err.printf("響應(yīng)結(jié)果: %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() 方法中創(chuàng)建了一個 JSON 字符串,用于通過 HTTP POST() 方法發(fā)送請求正文。
接下來,我們將使用構(gòu)建器模式創(chuàng)建一個 HttpRequest 實(shí)例,然后同步調(diào)用 REST API。
在創(chuàng)建請求時,我們通過調(diào)用 POST() 方法將 HTTP 方法設(shè)置為 POST,還通過在 BodyPublisher 實(shí)例中封裝 JSON 字符串來設(shè)置 API URL 和請求正文。
響應(yīng)是通過使用 BodyHandler 實(shí)例從 HTTP 響應(yīng)中提取的。
2.3 Apache HttpComponents
HttpComponents 是 Apache 軟件基金會下的一個項(xiàng)目,它包含了一組用于處理 HTTP 的低級 Java 組件。該項(xiàng)目下的組件分為:
- HttpCore:一組低級的 HTTP 傳輸組件,可以用來構(gòu)建自定義的客戶端和服務(wù)器端 HTTP 服務(wù)。
- HttpClient:基于 HttpCore 的一個符合 HTTP 規(guī)范的 HTTP 代理實(shí)現(xiàn)。它還提供了可重用的組件,用于客戶端身份驗(yàn)證、HTTP 狀態(tài)管理和 HTTP 連接管理。
引入依賴
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.4</version>
</dependency>
如果是Spring Boot項(xiàng)目中你無需設(shè)置具體的版本,Spring Boot已經(jīng)自動適配了對應(yīng)的版本。
異步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()) ;
}
}
請求結(jié)果
code: 200, reason: OK
result: [{"id":1,"name":"狗蛋"},{"id":2,"name":"観月あかね"},{"id":666,"name":"莉莉"}]
在這里,我們通過在一個擴(kuò)展的 try 塊中使用默認(rèn)參數(shù)實(shí)例化 CloseableHttpAsyncClient 來創(chuàng)建客戶端。之后,我們啟動客戶端。
接下來,我們使用 SimpleHttpRequest 創(chuàng)建請求,并通過調(diào)用 execute() 方法進(jìn)行異步調(diào)用,同時附加一個 FutureCallback 類來捕獲和處理 HTTP 響應(yīng)。
同步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;
}
請求結(jié)果
code: 200, reason:
result: {"id":666,"name":"Heyzo"}
在這里,我們在 prepareRequest 方法中創(chuàng)建了一個 JSON 字符串,用于以 HTTP POST 方法發(fā)送請求正文。
接下來,我們用 StringEntity 類封裝 JSON 字符串,并將其設(shè)置在 HttpPost 類中,從而創(chuàng)建請求。
我們通過調(diào)用 CloseableHttpClient 類上的 execute() 方法對應(yīng)用程序接口進(jìn)行同步調(diào)用,該方法將使用 StringEntity 實(shí)例填充的 HttpPost 對象作為輸入?yún)?shù)。
2.4 OKHttpClient
OkHttpClient 是一個開源庫,最初由 Square 于 2013 年發(fā)布。
引入依賴
<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) {
}
}) ;
}
在這里,我們使用構(gòu)建器模式來設(shè)置讀寫操作的超時值,從而定制客戶端。
接下來,我們使用 Request.Builder 創(chuàng)建請求已經(jīng)配置響應(yīng)測試。然后,我們在客戶端上進(jìn)行異步 HTTP 調(diào)用,并通過附加回調(diào)處理程序接收響應(yīng)。
通過POST請求
public static void invokePost() throws Exception {
OkHttpClient client = new OkHttpClient.Builder()
.readTimeout(1000, TimeUnit.MILLISECONDS)
.writeTimeout(1000, TimeUnit.MILLISECONDS)
.build() ;
// 1.準(zhǔn)備發(fā)送的請求數(shù)據(jù)
String requestBody = prepareRequest() ;
// 2.創(chuàng)建 Request Body
RequestBody body = RequestBody.create(requestBody, MediaType.parse("application/json")) ;
// 3.創(chuàng)建HTTP請求
Request request = new Request.Builder().url(URLConstants.SAVE).post(body).build() ;
// 4.同步調(diào)用發(fā)送請求
Response response = client.newCall(request).execute() ;
System.out.printf("result: %s%n", response.body().string()) ;
}
請求結(jié)果
result: {"id":666,"name":"Heyzo"}
在這里,通過 prepareRequest() 方法中創(chuàng)建了一個 JSON 字符串,用于以 HTTP POST 方法發(fā)送請求正文。
接下來,使用 Request.Builder 創(chuàng)建請求。
然后,在通過 OkHttpClient#newCall() 方法對 API 進(jìn)行同步調(diào)用。
當(dāng)我們創(chuàng)建一個單一的 OkHttpClient 實(shí)例并在應(yīng)用程序中的所有 HTTP 調(diào)用中重復(fù)使用它時,OkHttp 的性能最佳。安卓應(yīng)用程序中常用的 HTTP 客戶端(如 Retrofit 和 Picasso)都使用 OkHttp。
2.5 Spring WebClient
Spring WebClient 是 Spring 5 在 Spring WebFlux 項(xiàng)目中引入的異步、反應(yīng)式 HTTP 客戶端,用于取代舊版 RestTemplate,在使用 Spring Boot 框架構(gòu)建的應(yīng)用程序中進(jìn)行 REST API 調(diào)用。它支持同步、異步和流場景。
引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
說明:如果你項(xiàng)目中也引入了starter-web模塊,那么創(chuàng)業(yè)的應(yīng)該還是基于servlet技術(shù)棧。我們這里引入webflux就是單純的使用WebClient。
異步GET請求
public static void invoke() {
WebClient client = WebClient.create() ;
client.get()
.uri(URLConstants.LIST)
.retrieve()
.bodyToMono(String.class)
.subscribe(System.err::println) ;
}
請求結(jié)果
[{"id":1,"name":"狗蛋"},{"id":2,"name":"観月あかね"},{"id":666,"name":"莉莉"}]
首先使用默認(rèn)設(shè)置創(chuàng)建客戶端WebClient。然后,調(diào)用客戶端上的 get() 方法來發(fā)送 HTTP GET 請求,調(diào)用 uri 來設(shè)置 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) ;
}
請求結(jié)果
result: {"id":666,"name":"Heyzo"}
在這里,我們在 prepareRequest() 方法中創(chuàng)建了一個 JSON 字符串,然后通過 HTTP POST 方法將該字符串作為請求體發(fā)送。
然后,通過retrieve()方法獲取響應(yīng)結(jié)果。
最后我們通過block()阻塞訂閱當(dāng)前的Mono。
3. 如何選擇
總結(jié)如下幾點(diǎn):
- 如果你不想添加任何外部庫,對于Java 11及以上的應(yīng)用程序,Java原生的HttpClient是首選。
- 對于Spring Boot應(yīng)用程序,特別是當(dāng)我們使用響應(yīng)式API時,Spring WebClient是更推薦的選擇。
- 當(dāng)我們需要對HTTP客戶端進(jìn)行最大程度的自定義和配置靈活性時,可以使用Apache HttpClient。由于其在社區(qū)中的廣泛使用,與其他庫相比,它在網(wǎng)上有最豐富的文檔資料。
- 當(dāng)我們使用外部客戶端庫時,推薦使用Square的OkHttpClient。正如我們在前面的例子中所見,它功能豐富、高度可配置,并且擁有比其他庫更容易使用的API。