Java 基礎(二十)網路框架設計 MyVolley (上)

diamond_lin發表於2017-11-30

上週答應大家的,手擼一個網路請求框架。
學了快兩個月的 java 基礎,現在我們來手擼一個網路請求框架練練手。
手寫一個網路請求框架需要掌握的知識點比較多,其中牽涉到設計模式、集合、泛型、多執行緒及併發、網路程式設計等知識,算是對 java 基本功比較全面的考查,同時,對架構能力也有一定的要求。

需求

先來看看需求~~

  • 支援請求 JSON 文字型別學,音訊,圖片型別,批量下載。上傳~
  • 請求各種資料時,呼叫層不用關心上傳引數的封裝
  • 獲取資料後,呼叫層不用關心 JSON 資料的解析
  • 回撥時,呼叫層只需要知道傳入的 JSON 的對應響應類
  • 回撥響應結果發生在主執行緒(執行緒切換)
  • 對下載,上傳擴充套件
  • 支援高併發請求,請求佇列一次獲取,可以設定最大併發數,設定先請求先執行

架構設計

首先我們來回顧一下一次網路請求的流程。

1.準備請求
2.根據請求的引數發起網路請求
3.請求結果處理及回撥

然後我們呼叫的程式碼大概長這個樣子~

Volley.sendRequest(null, url, GankResponse.class, GET, new IDataListener<GankResponse>() {
        @Override
        public void onSuccess(GankResponse response) {}

        @Override
        public void onFail(int errorCode, String errorMsg) {}
});複製程式碼

哈哈,我憑空想象的,其實我並沒有用過 Volley。

嗯,我們最終的目的也是把程式碼寫成這樣子。

剛剛我們說了一次網路請求大概是三個步驟,接下來,我們就根據這三個步驟來設計網路架構。敲黑板,記住了這三個階段。

階段一:準備請求

1.準備引數

這個階段一般是從 Activity 介面發起的,說白一點好像就是我客戶端頁面不知道顯示什麼樣的資料,去找伺服器要。一次網路請求實際上就是和服務區的一次互動,在互動過程中,根據 HTTP 協議(之前講過,弄不明白的回去翻翻我的文章),我們需要準備以下幾個引數

  • url 網址
  • requestParams 請求引數
  • 請求方法(GET、POST 等)
  • responseBean 接收響應的 bean
  • 請求結果回撥給 activity (IDataListener①)

其中前三個引數是用來做網路請求的,後兩個引數是用來做請求響應的。

因此,我們這裡需要一個一個 bean (RequestHolder ②)來封裝請求引數RequestHolder需要如下屬性

  • requestParams
  • url
  • 請求方法型別(GET、POST)

然後我們需要一個類(IHttpService ③)去發起這個網路請求,這個類最少需要以下幾個屬性

  • RequestHolder 例項
  • excute 方法執行網路請求
  • IHttpListener④(持有請求結果處理類例項)

    至於上面說的這個IHttpListener,屬於階段三,我們來看看IHttpListener。

階段三:請求結果處理及回撥 IHttpListener

我們先跳過階段二。

上面我們說了,IHttpService持有IHttpListener,並且在請求結束之後要處理並回撥。
請求結果無非就是兩種,成功和不成功。
所以這個類只需要有兩種行為(方法)和一個屬性

  • onSuccess
  • onFail
  • 持有 IDataListener的例項
  • 解析資料

  • onSuccess
    IHttpService 在網路請求成功之後呼叫這個方法,IHttpListener 需要解析資料(比如說 json 傳、InputStream 轉換成 image、File 等)然後呼叫 IDataListener 返回給 activity

  • onFail
    IHttpService 網路請求失敗呼叫這個方法,IDataListener 直接呼叫 IDataListener 將錯誤資訊返回給 activity

  • 解析資料
    這一步是發生在請求成功之後得到源資料,然後根據源資料的不同型別做不同的解析,最後呼叫IHttpListener 返回給 activity。

階段二:根據請求的引數發起網路請求

根據我們剛才的設計,網路請求的真正發起是IHttpService 的 excuse 方法。然後網路請求需要在子執行緒,我們可以把一次網路請求看成是一個非同步任務,所以我們需要一個類去實現 Runnable 介面,然後在 run 方法裡去呼叫 IHttpService 的excuse 執行網路請求。暫且我們把這個實現了 Runnable 介面的類叫 HttpTask⑤,這個類比較簡單,只需要持有 IHttpService 的例項即可。

  • IHttpService 例項

