直擊痛點的一款 HTTP 客戶端框架!
大家好,我是二哥呀!
今天來給大家推薦一款直擊痛點的 HTTP 客戶端框架,可以超高效率地完成和第三方接口的對接。
在介紹本篇的主角之前,我們先來了解下 Java 生態(tài)中的 HTTP 組件庫,大致可以分為三類:
JDK 自帶的 HttpURLConnection 標(biāo)準(zhǔn)庫; Apache HttpComponents HttpClient; OkHttp。
使用 HttpURLConnection 發(fā)起 HTTP 請求最大的優(yōu)點是不需要引入額外的依賴,但是使用起來非常繁瑣,也缺乏連接池管理、域名機械控制等特性支持。
使用標(biāo)準(zhǔn)庫的最大好處就是不需要引入額外的依賴,但使用起來比較繁瑣,就像直接使用 JDBC 連接數(shù)據(jù)庫那樣,需要很多模板代碼。來發(fā)起一個簡單的 HTTP POST 請求吧。
public class HttpUrlConnectionDemo {
public static void main(String[] args) throws IOException {
String urlString = "https://httpbin.org/post";
String bodyString = "password=123";
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
OutputStream os = conn.getOutputStream();
os.write(bodyString.getBytes("utf-8"));
os.flush();
os.close();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
InputStream is = conn.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
System.out.println("響應(yīng)內(nèi)容:" + sb.toString());
} else {
System.out.println("響應(yīng)碼:" + conn.getResponseCode());
}
}
}
HttpURLConnection 發(fā)起的 HTTP 請求比較原始,基本上算是對網(wǎng)絡(luò)傳輸層的一次淺層次的封裝;有了 HttpURLConnection 對象后,就可以獲取到輸出流,然后把要發(fā)送的內(nèi)容發(fā)送出去;再通過輸入流讀取到服務(wù)器端響應(yīng)的內(nèi)容;最后打印。
不過 HttpURLConnection 不支持 HTTP/2.0,為了解決這個問題,Java 9 的時候官方的標(biāo)準(zhǔn)庫增加了一個更高級別的 HttpClient,再發(fā)起 POST 請求就顯得高大上多了,不僅支持異步,還支持順滑的鏈?zhǔn)秸{(diào)用。
public class HttpClientDemo {
public static void main(String[] args) throws URISyntaxException {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/post"))
.headers("Content-Type", "text/plain;charset=UTF-8")
.POST(HttpRequest.BodyPublishers.ofString("二哥牛逼"))
.build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.join();
}
}
Apache HttpComponents HttpClient 支持的特性也非常豐富:
基于標(biāo)準(zhǔn)、純凈的Java語言,實現(xiàn)了HTTP1.0和HTTP1.1; 以可擴展的面向?qū)ο蟮慕Y(jié)構(gòu)實現(xiàn)了HTTP全部的方法; 支持加密的HTTPS協(xié)議(HTTP通過SSL協(xié)議); Request的輸出流可以避免流中內(nèi)容體直接從socket緩沖到服務(wù)器; Response的輸入流可以有效的從socket服務(wù)器直接讀取相應(yīng)內(nèi)容。
public class HttpComponentsDemo {
public static void main(String[] args) throws IOException, IOException, ParseException {
try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
HttpPost httpPost = new HttpPost("http://httpbin.org/post");
List<NameValuePair> nvps = new ArrayList<>();
nvps.add(new BasicNameValuePair("name", "二哥"));
httpPost.setEntity(new UrlEncodedFormEntity(nvps, Charset.forName("UTF-8")));
try (CloseableHttpResponse response2 = httpclient.execute(httpPost)) {
System.out.println(response2.getCode() + " " + EntityUtils.toString(response2.getEntity()));
}
}
}
}
OkHttp 是一個執(zhí)行效率比較高的 HTTP 客戶端:
支持 HTTP/2.0,當(dāng)多個請求對應(yīng)同一個 Host 地址時,可共用同一個 Socket; 連接池可減少請求延遲; 支持 GZIP 壓縮,減少網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)大小; 支持 Response 數(shù)據(jù)緩存,避免重復(fù)網(wǎng)絡(luò)請求;
public class OkHttpPostDemo {
public static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
OkHttpClient client = new OkHttpClient();
String post(String url, String json) throws IOException {
RequestBody body = RequestBody.create(json, JSON);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
}
public static void main(String[] args) throws IOException {
OkHttpPostDemo example = new OkHttpPostDemo();
String json = "{'name':'二哥'}";
String response = example.post("https://httpbin.org/post", json);
System.out.println(response);
}
}
那今天介紹的這款輕量級的 HTTP 客戶端框架 Forest,正是基于 Httpclient和OkHttp 的,屏蔽了不同細節(jié)的 HTTP 組件庫所帶來的所有差異。
Forest 的字面意思是森林的意思,更內(nèi)涵點的話,可以拆成For和Rest兩個單詞,也就是“為了Rest”(Rest為一種基于HTTP的架構(gòu)風(fēng)格)。而合起來就是森林,森林由很多樹木花草組成(可以理解為各種不同的服務(wù)),它們表面上看獨立,實則在地下根莖交錯縱橫、相互連接依存,這樣看就有點現(xiàn)代分布式服務(wù)化的味道了。最后,這兩個單詞反過來讀就像是Resultful。
項目地址:
https://gitee.com/dromara/forest
雖然 star 數(shù)還不是很多,但 star 趨勢圖正在趨于爬坡階段,大家可以拿來作為一個練手項目,我覺得還是不錯的選擇。

