天天看點

輕量級HTTP用戶端架構—Forest學習筆記一、Forest二、HttpClient三、Okhttp四、Forest使用

輕量級HTTP用戶端架構—Forest學習筆記

一、Forest

1.1 業務需求

一般情況下是後端提供接口,前端調用,解決需求,但是有的時候為了友善,複用别人的接口(網上的,公共的第三方接口(短信、天氣等)),就出現了後端調用後端接口的情況。

此外,因為業務關系,要和許多不同第三方公司進行對接。這些服務商都提供基于http的api,但是每家公司提供api具體細節差别很大。有的基于RESTFUL規範,有的基于傳統的http規範;有的需要在header裡放置簽名,有的需要SSL的雙向認證,有的隻需要SSL的單向認證;有的以JSON方式進行序列化,有的以XML方式進行序列化······類似于這樣細節的差别較多。

不涉及業務的公共http調用套件 ???

1.2 Forest簡介

輕量級HTTP用戶端架構—Forest學習筆記一、Forest二、HttpClient三、Okhttp四、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學習筆記一、Forest二、HttpClient三、Okhttp四、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(網絡攔截器)對請求依次處理,與伺服器建立連接配接後,擷取傳回資料,再經過上述攔截器依次 處理後,最後将結果傳回給調用方。具體過程如下圖所示:

輕量級HTTP用戶端架構—Forest學習筆記一、Forest二、HttpClient三、Okhttp四、Forest使用

3.3 具體架構圖

輕量級HTTP用戶端架構—Forest學習筆記一、Forest二、HttpClient三、Okhttp四、Forest使用

(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)攔截器:責任鍊模式

輕量級HTTP用戶端架構—Forest學習筆記一、Forest二、HttpClient三、Okhttp四、Forest使用

(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

    /

    application.properties

    配置(spring、Spring Boot項目)以及通過

    ForestConfiguration

    對象(普通Java項目)設定
  • 接口配置
  • 請求配置
    輕量級HTTP用戶端架構—Forest學習筆記一、Forest二、HttpClient三、Okhttp四、Forest使用
    Forest 的配置層級:
  1. 全局配置:針對全局所有請求,作用域最大,配置讀取的優先級最小。
  2. 接口配置:作用域為某一個

    interface

    中定義的請求,讀取的優先級最小。可以通過在

    interface

    上修飾

    @BaseRequest

    注解進行配置。
  3. 請求配置:作用域為某一個具體的請求,讀取的優先級最高。可以在接口的方法上修飾

    @Request

    注解進行 HTTP 資訊配置的定義。

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用戶端架構—Forest學習筆記一、Forest二、HttpClient三、Okhttp四、Forest使用

該接口調用後所實際産生的 HTTP 請求如下:

GET http://localhost:8080/hello/user
HEADER:
    Accept-Charset: utf-8
    Content-Type: text/plain
           

(2)資料綁定

輕量級HTTP用戶端架構—Forest學習筆記一、Forest二、HttpClient三、Okhttp四、Forest使用

這段調用所實際産生的 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三部分:

輕量級HTTP用戶端架構—Forest學習筆記一、Forest二、HttpClient三、Okhttp四、Forest使用
輕量級HTTP用戶端架構—Forest學習筆記一、Forest二、HttpClient三、Okhttp四、Forest使用

使用Postman進行測試:

輕量級HTTP用戶端架構—Forest學習筆記一、Forest二、HttpClient三、Okhttp四、Forest使用
本案例中所有代碼已上傳至本人部落格資源下載下傳首頁:https://download.csdn.net/download/qq_38233258/16731710
輕量級HTTP用戶端架構—Forest學習筆記一、Forest二、HttpClient三、Okhttp四、Forest使用

繼續閱讀