然後針對一個非同步任務 HttpTask,我們要做一個併發管理,所以需要引入執行緒池來管理所有的 HttpTask。所以我們需要一個單例的 ThreadPoolManger⑥ 來管理所有的 HttpTask,ThreadPoolManger 裡面我們可以直接用 Executors 建立一個執行緒池,至於執行緒池的細節,限制最大併發、阻塞、生產消費者模型是怎麼實現的,可以回過頭去java 技術執行緒相關的內容。

ThreadPoolManger

  • ExecutorService 例項

Volley 類

好,到這裡,我們的網路請求需要的東西差不多就準備好了,現在我們可以在 Activity 裡面建立RequestHolder、IHttpService、IHttpListener、HttpTask等類發起網路請求了,但是好像要建立的東西比較多,而且很多東西不是呼叫者需要關心的東西,容易出錯。對於客戶端來說,我只需要根據伺服器的要求,準備請求引數以及相應引數的型別去接收伺服器的響應即可,而不需要關心請求的過程。
所以我們在這裡建立一個 Volley⑦類,去封裝請求、響應相關的各種類,然後將一個 HttpTask 丟到執行緒池裡面去。這個類很簡單,只需要一個靜態的 sendRequest 方法即可,方法引數需要傳入 url、requestParams等。

  • sendRequest

小結

邏輯有點亂,我來捋一下邏輯。首先是從 Activity 開始,有一個和伺服器互動的需求。步驟如下:

1.呼叫 Volley 的靜態方法sendRequest,傳入方法需要的 url、RequestParams 等各種引數。
2.Volley 的sendRequest 方法根據方法引數,構建RequestHolder、IHttpService、IHttpListener、HttpTask等類,並將HttpTask 丟進ThreadPoolManger的執行緒池裡面等待執行。
3.ThreadPoolManger的執行緒池裡面實際上是一個阻塞佇列,會根據任務取出HttpTask這個任務並執行。
4.HttpTask的 run 方法被呼叫,run 方法呼叫IHttpService 的 excuse 方法,正式發起網路請求。
5.網路請求結束,IHttpService 呼叫IHttpListener 的 成功/失敗 的方法進行相應的處理。
6.網路請求成功,IHttpListener 解析 源資料,並將解析的資料通過 IDataListener回撥給 activity。
7.網路請求失敗,IHttpListener 直接呼叫IDataListener的 fail 方法告知 activity 請求失敗。

哦,對了,執行緒切換忘記講,大家思考一下,應該在哪個類切回主執行緒?答案我會在程式碼中體現。

架構設計大概就是這樣子,為了便於大家理解,我畫了一個類關係圖,大家湊合著看看。

填坑

之前給大家吹了牛逼,說會實現哪些需求,用到哪些知識點,用到哪些設計模式,在擼程式碼之前,我先給大家講解一下。

  • 支援請求 JSON 文字型別,音訊,圖片型別,批量下載、上傳~
    支援請求 JSON 類、圖片、音訊等資源的下載,這個做了擴充套件預留。我們只需要根據請求的資料型別,做不同的實現即可,不同的實現都需要繼承 IHttpService 介面。

  • 請求各種資料時,呼叫層不用關心上傳引數的封裝
    直接呼叫public static IHttpService sendRequest(requestParams,url,responseClass,type,listener)方法即可。

  • 獲取資料後,呼叫層不用關心 JSON 資料的解析
    資料的解析同IHttpService,同樣只需要根據不同的資料型別,做不同的 IHttpListener 實現即可。

  • 回撥時,呼叫層只需要知道傳入的 JSON 的對應響應類
    使用了泛型,會在IHttpListener 的 Json 資料實現類裡面將 json 資料轉換成響應類的 bean。

  • 回撥響應結果發生在主執行緒(執行緒切換)
    已實現,在IHttpListener 的實現類裡面。

  • 對下載,上傳擴充套件
    預留了介面,同樣只需要對IHttpService 和 IHttpListener介面進行擴充套件,下一篇文章會講解實現。

  • 支援高併發請求,請求佇列一次獲取,可以設定最大併發數,設定先請求先執行
    通過執行緒池實現。