Forest 本身是處理前端過程的框架,是對后端 HTTP API 框架的進一步封裝。

前端部分:
通過RPC方式去發(fā)送HTTP請求, 方便解耦 支持GET, HEAD, POST等所有請求方法 支持Spring和Springboot集成 JSON字符串到Java對象的自動化解析 XML文本到Java對象的自動化解析 支持靈活的模板表達式 支持攔截器處理請求的各個生命周期 支持自定義注解
后端部分:
支持OkHttp 支持Httpclient
Forest 容易上手,不需要調(diào)用HTTP底層接口,而是像 Dubbo 那樣的 RPC 框架一樣,只需要定義接口、調(diào)用接口即可。幾分鐘內(nèi)就可完成請求的定義、發(fā)送、接收響應(yīng)、數(shù)據(jù)解析、錯誤處理、日志打印等過程。
配置輕量,遵循約定優(yōu)于配置的原則,只需在需要的時候進行配置,不配置也不會影響Forest請求的正常調(diào)用。
簡單優(yōu)雅,將 HTTP 請求細節(jié)封裝成 Java 接口 + 注解的形式,不必再關(guān)心發(fā)送 HTTP 請求的具體過程。使得 HTTP 請求信息與業(yè)務(wù)代碼解耦,方便管理大量 HTTP 的 URL、Header、Body 等信息。
擴展靈活,允許自定義攔截器、甚至是自定義注解,以此來擴展Forest的能力。
Forest 不需要我們編寫具體的 HTTP 調(diào)用過程,只需要定義一個接口,然后通過 Forest 注解將 HTTP 請求的信息添加到接口的方法上即可。請求發(fā)送方通過調(diào)用定義的接口就能自動發(fā)送請求和接受請求的響應(yīng)。
Forest 之所以能做到這樣,是因為它將定義好的接口通過動態(tài)代理的方式生成了一個具體的實現(xiàn)類,然后組織、驗證 HTTP 請求信息,綁定動態(tài)數(shù)據(jù),轉(zhuǎn)換數(shù)據(jù)形式,SSL 驗證簽名,調(diào)用后端 HTTP API執(zhí)行實際請求,等待響應(yīng),失敗重試,轉(zhuǎn)換響應(yīng)數(shù)據(jù)到 Java 類型等臟活累活都由這動態(tài)代理的實現(xiàn)類給包了。
廢話就不再多說,直接開始實戰(zhàn)。
第一步,添加 Maven 依賴。
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-core</artifactId>
<version>1.5.1</version>
</dependency>
第二步,構(gòu)建 HTTP 請求。
在 Forest 中,所有的 HTTP 請求信息都要綁定到某一個接口的方法上,不需要編寫具體的代碼去發(fā)送請求。請求發(fā)送方通過調(diào)用事先定義好 HTTP 請求信息的接口方法。
public interface ForRestClient {
@Request(
url = "http://httpbin.org/post",
type = "POST"
)
String simplePost(@Body("name") String name);
}
通過 @Post 注解,將上面的ForRestClient接口中的 simplePost() 方法綁定了一個 HTTP 請求,使用 POST 方式,可以使用@Body注解修飾參數(shù)的方式,將傳入?yún)?shù)的數(shù)據(jù)綁定到 HTTP 請求體中。然后將請求響應(yīng)的數(shù)據(jù)以String的方式返回給調(diào)用者。
第三步,調(diào)用接口。
public class ForRestDemo {
public static void main(String[] args) {
// 實例化Forest配置對象
ForestConfiguration configuration = ForestConfiguration.configuration();
configuration.setBackendName("httpclient");
// 通過Forest配置對象實例化Forest請求接口
ForRestClient myClient = configuration.createInstance(ForRestClient.class);
// 調(diào)用Forest請求接口,并獲取響應(yīng)返回結(jié)果
String result = myClient.simplePost("二哥");
System.out.println(result);
}
}
ForestConfiguration為 Forest 的全局配置對象類,所有的 Forest 的全局基本配置信息由此類進行管理。
可以來看一下運行后的日志信息:
{
"args": {},
"data": "",
"files": {},
"form": {
"name": "\u4e8c\u54e5"
},
"headers": {
"Content-Length": "23",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org",
"User-Agent": "Apache-HttpClient/4.5.2 (Java/11.0.8)",
"X-Amzn-Trace-Id": "Root=1-60b533aa-58b41e4967803d99593c53a0"
},
"json": null,
"origin": "161.81.21.32",
"url": "http://httpbin.org/post"
}
此時,一個簡單的 Forest 上手小栗子就跑通了。
如果是 Spring Boot 項目的話,就不需要 ForestConfiguration 了,只需要在啟動類或者配置類上添加 @ForestScan 注解就可以了。
@SpringBootApplication
@Configuration
@ForestScan(basePackages = "com.yoursite.client")
public class MyApp {
...
}
Forest 除了支持GET和POST,也支持其他幾種 HTTP 請求方式,比如PUT、HEAD、 OPTIONS、DELETE。只需要在構(gòu)建接口的時候使用對應(yīng)的注解就可以了,比如說 PUT:
// PUT請求
@Put("http://localhost:8080/hello")
String simplePut();
在POST和PUT請求方法中,通常使用 HTTP 請求體進行數(shù)據(jù)傳輸,在 Forest 中,可以使用 @Body、@JSONBody、@XMLBody 等多種方式設(shè)置請求體數(shù)據(jù)。
/**
* 直接修飾一個JSON字符串
*/
@Post("http://localhost:8080/hello/user")
String helloUser(@JSONBody String userJson);
Forest 請求會自動將響應(yīng)的返回數(shù)據(jù)反序列化成對應(yīng)的數(shù)據(jù)類型,分兩步走。
第一步:定義dataType屬性
dataType屬性指定了該請求響應(yīng)返回的數(shù)據(jù)類型,可選的數(shù)據(jù)類型有三種: text, json, xml,默認為 text。
/**
* dataType為json或xml時,F(xiàn)orest會進行相應(yīng)的反序列化
*/
@Request(
url = "http://localhost:8080/text/data",
dataType = "json"
)
Map getData();
第二步:指定反序列化的目標(biāo)類型
反序列化需要一個目標(biāo)類型,而該類型其實就是方法的返回值類型,如返回值為String就會反序列成String字符串,返回值為Map就會反序列化成一個HashMap對象,也可以指定為自定義的Class類型。
如果有這樣一個 User 類:
public class User {
private String username;
private String score;
// Setter和Getter ...
}
返回的數(shù)據(jù)為 JSON 字符串:
{"username": "Foo", "score": "82"}
那請求接口就應(yīng)該定義成這樣:
/**
* dataType屬性指明了返回的數(shù)據(jù)類型為JSON
*/
@Get(
url = "http://localhost:8080/user?id=${0}",
dataType = "json"
)
User getUser(Integer id)
另外,大家需要了解一下 Gzip,它是現(xiàn)在一種流行的文件壓縮算法,有相當(dāng)廣泛的應(yīng)用范圍。尤其是當(dāng)Gzip用來壓縮存文本文件的時候效果尤為明顯,大概能減少70%以上的文件大小。很多 HTTP 服務(wù)器都支持 Gzip,比如 Tomcat,經(jīng)過這些服務(wù)壓縮過的數(shù)據(jù)可以降低網(wǎng)絡(luò)傳輸?shù)牧髁浚岣呖蛻舳说捻憫?yīng)速度。
Forest從1.5.2-BETA版本開始支持Gzip的解壓,其解壓的方式也很簡單,在方法或接口類上加上 @DecompressGzip 注解即可。
/**
* 為請求方法添加Gzip解壓能力
*/
@Get("/transaction")
@DecompressGzip
String transaction(String infno);
更重要的一點是,F(xiàn)orest 可以通過設(shè)置@Request注解的async屬性為true來實現(xiàn)異步請求。
@Request(
url = "http://localhost:8080/hello/user?username=${0}",
async = true,
headers = {"Accept:text/plain"}
)
void asyncGet(String username, OnSuccess<String> onSuccess);
異步請求時,通過 OnSuccess<T> 回調(diào)函數(shù)來接受響應(yīng)數(shù)據(jù),而不是通過接口方法的返回值,所以這里的返回值類型一般會定義為void。
調(diào)用該接口方法時,可以通過下面的方式:
myClient.send("foo", (String resText, ForestRequest request, ForestResponse response) -> {
// 成功響應(yīng)回調(diào)
System.out.println(resText);
},
(ForestRuntimeException ex, ForestRequest request, ForestResponse response) -> {
// 異常回調(diào)
System.out.println(ex.getMessage());
});
除了上面提到的這些功能,F(xiàn)orset 還支持更高級的用法:
HTTPS 文件上傳下載 攔截器 使用代理 自定義注解
大家可以去看一下 Forset 的官方文檔,然后在本地實踐一下,還是能學(xué)到不少知識的,尤其是 HTTPS 和文件上傳下載這塊,只需要簡單的配置就能完成,我個人感覺還是挺值得去學(xué)習(xí)和借鑒的。
開源精神難能可貴,好的開源需要大家的添磚加瓦和支持。希望這篇文章能給大家在選擇 HTTP 客戶端框架時帶來一個新的選擇,對,就是 Forest。
這篇文章不僅介紹了 Forest 這個輕量級的 HTTP 客戶端框架,還回顧了它的底層實現(xiàn):HttpClient 和 OkHttp,希望能對大家有所幫助。
PS:今天是六一兒童節(jié),也是六月份的第一天,記得給自己買個小神童吃哈~~~~
我是二哥呀,我們下期見~
