輕量級HTTP用戶端架構—Forest學習筆記
一、Forest
1.1 業務需求
一般情況下是後端提供接口,前端調用,解決需求,但是有的時候為了友善,複用别人的接口(網上的,公共的第三方接口(短信、天氣等)),就出現了後端調用後端接口的情況。
此外,因為業務關系,要和許多不同第三方公司進行對接。這些服務商都提供基于http的api,但是每家公司提供api具體細節差别很大。有的基于RESTFUL規範,有的基于傳統的http規範;有的需要在header裡放置簽名,有的需要SSL的雙向認證,有的隻需要SSL的單向認證;有的以JSON方式進行序列化,有的以XML方式進行序列化······類似于這樣細節的差别較多。
不涉及業務的公共http調用套件 ???
1.2 Forest簡介
Forest 是一個開源的 Java HTTP 用戶端架構,它能夠将 HTTP 的所有請求資訊(包括 URL、Header 以及 Body 等資訊)綁定到自定義的 Interface 方法上,能夠通過調用本地接口方法的方式發送 HTTP 請求。使用 Forest 就像使用類似 Dubbo 那樣的 RPC 架構一樣,隻需要定義接口,調用接口即可,不必關心具體發送 HTTP 請求的細節。同時将 HTTP 請求資訊與業務代碼解耦,友善統一管理大量 HTTP 的 URL、Header 等資訊。而請求的調用方完全不必在意 HTTP 的具體内容,即使該 HTTP 請求資訊發生變更,大多數情況也不需要修改調用發送請求的代碼。
1.2.1 Forest特性
- 以Httpclient和OkHttp為後端架構
- 通過調用本地方法的方式去發送Http請求,實作了業務邏輯與Http協定之間的解耦
- 因為針對第三方接口,是以不需要依賴Spring Cloud和任何注冊中心
- 支援所有請求方法:GET,HEAD,OPTIONS,TRACE,POST,DELETE,PUT,PATCH
- 支援檔案上傳和下載下傳
- 支援靈活的模闆表達式
- 支援攔截器處理請求的各個生命周期
- 支援自定義注解
- 支援OAuth2驗證
- 支援過濾器來過濾傳入的資料
- 基于注解、配置化的方式定義Http請求
- 支援Spring和Springboot內建
- JSON字元串到Java對象的自動化解析
- XML文本到Java對象的自動化解析
- JSON、XML或其他類型轉換器可以随意擴充和替換
- 支援JSON轉換架構:Fastjson,Jackson,Gson
- 支援JAXB形式的XML轉換
- 可以通過OnSuccess和OnError接口參數實作請求結果的回調
- 配置簡單,一般隻需要@Request一個注解就能完成絕大多數請求的定義
- 支援異步請求調用
- 約定大于配置
- 自定義攔截器、自定義注解,擴充Forest的能力
1.2.2 Forest工作原理
Forest會将定義好的接口通過動态代理的方式生成一個具體的實作類,然後組織、驗證 HTTP 請求資訊,綁定動态資料,轉換資料形式,SSL 驗證簽名,調用後端 HTTP API(httpclient 等 API)執行實際請求,等待響應,失敗重試,轉換響應資料到 Java 類型等髒活累活都由這動态代理的實作類給包了。請求發送方調用這個接口時,實際上就是在調用這個幹髒活累活的實作類。
1.2.3 Forest架構
HTTP 發送請求的過程分為前端部分和後端部分,Forest 本身是處理前端過程的架構,是對後端 HTTP API 架構的進一步封裝。
前端部分:
(1)Forest 配置: 負責管理 HTTP 發送請求所需的配置。
(2)Forest 注解: 用于定義 HTTP 發送請求的所有相關資訊,一般定義在 interface 上和其方法上。
(3)動态代理: 使用者定義好的 HTTP 請求的
interface
将通過動态代理産生實際執行發送請求過程的代理類。
(4)模闆表達式: 模闆表達式可以嵌入在幾乎所有的 HTTP 請求參數定義中,它能夠将使用者通過參數或全局變量傳入的資料動态綁定到 HTTP 請求資訊中。
(5)資料轉換: 此子產品将字元串資料和
JSON
或
XML
形式資料進行互轉。目前 JSON 轉換器支援
Jackson
、
Fastjson
、
Gson
三種,XML 支援
JAXB
一種。
(6)攔截器: 使用者可以自定義攔截器,攔截指定的一個或一批請求的開始、成功傳回資料、失敗、完成等生命周期中的各個環節,以插入自定義的邏輯進行處理。
(7)過濾器: 用于動态過濾和處理傳入 HTTP 請求的相關資料。
(8)SSL: Forest 支援單向和雙向驗證的 HTTPS 請求,此子產品用于處理 SSL 相關協定的内容
後端部分:
後端為實際執行 HTTP 請求發送過程的第三方 HTTP API,目前支援
okHttp3
和
httpclient
兩種後端 API
Spring Boot Starter Forest:提供對
Spring Boot
的支援
二、HttpClient
HTTP 協定可能是現在 Internet 上使用得最多、最重要的協定了,越來越多的 Java 應用程式需要直接通過 HTTP 協定來通路網絡資源。雖然JDK 的 java net包中已經提供了通路 HTTP 協定的基本功能,但是對于大部分應用程式來說,JDK 庫本身提供的功能還不夠豐富和靈活。HttpClient用來提供高效的、最新的、功能豐富的支援 HTTP 協定的用戶端程式設計工具包,并且它支援 HTTP 協定的最新版本。
最初,HttpClient 是Apache Jakarta Common 下的子項目,可以用來提供高效的、最新的、功能豐富的支援 HTTP 協定的用戶端程式設計工具包,并且它支援 HTTP 協定最新的版本和建議。
如今,Apache Jakarta Commons HttpClient項目已經壽終正寝,不再開發和維護。取而代之的是Apache Httpcomponents項目,它包括HttpClient和HttpCore兩大子產品,能提供更好的性能和更大的靈活性。
2.1 主要功能
HttpClient 提供的主要的功能:
- 實作了所有HTTP的方法(GET、POST、PUT、HEAD等)
- 支援自動轉向
- 支援 HTTPS 協定
- 支援代理伺服器
- ······
2.2 使用方法
使用HttpClient發送請求和接收響應一般分為以下幾步:
(1)建立HttpClient對象;
(2)建立請求方法的執行個體,并指定請求URL。如果需要發送GET請求,建立HttpGet對象;如果需要發送POST請求,建立HttpPost對象;
(3)如果需要發送請求參數,可調用HttpGet、HttpPost共同的setParams(HetpParams params)方法來添加請求參數;對于HttpPost對象而言,也可調用
setEntity(HttpEntity entity)方法來設定請求參數;
(4)調用HttpClient對象的execute(HttpUriRequest request)發送請求,該方法傳回一個HttpResponse;
(5)調用HttpResponse的getAllHeaders()、getHeaders(String name)等方法可擷取伺服器的響應頭;調用HttpResponse的getEntity()方法可擷取HttpEntity對象,該對象包裝了伺服器的響應内容,程式可通過該對象擷取伺服器的響應内容;
(6)釋放連接配接。無論執行方法是否成功,都必須釋放連接配接。
三、Okhttp
3.1 Okhttp簡介
Okhttp作為目前Android使用最為廣泛的網絡架構之一,是一個高效的HTTP Client,其高效性展現在:
- 支援Spdy、Http1.X、Http2、Quic以及WebSocket
- 連接配接池複用底層TCP(Socket),減少請求延時
- 無縫的支援GZIP減少資料流量
- 緩存響應資料減少重複的網絡請求
- 請求失敗自動重試主機的其他ip,自動重定向
- ······
3.2 Okhttp請求機制
首先來了解下HTTP client、request、response。HTTP client的作用是接受request請求并傳回response資訊。request請求通常包含一個 URL,一個方法 (比如GET/POST),以及一個headers清單,還可能包含一個body(特定内容類型的資料流)。response則通常用響應代碼(比如200表示成功,404表示未找到)、headers和可選的body來響應request請求。
Okhttp的請求機制,可以概括為以下流程:
(1)通過OkhttpClient建立一個Call,發起同步或異步請求;
(2)okhttp通過Dispatcher對所有的RealCall(Call的具體實作類)進行統一管理,并通過execute()及enqueue()方法對同步或異步請求進行處理;
(3)execute()及enqueue()這兩個方法會最終調用RealCall中的getResponseWithInterceptorChain()方法,從攔截器鍊中擷取傳回結果;
(4)攔截器鍊中,依次通過RetryAndFollowUpInterceptor(重定向攔截器)、BridgeInterceptor(橋接攔截器)、CacheInterceptor(緩存攔截器)、
ConnectInterceptor(連接配接攔截器)、CallServerInterceptor(網絡攔截器)對請求依次處理,與伺服器建立連接配接後,擷取傳回資料,再經過上述攔截器依次 處理後,最後将結果傳回給調用方。具體過程如下圖所示:
3.3 具體架構圖
(1)RetryAndFollowUpInterceptor:負責重定向:建構一個StreamAllocation對象,然後調用下一個攔截器擷取結果,從傳回結果中擷取重定向的request,如果重定向的request不為空的話,并且不超過重定向最大次數的話就進行重定向,否則傳回結果。注意:這裡是通過一個while(true)的循環完成下一輪的重定向請求。
-
StreamAllocation為什麼在第一個攔截器中就進行建立?
便于取消請求以及出錯釋放資源。
-
StreamAllocation的作用是什麼?
StreamAllocation負責統籌管理Connection、Stream、Call三個實體類,具體就是為一個Call(Realcall),尋找( findConnection() )一個Connection(RealConnection),擷取一個Stream(HttpCode)。
(2)BridgeInterceptor:負責将原始Requset轉換給發送給服務端的Request以及将Response轉化成對調用方友好的Response。
具體就是對request添加Content-Type、Content-Length、cookie、Connection、Host、Accept-Encoding等請求頭以及對傳回結果進行解壓、保持cookie等。
(3)CacheInterceptor:負責讀取緩存以及更新緩存。
在請求階段:
讀取候選緩存cacheCandidate;
根據originOequest和cacheresponse建立緩存政策CacheStrategy;
根據緩存政策,來決定是否使用網絡或者使用緩存或者傳回錯誤。
(4)ConnectInterceptor:負責與伺服器建立連接配接,使用StreamAllocation.newStream來和服務端建立連接配接,并傳回輸入輸出流(HttpCodec),實際上是通過
StreamAllocation中的findConnection尋找一個可用的Connection,然後調用Connection的connect方法,使用socket與服務端建立連接配接。
(5)CallServerInterceptor:負責從伺服器讀取響應的資料,主要的工作就是把請求的Request寫入到服務端,然後從服務端讀取Response。
3.4 設計模式
(1)攔截器:責任鍊模式
(2)okhttpclient:外觀模式
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new HttpLoggingInterceptor())
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();
在這裡,我們執行個體化了一個HTTP的用戶端client,然後配置了它的一些參數,比如攔截器、逾時時間。我們通過一個統一的對象,調用一個接口或方法,就能完成我們的需求,而其内部的各種複雜對象的調用和跳轉都不需要我們關心,進而降低通路複雜系統的内部子系統時的複雜度,簡化用戶端與之的接口。
(3)Request:建造者模式
val request: Request = Request.Builder()
.url(url)
.build()
//Request.kt
open class Builder {
internal var url: HttpUrl? = null
internal var method: String
internal var headers: Headers.Builder
internal var body: RequestBody? = null
constructor() {
this.method = "GET"
this.headers = Headers.Builder()
}
open fun build(): Request {
return Request(
checkNotNull(url) { "url == null" },
method,
headers.build(),
body,
tags.toImmutableMap()
)
}
}
從Request的生成代碼中可以看到,用到了其内部類Builder,然後通過Builder類組裝出了一個完整的有着各種參數的Request類。我們可以通過Builder,建構不同的Request請求,隻需要傳入不同的請求位址url,請求方法method,頭部資訊headers,請求體body即可。
(4)享元模式:通過線程池、連接配接池共享對象
(5)工廠模式:通過OkHttpClient生産出産品RealCall
四、Forest使用
4.1 Forest基礎
4.1.1 配置層級
- 全局配置:
/application.yml
配置(spring、Spring Boot項目)以及通過application.properties
對象(普通Java項目)設定ForestConfiguration
- 接口配置
- 請求配置 Forest 的配置層級:
- 全局配置:針對全局所有請求,作用域最大,配置讀取的優先級最小。
- 接口配置:作用域為某一個
中定義的請求,讀取的優先級最小。可以通過在interface
上修飾interface
注解進行配置。@BaseRequest
- 請求配置:作用域為某一個具體的請求,讀取的優先級最高。可以在接口的方法上修飾
注解進行 HTTP 資訊配置的定義。@Request
4.1.2 全局基本配置
下面以Spring Boot項目為例:
在
application.yaml
/
application.properties
中配置的 HTTP 基本參數
forest:
bean-id: config0 # 在spring上下文中bean的id, 預設值為forestConfiguration
backend: okhttp3 # 後端HTTP API: okhttp3,預設為okhttp3,也可以改為httpclient
max-connections: 1000 # 連接配接池最大連接配接數,預設值為500
max-route-connections: 500 # 每個路由的最大連接配接數,預設值為500
timeout: 3000 # 請求逾時時間,機關為毫秒, 預設值為3000
connect-timeout: 3000 # 連接配接逾時時間,機關為毫秒, 預設值為2000
retry-count: 1 # 請求失敗後重試次數,預設為0次不重試
ssl-protocol: SSLv3 # 單向驗證的HTTPS的預設SSL協定,預設為SSLv3
logEnabled: true # 打開或關閉日志,預設為true
log-request: true # 打開/關閉Forest請求日志(預設為 true)
log-response-status: true # 打開/關閉Forest響應狀态日志(預設為 true)
log-response-content: true # 打開/關閉Forest響應内容日志(預設為 false)
配置Bean ID
Forest 允許在 yaml 檔案中配置 Bean Id,它對應着
ForestConfiguration
對象在 Spring 上下文中的 Bean 名稱。
forest:
bean-id: config0 # 在spring上下文中bean的id,預設值為forestConfiguration
然後便可以在 Spring 中通過 Bean 的名稱引用到它
@Resource(name = "config0")
private ForestConfiguration config0;
4.1.3 建構接口
在 Forest 依賴加入好之後,就可以建構 HTTP 請求的接口了,在 Forest 中,所有的 HTTP 請求資訊都要綁定到某一個接口的方法上,不需要編寫具體的代碼去發送請求。請求發送方通過調用事先定義好 HTTP 請求資訊的接口方法,自動去執行 HTTP 發送請求的過程,其具體發送請求資訊就是該方法對應綁定的 HTTP 請求資訊。
public interface MyClient {
/**
* 擷取使用者所有裝置資訊
*/
@Post(url = "https://yunlong.farm.xiaomaiot.com/v6/device_chunk/all",
headers = {
"token: ${token}",
"Content-Type:application/json"
})
String getDevice(@DataVariable("token") String token);
}
4.1.4 HTTP Method
(1)POST方式
public interface MyClient {
/**
* 通過 @Request 注解的 type 參數指定 HTTP 請求的方式。
*/
@Request(
url = "http://localhost:8080/hello",
type = "POST"
)
String simplePost();
/**
* 使用 @Post 注解,可以去掉 type = "POST" 這行屬性
*/
@Post("http://localhost:8080/hello")
String simplePost();
/**
* 使用 @PostRequest 注解,和上面效果等價
*/
@PostRequest("http://localhost:8080/hello")
String simplePost();
}
(2)GET請求
// GET請求
@Request(
url = "http://localhost:8080/hello",
type = "get"
)
String simpleGet();
(3)PUT請求
// PUT請求
@Request(
url = "http://localhost:8080/hello",
type = "put"
)
String simplePut();
(4)HEAD請求
// HEAD請求
@Request(
url = "http://localhost:8080/hello",
type = "head"
)
String simpleHead();
(5)Options請求
// Options請求
@Request(
url = "http://localhost:8080/hello",
type = "options"
)
String simpleOptions();
(6)Delete請求
// Delete請求
@Request(
url = "http://localhost:8080/hello",
type = "delete"
)
String simpleDelete();
注:
-
和@Get
兩個注解的效果是等價的,@GetRequest
和@Post
、@PostRequest
和@Put
等注解也是同理@PutRequest
- HEAD請求類型沒有對應的@Head注解,隻有@HeadRequest注解,原因是容易和@Header注解混淆
4.1.5 HTTP URL
HTTP請求可以沒有請求頭、請求體,但一定會有
URL
,以及很多請求的參數都是直接綁定在
URL
的
Query
部分上。
基本
URL
設定方法隻要在
url
屬性中填入完整的請求位址即可。
除此之外,也可以從外部動态傳入
URL
:
/**
* 整個完整的URL都通過 @DataVariable 注解修飾的參數動态傳入
*/
@Request("${myURL}")
String send1(@DataVariable("myURL") String myURL);
/**
* 通過參數轉入的值值作為URL的一部分
*/
@Request("http://${myURL}/abc")
String send2(@DataVariable("myURL") String myURL);
4.1.6 HTTP Header
(1)headers屬性
該接口調用後所實際産生的 HTTP 請求如下:
GET http://localhost:8080/hello/user
HEADER:
Accept-Charset: utf-8
Content-Type: text/plain
(2)資料綁定
這段調用所實際産生的 HTTP 請求如下:
GET http://localhost:8080/hello/user
HEADER:
Accept-Charset: gbk
Content-Type: text/plain
(3)@Header注解
/**
* 使用 @Header 注解将參數綁定到請求頭上
* @Header 注解的 value 指為請求頭的名稱,參數值為請求頭的值
* @Header("Accept") String accept将字元串類型參數綁定到請求頭 Accept 上
* @Header("accessToken") String accessToken将字元串類型參數綁定到請求頭 accessToken 上
*/
@Post("http://localhost:8080/hello/user?username=foo")
void postUser(@Header("Accept") String accept, @Header("accessToken") String accessToken);
/**
* 使用 @Header 注解可以修飾 Map 類型的參數
* Map 的 Key 指為請求頭的名稱,Value 為請求頭的值
* 通過此方式,可以将 Map 中所有的鍵值對批量地綁定到請求頭中
*/
@Post("http://localhost:8080/hello/user?username=foo")
void headHelloUser(@Header Map<String, Object> headerMap);
/**
* 使用 @Header 注解可以修飾自定義類型的對象參數
* 依據對象類的 Getter 和 Setter 的規則取出屬性
* 其屬性名為 URL 請求頭的名稱,屬性值為請求頭的值
* 以此方式,将一個對象中的所有屬性批量地綁定到請求頭中
*/
@Post("http://localhost:8080/hello/user?username=foo")
void headHelloUser(@Header MyHeaderInfo headersInfo);
4.1.7 HTTP Body
在
POST
和
PUT
等請求方法中,通常使用 HTTP 請求體進行傳輸資料。在 Forest 中有多種方式設定請求體資料。
(1)@Body注解
您可以使用
@Body
注解修飾參數的方式,将傳入參數的資料綁定到 HTTP 請求體中。
/**
* 預設body格式為 application/x-www-form-urlencoded,即以表單形式序列化資料
*/
@Post(
url = "http://localhost:8080/user",
headers = {"Accept:text/plain"}
)
String sendPost(@Body("username") String username, @Body("password") String password);
(2)表單格式
上面使用 @Body 注解的例子用的是普通的表單格式,也就是
contentType
屬性為
application/x-www-form-urlencoded
的格式,即
contentType
不做配置時的預設值。
表單格式的請求體以字元串
key1=value1&key2=value2&...&key{n}=value{n}
的形式進行傳輸資料,其中
value
都是已經過 URL Encode 編碼過的字元串。
/**
* contentType屬性設定為 application/x-www-form-urlencoded 即為表單格式,
* 當然不設定的時候預設值也為 application/x-www-form-urlencoded, 也同樣是表單格式。
* 在 @Body 注解的 value 屬性中設定的名稱為表單項的 key 名,
* 而注解所修飾的參數值即為表單項的值,它可以為任何類型,不過最終都會轉換為字元串進行傳輸。
*/
@Post(
url = "http://localhost:8080/user",
contentType = "application/x-www-form-urlencoded",
headers = {"Accept:text/plain"}
)
String sendPost(@Body("key1") String value1, @Body("key2") Integer value2, @Body("key3") Long value3);
(3)JSON格式
(4)XML格式
4.1.8 @BaseRequest注解
@BaseRequest
注解定義在接口類上,在
@BaseRequest
上定義的屬性會被配置設定到該接口中每一個方法上,但方法上定義的請求屬性會覆寫
@BaseRequest
上重複定義的内容。 是以可以認為
@BaseRequest
上定義的屬性内容是所在接口中所有請求的預設屬性。
/**
* @BaseRequest 為配置接口層級請求資訊的注解,
* 其屬性會成為該接口下所有請求的預設屬性,
* 但可以被方法上定義的屬性所覆寫
*/
@BaseRequest(
baseURL = "http://localhost:8080", // 預設域名
headers = {
"Accept:text/plain" // 預設請求頭
},
sslProtocol = "TLS" // 預設單向SSL協定
)
public interface MyClient {
// 方法的URL不必再寫域名部分
@Get("/hello/user")
String send1(@Query("username") String username);
// 若方法的URL是完整包含http://開頭的,那麼會以方法的URL中域名為準,不會被接口層級中的baseURL屬性覆寫
@Get("http://www.xxx.com/hello/user")
String send2(@Query("username") String username);
@Get(
url = "/hello/user",
headers = {
"Accept:application/json" // 覆寫接口層級配置的請求頭資訊
}
)
String send3(@Query("username") String username);
}
4.1.9 資料轉換
(1)序列化
Forest中對資料進行序列化可以通過指定
contentType
屬性或
Content-Type
頭指定内容格式。
@Request(
url = "http://localhost:8080/hello/user",
type = "post",
contentType = "application/json" // 指定contentType為application/json
)
String postJson(@Body MyUser user); // 自動将user對象序列化為JSON格式
同理,指定為
application/xml
會将參數序列化為
XML
格式,
text/plain
則為文本,預設的
application/x-www-form-urlencoded
則為表格格式。
(2)反序列化
HTTP請求響應後傳回結果的資料同樣需要轉換,Forest則會将傳回結果自動轉換為您通過方法傳回類型指定對象類型。這個過程就是反序列化,您可以通過
dataType
指定傳回資料的反序列化格式。
@Request(
url = "http://localhost:8080/data",
dataType = "json" // 指定dataType為json,将按JSON格式反序列化資料
)
Map getData(); // 請求響應的結果将被轉換為Map類型對象
4.1.10 日志管理
Forest在發送請求時和接受響應資料時都會自動列印出HTTP請求相關的日志,其中包括:請求日志、響應狀态日志、響應内容日志。
(1)請求日志
請求日志會列印出所有請求發送的内容,其中包括請求行、請求頭、請求體三部分
[Forest] Request:
POST http://localhost:8080/test HTTP
Headers:
accessToken: abcdefg123456
Body: username=foo&password=bar
(2)響應狀态日志
響應狀态日志包含了HTTP請求響應後接受到的狀态碼,以及響應時間
[Forest] Response: Status = 200, Time = 11ms
(3)響應内容日志
響應内容日志則會列印出請求發送的目标伺服器響應後,傳回給請求接受方的實際資料内容
此外,Forest還支援回調函數以及異步請求等。
4.2 Forest進階
4.2.1 HTTPS
(1)單向認證
如果通路的目标站點的SSL證書由信任的Root CA釋出的,無需做任何事情便可以自動信任
public interface Gitee {
@Request(url = "https://gitee.com")
String index();
}
Forest的單向驗證的預設協定為
SSLv3
,如果一些站點的API不支援該協定,可以在全局配置中将
ssl-protocol
屬性修改為其它協定,如:
TLSv1.1
,
TLSv1.2
,
SSLv2
等等。
forest:
...
ssl-protocol: TLSv1.2
全局配置可以配置一個全局統一的SSL協定,但現實情況是有很多不同服務(尤其是第三方)的API會使用不同的SSL協定,這種情況需要針對不同的接口設定不同的SSL協定。
/**
* 在某個請求接口上通過 sslProtocol 屬性設定單向SSL協定
*/
@Get(
url = "https://localhost:5555/hello/user",
sslProtocol = "SSL"
)
ForestResponse<String> truestSSLGet();
也可以在
@BaseRequest
注解中設定一整個接口類的SSL協定
@BaseRequest(sslProtocol = "TLS")
public interface SSLClient {
@Get("https://localhost:5555/hello/user")
String testSend();
}
(2)雙向認證
若是需要在Forest中進行雙向驗證的HTTPS請求,處理如下:
在全局配置中添加
keystore
配置:
forest:
...
ssl-key-stores:
- id: keystore1 # id為該keystore的名稱,必填
file: test.keystore # 公鑰檔案位址
keystore-pass: 123456 # keystore秘鑰
cert-pass: 123456 # cert秘鑰
protocols: SSLv3 # SSL協定
接着,在
@Request
中引入該
keystore
的
id
即可
@Request(
url = "https://localhost:5555/hello/user",
keyStore = "keystore1"
)
String send();
也可以在全局配置中配多個
keystore
:
forest:
...
ssl-key-stores:
- id: keystore1 # 第一個keystore
file: test1.keystore
keystore-pass: 123456
cert-pass: 123456
protocols: SSLv3
- id: keystore2 # 第二個keystore
file: test2.keystore
keystore-pass: abcdef
cert-pass: abcdef
protocols: SSLv3
...
4.2.2 異常處理
發送HTTP請求不會總是成功的,總會有失敗的情況。Forest提供多種異常處理的方法來處理請求失敗的過程。
(1)try-catch方式
最常用的是直接用
try-catch
。Forest請求失敗的時候通常會以抛異常的方式報告錯誤, 擷取錯誤資訊隻需捕獲
ForestNetworkException
異常類的對象,如示例代碼所示:
/**
* try-catch方式:捕獲ForestNetworkException異常類的對象
*/
try {
String result = myClient.send();
} catch (ForestNetworkException ex) {
int status = ex.getStatusCode(); // 擷取請求響應狀态碼
ForestResponse response = ex.getResponse(); // 擷取Response對象
String content = response.getContent(); // 擷取請求的響應内容
String resResult = response.getResult(); // 擷取方法傳回類型對應的最終資料結果
}
(2)回調函數方式
第二種方式是使用
OnError
回調函數,如示例代碼所示:
/**
* 在請求接口中定義OnError回調函數類型參數
*/
@Request(
url = "http://localhost:8080/hello/user",
headers = {"Accept:text/plain"},
data = "username=${username}"
)
String send(@DataVariable("username") String username, OnError onError);
調用的代碼如下:
// 在調用接口時,在Lambda中處理錯誤結果
myClient.send("foo", (ex, request, response) -> {
int status = response.getStatusCode(); // 擷取請求響應狀态碼
String content = response.getContent(); // 擷取請求的響應内容
String result = response.getResult(); // 擷取方法傳回類型對應的最終資料結果
});
(3)ForestResponse
第三種,用
ForestResponse
類作為請求方法的傳回值類型,示例代碼如下:
/**
* 用`ForestResponse`類作為請求方法的傳回值類型, 其泛型參數代表實際傳回資料的類型
*/
@Request(
url = "http://localhost:8080/hello/user",
headers = {"Accept:text/plain"},
data = "username=${username}"
)
ForestResponse<String> send(@DataVariable("username") String username);
調用和處理的過程如下:
ForestResponse<String> response = myClient.send("foo");
// 用isError方法判斷請求是否失敗, 比如404, 500等情況
if (response.isError()) {
int status = response.getStatusCode(); // 擷取請求響應狀态碼
String content = response.getContent(); // 擷取請求的響應内容
String result = response.getResult(); // 擷取方法傳回類型對應的最終資料結果
}
(4)攔截器方式
若要批量處理各種不同請求的異常情況,可以定義一個攔截器, 并在攔截器的
onError
方法中處理異常,示例代碼如下:
public class ErrorInterceptor implements Interceptor<String> {
// ... ...
@Override
public void onError(ForestRuntimeException ex, ForestRequest request, ForestResponse response) {
int status = response.getStatusCode(); // 擷取請求響應狀态碼
String content = response.getContent(); // 擷取請求的響應内容
Object result = response.getResult(); // 擷取方法傳回類型對應的傳回資料結果
}
}
4.2.3 攔截器
(1)建構攔截器
實作com.dtflys.forest.interceptor.Interceptor接口
public class SimpleInterceptor implements Interceptor<String> {
private final static Logger log = LoggerFactory.getLogger(SimpleInterceptor.class);
/**
* 該方法在被調用時,并在beforeExecute前被調用
* @Param request Forest請求對象
* @Param args 方法被調用時傳入的參數數組
*/
@Override
public void onInvokeMethod(ForestRequest request, ForestMethod method, Object[] args) {
log.info("on invoke method");
// addAttribute作用是添加和Request以及該攔截器綁定的屬性
addAttribute(request, "A", "value1");
addAttribute(request, "B", "value2");
}
/**
* 該方法在請求發送之前被調用, 若傳回false則不會繼續發送請求
* @Param request Forest請求對象
*/
@Override
public boolean beforeExecute(ForestRequest request) {
log.info("invoke Simple beforeExecute");
// 執行在發送請求之前處理的代碼
request.addHeader("accessToken", "11111111"); // 添加Header
request.addQuery("username", "foo"); // 添加URL的Query參數
return true; // 繼續執行請求傳回true
}
/**
* 該方法在請求成功響應時被調用
*/
@Override
public void onSuccess(String data, ForestRequest request, ForestResponse response) {
log.info("invoke Simple onSuccess");
// 執行成功接收響應後處理的代碼
int status = response.getStatusCode(); // 擷取請求響應狀态碼
String content = response.getContent(); // 擷取請求的響應内容
String result = data; // data參數是方法傳回類型對應的傳回資料結果
result = response.getResult(); // getResult()也可以擷取傳回的資料結果
response.setResult("修改後的結果: " + result); // 可以修改請求響應的傳回資料結果
// 使用getAttributeAsString取出屬性,這裡隻能取到與該Request對象,以及該攔截器綁定的屬性
String attrValue1 = getAttributeAsString(request, "A1");
}
/**
* 該方法在請求發送失敗時被調用
*/
@Override
public void onError(ForestRuntimeException ex, ForestRequest request, ForestResponse response) {
log.info("invoke Simple onError");
// 執行發送請求失敗後處理的代碼
int status = response.getStatusCode(); // 擷取請求響應狀态碼
String content = response.getContent(); // 擷取請求的響應内容
String result = response.getResult(); // 擷取方法傳回類型對應的傳回資料結果
}
/**
* 該方法在請求發送之後被調用
*/
@Override
public void afterExecute(ForestRequest request, ForestResponse response) {
log.info("invoke Simple afterExecute");
// 執行在發送請求之後處理的代碼
int status = response.getStatusCode(); // 擷取請求響應狀态碼
String content = response.getContent(); // 擷取請求的響應内容
String result = response.getResult(); // 擷取方法傳回類型對應的最終資料結果
}
}
4.2.4 檔案上傳下載下傳
(1)上傳
/**
* 用@DataFile注解修飾要上傳的參數對象
* OnProgress參數為監聽上傳進度的回調函數
*/
@Post(url = "/upload")
Map upload(@DataFile("file") String filePath, OnProgress onProgress);
調用上傳接口以及監聽上傳進度的代碼如下:
Map result = myClient.upload("D:\\TestUpload\\xxx.jpg", progress -> {
System.out.println("total bytes: " + progress.getTotalBytes()); // 檔案大小
System.out.println("current bytes: " + progress.getCurrentBytes()); // 已上傳位元組數
System.out.println("progress: " + Math.round(progress.getRate() * 100) + "%"); // 已上傳百分比
if (progress.isDone()) { // 是否上傳完成
System.out.println("-------- Upload Completed! --------");
}
});
在檔案上傳的接口定義中,除了可以使用字元串表示檔案路徑外,還可以用以下幾種類型的對象表示要上傳的檔案:
/**
* File類型對象
*/
@Post(url = "/upload")
Map upload(@DataFile("file") File file, OnProgress onProgress);
/**
* byte數組
* 使用byte數組和Inputstream對象時一定要定義fileName屬性
*/
@Post(url = "/upload")
Map upload(@DataFile(value = "file", fileName = "${1}") byte[] bytes, String filename);
/**
* Inputstream 對象
* 使用byte數組和Inputstream對象時一定要定義fileName屬性
*/
@Post(url = "/upload")
Map upload(@DataFile(value = "file", fileName = "${1}") InputStream in, String filename);
/**
* Spring Web MVC 中的 MultipartFile 對象
*/
@PostRequest(url = "/upload")
Map upload(@DataFile(value = "file") MultipartFile multipartFile, OnProgress onProgress);
/**
* Spring 的 Resource 對象
*/
@Post(url = "/upload")
Map upload(@DataFile(value = "file") Resource resource);
(2)多檔案批量上傳
/**
* 上傳Map包裝的檔案清單
* 其中 ${_key} 代表Map中每一次疊代中的鍵值
*/
@PostRequest(url = "/upload")
ForestRequest<Map> uploadByteArrayMap(@DataFile(value = "file", fileName = "${_key}") Map<String, byte[]> byteArrayMap);
/**
* 上傳List包裝的檔案清單
* 其中 ${_index} 代表每次疊代List的循環計數(從零開始計)
*/
@PostRequest(url = "/upload")
ForestRequest<Map> uploadByteArrayList(@DataFile(value = "file", fileName = "test-img-${_index}.jpg") List<byte[]> byteArrayList);
/**
* 上傳數組包裝的檔案清單
* 其中 ${_index} 代表每次疊代List的循環計數(從零開始計)
*/
@PostRequest(url = "/upload")
ForestRequest<Map> uploadByteArrayArray(@DataFile(value = "file", fileName = "test-img-${_index}.jpg") byte[][] byteArrayArray);
(3)下載下傳
/**
* 在方法上加上@DownloadFile注解
* dir屬性表示檔案下載下傳到哪個目錄
* filename屬性表示檔案下載下傳成功後以什麼名字儲存,如果不填,這預設從URL中取得檔案名
* OnProgress參數為監聽上傳進度的回調函數
*/
@Get(url = "http://localhost:8080/images/xxx.jpg")
@DownloadFile(dir = "${0}", filename = "${1}")
File downloadFile(String dir, String filename, OnProgress onProgress);
調用下載下傳接口以及監聽上傳進度的代碼如下:
File file = myClient.downloadFile("D:\\TestDownload", progress -> {
System.out.println("total bytes: " + progress.getTotalBytes()); // 檔案大小
System.out.println("current bytes: " + progress.getCurrentBytes()); // 已下載下傳位元組數
System.out.println("progress: " + Math.round(progress.getRate() * 100) + "%"); // 已下載下傳百分比
if (progress.isDone()) { // 是否下載下傳完成
System.out.println("-------- Download Completed! --------");
}
});
如果您不想将檔案下載下傳到硬碟上,而是直接在記憶體中讀取,可以去掉@DownloadFile注解,并且用以下幾種方式定義接口:
/**
* 傳回類型用byte[],可将下載下傳的檔案轉換成位元組數組
*/
@GetRequest(url = "http://localhost:8080/images/test-img.jpg")
byte[] downloadImageToByteArray();
/**
* 傳回類型用InputStream,用流的方式讀取檔案内容
*/
@GetRequest(url = "http://localhost:8080/images/test-img.jpg")
InputStream downloadImageToInputStream();
4.2.5 其它
使用Cookie、使用代理、自定義注解、模闆表達式······
本文檔部分内容摘自官方文檔,具體詳情可參見:Forest官網:http://forest.dtflyx.com/
注:
筆者寫了一個基于Springboot的demo(Maven項目),分别采用Forest、HttpClient、Okhttp三種方式調用高德地圖API,Forest中還包括攔截器的使用、下載下傳圖檔等示例,項目具體目錄結構如下圖所示,包括Forest、HttpClient、Okhttp三部分:
使用Postman進行測試:
本案例中所有代碼已上傳至本人部落格資源下載下傳首頁:https://download.csdn.net/download/qq_38233258/16731710