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