會用到的知識點
  • 泛型
    請求引數、回撥引數都是泛型實現
  • 請求佇列
    執行緒池裡面做了封裝,這裡直接呼叫concurrent 包裡面的工具類 Executors建立了執行緒池,裡面用到了 LinkedBlockingQueue 。

  • 阻塞佇列
    LinkedBlockingQueue 就是阻塞佇列,具體參考 concurrent 工具包的講解。

  • 執行緒拒絕策略
    待實現

用到的設計模式
  • 模板方法模式

    模板方法:定義一個操作中演算法的框架,而將一些步驟延遲到子類中。模板方法模式使得子類可以不改變一個演算法的結構即可重定義該演算法的某些特定步驟
    我的HttpTask 裡面的網路請求痛的都是 IHttpService 類,根據請求的不同資料,使用不同的子類,同理還有 IHttpListener 。

  • 單例模式
    ThreadPoolManger 是單例

  • 策略模式
    根據不同的請求資料,選擇使用IHttpService JSonHTTPService 或者FileDownHttpService(這個類在檔案下載模組中)

  • 生產者消費者模式
    執行緒池本身就是一個生產者消費者模型,Activity 發生一個請求,Volley 將請求生產成一個 HttpTask 丟進執行緒池,然後請求結束就相當於消費了這個請求。

擼程式碼

架構設計出來了,類關係圖也設計好了,接下來我們就開始擼程式碼了。

從哪裡開始擼呢? 你可以選擇按照邏輯順序,從 activity 裡面呼叫 Volley 開始擼,差什麼類就擼什麼類,遇神殺神遇佛殺佛。
然而我不推薦這種方法,這種方法容易出錯,效率低。

首先,我們先來看看,上面分析的時候,我標記的幾個重點類物件

①IDataListener
②RequestHolder
③IHttpService
④IHttpListener
⑤HttpTask
⑥ThreadManger
⑦Volley

接下來,我們就來按照這個順序擼程式碼吧。

IDataListener

這個類很簡單,只需要做一個請求結果回撥。

public interface IDataListener<T> {
    /**
     * @param t 響應引數
     */
    void onSuccess(T t);

    void onFail(int errorCode, String errorMsg);
}複製程式碼

其中 T 是泛型操作,因為我們也不知道返回結果會是什麼型別的資料結構。

RequestHolder

現在直接擼這個類,我們會遇到問題,因為RequestHolder會持有 IHttpService 和 IHttpListener 的引用,所以我們應該先把兩個介面擼出來。

這個類很簡單,只需要持有IHttpService、IHttpListener、URL、requestParams、RequestHolder 等引數就行了,沒有任何邏輯操作。

注意:這裡只需要持有IHttpService、和IHttpListener的介面引用,不要去持有這兩個介面的例項引用。

IHttpService

由於 IHttpService 被 RequestHolder 持有且是真正的網路請求相關的類構造類以及執行類,所以我們需要定義設定一下介面方法。

public interface IHttpService {

    /**
     * 設定 url
     *
     * @param url url address
     */
    void setUrl(String url);

    /**
     * 設定處理介面
     *
     * @param listener 處理介面
     */
    void setHttpListener(IHttpListener listener);

    /**
     * 設定請求引數
     *
     * @param data 請求引數 byte 陣列
     */
    void setRequestData(byte[] data);

    /**
     * 執行請求
     */
    void excute();

    void cancel();

    boolean isCancel();

    void setRequestHeader(Map<String, String> map);

    void setRequestType(String type);

}複製程式碼

定義了以上介面方法,有些引數真的是懶得寫註釋了,你們應該都看得懂。

然後在 IHttpService 的實現類裡面需要做具體的 Http 請求,這裡我偷個懶,直接用了 HttpClient。具體實現類 JsonHttpService大家可以去下載我的原始碼閱讀。

HttpListener

這個類是交給 IHttpService 類在網路請求結束之後負責解析資料的,由於網路請求的結果只會有兩種--成功和失敗,所以這裡就不定義解析資料的方法了,因為它是在網路請求成功之後呼叫的。

public interface IHttpListener {

    /**
     * 網路請求成功回撥
     *
     * @param httpEntity 網路請求返回結果
     */
    void onSuccess(HttpEntity httpEntity);

    void onFail(int errorCode, String errorMsg);
}複製程式碼

然後 IHttpListener 持有 IDataListener 例項,最後呼叫 IDataListener 來回撥給 Activity。
對了,這裡需要做執行緒切換哦,具體實現程式碼也很簡單,請下載原始碼自行閱讀。

HttpTask

整套網路請求寫完了,我們需要一個類來呼叫 IHttpService 的 excuse 的方法去執行網路請求,且excuse方法必須在子執行緒。我們把一次網路請求封裝成一個非同步任務,即一個 Runnable,程式碼很簡單。

public class HttpTask<T> implements Runnable {

    private IHttpService mHttpService;

    public HttpTask(RequestHolder<T> holder) throws UnsupportedEncodingException {
        mHttpService = holder.getHttpService();
        mHttpService.setHttpListener(holder.getHttpListener());
        mHttpService.setRequestType(holder.getRequestType());
        mHttpService.setUrl(holder.getUrl());
        mHttpService.setRequestHeader(holder.getRequestHeader());
        T requestParams = holder.getRequestParams();
        if (requestParams != null) {
            String requestInfo = JSON.toJSONString(requestParams);
            mHttpService.setRequestData(requestInfo.getBytes("UTF-8"));
        }
    }

    @Override
    public void run() {
        if (!mHttpService.isCancel()) {
            mHttpService.excute();
        }
    }
}複製程式碼

ThreadPoolManger

最後我們需要一個執行緒池管理HttpTask。所以我們需要一個ThreadPoolManger,因為這個類必須保證唯一,所以單例。執行緒池管理我們直接用 concurrent 包封裝好的Executors 類建立,這個類程式碼很簡單,如下:

public class ThreadPoolManger {
    private static volatile ThreadPoolManger mInstance;
    private final ExecutorService mExecutorService;

    private ThreadPoolManger() {
        mExecutorService = Executors.newFixedThreadPool(4);
    }

    public static ThreadPoolManger getInstance() {
        if (mInstance == null) {
            synchronized (ThreadPoolManger.class) {
                mInstance = new ThreadPoolManger();
            }
        }
        return mInstance;
    }

    public void execute(HttpTask task) {
        mExecutorService.execute(task);
    }

}複製程式碼

Volley

最後建立IHttpService、RequestHolder、IHttpListener、HttpTask 等類,並將HttpTask 丟進執行緒池。這一系列操作對於呼叫者來說是不需要關心的,可以隱藏,所以需要一個 Volley 類去隱藏這個過程。程式碼也很簡單:

public class Volley {
    static HashMap<String, String> mGlobalHeader = new HashMap<>();

    public static <T, M> IHttpService sendRequest(T requestParams, String url, Class<M> responseClass, @RequestType String type,IDataListener<M> listener) {
        IHttpService jsonHttpService = new JsonHttpService();

        IHttpListener jsonDealListener = new JsonDealListener<>(listener, responseClass);

        RequestHolder<T> requestHolder = new RequestHolder<>(requestParams, jsonHttpService, jsonDealListener, url,type);

        try {
            HttpTask<T> task = new HttpTask<>(requestHolder);
            ThreadPoolManger.getInstance().execute(task);
        } catch (UnsupportedEncodingException e) {
            listener.onFail(0, e.getMessage());
        }
        return jsonHttpService;
    }


    public static void setGlobalHeader(String key, String value) {
        mGlobalHeader.put(key, value);
    }

}複製程式碼

至此,除了IHttpListener 和 IHttpService 的實現類沒有貼上來,整個網路請求架構的雛形已經完成了。
我們來測試一下~

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void test(View view) {
        String url = "http://gank.io/api/data/Android/10/1";
        for (int i = 0; i < 50; i++) {
            Volley.sendRequest(null, url, GankResponse.class, GET, new IDataListener<GankResponse>() {
                @Override
                public void onSuccess(GankResponse response) {
                    Log.e("___", "請求成功");
                }

                @Override
                public void onFail(int errorCode, String errorMsg) {
                    Log.e("___error", errorMsg + "__errorCode:" + errorCode);
                }
            });
        }
    }
}複製程式碼

以上是在 MainActivity 裡面點選了一個按鈕,做了一個50次網路請求的併發操作,親測成功。

下期預告

下期我們將基於這個架構,繼續做下載的擴充套件。

其中會涉及到多執行緒下載,斷點續傳等各種操作,如果時間來的及的話,還會結合資料庫做一些神奇的操作。

哦,對了,貼上

程式碼地址

如果我的文章能給你帶來幫助,請記得點個 star,麼麼噠~

相關文章