Retrofit2與服務端例項講解

葉志陳發表於2018-06-02

網路上對 Retrofit2 的各種介紹文章已經很多了,不過往往只是對其用法進行介紹,而缺少相應的實踐,這一方面是因為網路上的免費API介面返回的資料格式和訪問模式(一般都只能使用 Get 模式)有限制,另一方面是因為並不是每位開發者都會寫服務端介面。這樣就造成了在學習 Retrofit2 的過程中,對某些引數的作用並不能直觀感受到,所以這裡我就嘗試著用 Nodejs 搭建了一個本地伺服器,提供了幾個介面用於支援 Get、Post 模式訪問呼叫,支援檔案上傳和檔案下載功能,返回的資料格式有 Json 物件和 Json 陣列,需要的引數格式可以由使用者來自由定義

本篇文章不會對 Retrofit2 的用法進行過多介紹,重點在於介紹服務端介面的搭建以及 Retrofit2 與服務端的互動

一、服務端

服務端介面採用的是 Nodejs,採用的 IDE 是 WebStormNodejs 版本是 10.2.0

開啟 WebStorm ,選擇新建工程,選擇 Node.js Express App 來建立一個工程

Retrofit2與服務端例項講解

建立的工程目錄如下所示,除了選中的三個檔案之外,其它都是 IDE 自動為我們構建的,upload 資料夾用於存放客戶端上傳來的檔案,resultJson.js 檔案用於統一服務端返回的資料格式,api.js 檔案用於存放編寫的介面並啟動伺服器,而我們主要需要關注的也就是 api.js 檔案

Retrofit2與服務端例項講解

1.1 、resultJson.js

這裡先介紹下 resultJson.js 檔案,其包含的全部程式碼如下所示

/**
 * 有正常結果返回時
 * @param res
 * @param data
 */
exports.onSuccess = function (res, data) {
    var result = {};
    result.code = 1;
    result.msg = 'success';
    result.data = data;
    res.json(result);
};

/**
 * 當發生錯誤時
 * @param res
 * @param code
 * @param msg
 */
exports.onError = function (res, code, msg) {
    var error = {};
    error.code = code;
    error.msg = msg;
    res.json(error);
};

/**
 * 無資料記錄
 * @param res
 */
exports.onNoRecord = function (res) {
    exports.onError(res, 1000, '無資料記錄');
};

/**
 * 引數錯誤
 * @param res
 */
exports.onParamsError = function (res) {
    exports.onError(res, 1001, '引數錯誤');
};

/**
 * 系統錯誤
 * @param res
 */
exports.onSystemError = function (res) {
    exports.onError(res, 1002, '系統錯誤');
};
複製程式碼

resultJson.js 對網路請求的各種可能結果進行了封裝,統一了服務端返回的資料格式。當有正常結果返回時,呼叫的是 onSuccess 方法,此時返回的資料格式類似於如下所示,返回碼 code 固定為 "1",,返回資訊 msg 固定為 "success",data 包含實際要返回的資料

{"code":1,"msg":"success","data":{"name":"leavesC","mobile":123456}}
複製程式碼

當傳遞給伺服器的引數錯誤時,呼叫的是 onParamsError 方法,返回的資料格式如下所示

{"code":1001,"msg":"引數錯誤"}
複製程式碼

其他非正常情況下返回的資料格式相同,僅僅是包含的返回碼和返回資訊值不同而已

1.2、api.js

api.js 檔案包含了所有介面,這裡先展示一個 Get 介面,其它介面會在使用到時陸續介紹

當中,require 函式用於載入需要的模組,就類似於 Java 中載入需要的依賴庫一樣。app.get() 表明該介面支援的是 Get 模式請求,訪問的介面路徑字尾是:“/Get/getString”,完整的訪問路徑是:http://localhost:1995/Get/getString

req 引數包含了客戶端帶來的請求引數res 引數用於寫入要向客戶端返回的資料app.listen(1995) 用於啟動伺服器,並指定在 1995 埠進行監聽

在客戶端訪問該介面時,介面會列印出客戶端帶來的所有請求引數和請求頭,以及實際生成的訪問連結

這樣,一個簡單的 Get 介面就完成了

//require 函式用於載入需要的模組
var express = require('express');
var bodyParser = require('body-parser');
var multiparty = require('multiparty');
var resultJson = require('../routes/resultJson');
var app = express();
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());

app.get('/Get/getString', function (req, res) {
    //請求的引數
    var query = req.query;
    for (var key in query) {
        console.log("引數 key is: ", key, " , value is: ", query[key]);
    }
    //請求頭
    var headers = req.headers;
    for (var key in headers) {
        console.log("頭部資訊 key is: ", key, " , value is: ", headers[key]);
    }
    //連結
    console.log("Url:", req.url);

    //如果該次訪問帶有key值為“userName”的請求頭,如果value不是“leavesC”,則認為請求的引數錯誤
    //如果不帶有key值為“userName”的請求頭,則不受影響
    //要注意,請求頭的key值會被置為小寫
    var userName = headers['username'];
    if (userName && userName !== 'leavesC') {
        return resultJson.onParamsError(res);
    }
    var data = {};
    data.name = 'leavesC';
    data.mobile = 123456;
    resultJson.onSuccess(res, data);
});

····

//啟動伺服器,並在指定的埠 1995 進行監聽
app.listen(1995);
複製程式碼

二、客戶端

客戶端使用的 IDE 是 IntelliJ IDEA,採用 Gradle 來構建工程,這樣使用起來就基本與 Android Studio 一致了

Retrofit2與服務端例項講解

引入對 Retrofit2 和 converter-gson 的支援

    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
複製程式碼

Get請求

由於我是在本地搭建的伺服器,所以用來構建 Retrofit 的 baseUrl 應該是指向本地 IP 地址

/**
 * 作者:chenZY
 * 時間:2018/5/29 18:53
 * 描述:https://github.com/leavesC
 */
public class HttpConfig {

    public static final String BASE_URL = "http://localhost:1995/";

}
複製程式碼

新建 GetService 介面用於宣告訪問上述 Get 介面的方法,各個方法包含的引數值各不一樣,根據服務端列印出的日誌資訊,就可以很容易地區分出各種方式之間的區別

/**
 * 作者:chenZY
 * 時間:2018/5/29 18:54
 * 描述:https://github.com/leavesC
 */
public interface GetService {

    //不帶任何引數的 Get 請求
    @GET("Get/getString")
    Call<ResponseBody> getNormal();

    //攜帶請求引數的 Get 請求
    @GET("Get/getString")
    Call<ResponseBody> getWithQuery(@Query("name") String name, @Query("age") int age);

    //攜帶請求引數的 Get 請求
    @GET("Get/getString")
    Call<ResponseBody> getWithMap(@QueryMap Map<String, String> map);

    //攜帶請求引數以及固定請求頭的 Get 請求
    @GET("Get/getString")
    @Headers({"userName:leavesC"})
    Call<ResponseBody> getWithQueryAndHeaders(@Query("name") String name, @Query("age") int age);

    //攜帶請求引數以及請求頭值不固定的 Get 請求
    @GET("Get/getString")
    Call<ResponseBody> getWithQueryAndHeader(@Header("userName") String userName, @Query("name") String name, @Query("age") int age);

    //將請求值作為連結一部分的 Get 請求
    @GET("Get/getString/{id}")
    Call<ResponseBody> getWithPath(@Path("id") int id);

    //將請求值作為連結一部分的 Get 請求,並使用 Gson Converter
    @GET("Get/getUser/{startId}/{number}")
    Call<ListResponse<User>> getWithGsonConverter(@Path("startId") int startId, @Path("number") int number);

}
複製程式碼

2.1、不帶任何引數

這裡看下不帶任何自定義的引數與請求頭的請求方式

//Get請求時不會帶任何自定義的引數與請求頭,訪問的連結是:/Get/getString
    private static void getNormal() {
        GetService getService = buildRetrofit().create(GetService.class);
        getService.getNormal().enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                if (response.isSuccessful()) {
                    try {
                        //返回的資料:{"code":1,"msg":"success","data":{"name":"leavesC","mobile":123456}}
                        System.out.println("onResponse body: " + response.body().string());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("onResponse code: " + response.code());
                }
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                System.out.println("onFailure: " + t.getMessage());
            }
        });
    }
複製程式碼

此時服務端列印出來的日誌資訊如下所示

頭部資訊 key is:  host  , value is:  localhost:1995
頭部資訊 key is:  connection  , value is:  Keep-Alive
頭部資訊 key is:  accept-encoding  , value is:  gzip
頭部資訊 key is:  user-agent  , value is:  okhttp/3.10.0
Url: /Get/getString
複製程式碼

客戶端獲得的資料如下所示

{"code":1,"msg":"success","data":{"name":"leavesC","mobile":123456}}
複製程式碼

2.2、帶上請求引數

如果在請求方法中帶上註解 @Query 以及對應的請求引數,則請求引數會作為訪問連結的字尾

	//Get請求時會帶上請求引數,引數將作為連結的字尾,生成的連結是:/Get/getString?name=leavesC&age=24
    private static void getWithQuery() {
        GetService getService = buildRetrofit().create(GetService.class);
        getService.getWithQuery("leavesC", 24).enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                if (response.isSuccessful()) {
                    try {
                        //返回的資料是:{"code":1,"msg":"success","data":{"name":"leavesC","mobile":123456}}
                        System.out.println("onResponse body: " + response.body().string());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("onResponse code: " + response.code());
                }
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                System.out.println("onFailure: " + t.getMessage());
            }
        });
    }
複製程式碼

此時服務端列印出來的日誌資訊如下所示

引數 key is:  name  , value is:  leavesC
引數 key is:  age  , value is:  24
頭部資訊 key is:  host  , value is:  localhost:1995
頭部資訊 key is:  connection  , value is:  Keep-Alive
頭部資訊 key is:  accept-encoding  , value is:  gzip
頭部資訊 key is:  user-agent  , value is:  okhttp/3.10.0
Url: /Get/getString?name=leavesC&age=24
複製程式碼

服務端通過 req.query 取得了客戶端帶來的引數資訊,服務端就可以按照引數資訊從資料庫中取得相應的資料,從而實現按條件索引資料

getWithMap() 方法的作用與 getWithQuery() 相同,這裡不贅述

2.3、帶上固定請求頭

getWithQueryAndHeaders() 方法則是用於攜帶請求引數以及固定請求頭的 Get 請求

//Get請求時帶上引數和請求頭資訊,引數將作為連結的字尾,生成的連結是:/Get/getString?name=leavesC&age=24
    //帶上的Header的key是:userName,value是:leavesC
    private static void getWithQueryAndHeaders() {
        GetService getService = buildRetrofit().create(GetService.class);
        getService.getWithQueryAndHeaders("leavesC", 24).enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                if (response.isSuccessful()) {
                    try {
                        //返回的資料是:{"code":1,"msg":"success","data":{"name":"leavesC","mobile":123456}}
                        System.out.println("onResponse body: " + response.body().string());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("onResponse code: " + response.code());
                }
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                System.out.println("onFailure: " + t.getMessage());
            }
        });
    }
複製程式碼

此時服務端列印出來的日誌資訊如下所示,可以看到頭部資訊相比之前多出了 username,且值正是在註解中所宣告的

引數 key is:  name  , value is:  leavesC
引數 key is:  age  , value is:  24
頭部資訊 key is:  username  , value is:  leavesC2
頭部資訊 key is:  host  , value is:  localhost:1995
頭部資訊 key is:  connection  , value is:  Keep-Alive
頭部資訊 key is:  accept-encoding  , value is:  gzip
頭部資訊 key is:  user-agent  , value is:  okhttp/3.10.0
Url: /Get/getString?name=leavesC&age=24
複製程式碼

頭部資訊可用於驗證訪問來源,即對客戶端的身份資訊進行驗證

在服務端我對 key 值為 userName 的頭部資訊的 value 值進行了判斷,如果客戶端包含 key 值為 userName 的頭部資訊,而其值不是 leavesC ,則返回的 Json 資料就會提示引數錯誤

修改 getWithQueryAndHeaders() 方法帶有的頭部資訊的值

/**
 * 作者:chenZY
 * 時間:2018/5/29 18:54
 * 描述:
 */
public interface GetService {

    //攜帶請求引數以及固定請求頭的 Get 請求
    @GET("Get/getString")
    @Headers({"userName:leavesC_2"})
    Call<ResponseBody> getWithQueryAndHeaders(@Query("name") String name, @Query("age") int age);

}
複製程式碼

此時服務端返回的資料將是

{"code":1001,"msg":"引數錯誤"}
複製程式碼

2.4、帶上非固定值的請求頭

用於標記非固定值請求頭的註解 @Header 作用於方法引數,從而實現請求頭的動態賦值

//Get請求時帶上引數和非固定值的請求頭,引數將作為連結的字尾,生成的連結是:/Get/getString?name=leavesC&age=24
    //帶上的Header的key是:userName,value是:Hi
    private static void getWithQueryAndHeader() {
        GetService getService = buildRetrofit().create(GetService.class);
        getService.getWithQueryAndHeader("Hi", "leavesC", 24).enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                if (response.isSuccessful()) {
                    try {
                        //返回的資料是:{"code":1,"msg":"success","data":{"name":"leavesC","mobile":123456}}
                        System.out.println("onResponse body: " + response.body().string());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("onResponse code: " + response.code());
                }
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                System.out.println("onFailure: " + t.getMessage());
            }
        });
    }

複製程式碼

服務端列印出來的日誌如下所示,和採用 @Headers 註解的方法區別不大,只是一個值是固定的,一個在執行時動態賦值

引數 key is:  name  , value is:  leavesC
引數 key is:  age  , value is:  24
頭部資訊 key is:  username  , value is:  Hi
頭部資訊 key is:  host  , value is:  localhost:1995
頭部資訊 key is:  connection  , value is:  Keep-Alive
頭部資訊 key is:  accept-encoding  , value is:  gzip
頭部資訊 key is:  user-agent  , value is:  okhttp/3.10.0
Url: /Get/getString?name=leavesC&age=24
複製程式碼

2.5、指定訪問路徑

還有一種在連結中加上訪問引數的方式,即將訪問引數做為連結實際的一部分

對應的客戶端方法是

    @GET("Get/getString/{id}")
    Call<ResponseBody> getWithPath(@Path("id") int id);
複製程式碼

此時需要在服務端再寫一個 Get 介面,介面路徑 “/Get/getString/:id” 中的 “:id” 的意思是:只有客戶端在訪問介面時明確帶上了引數值(不用宣告Key),才會進入到此介面的回撥函式裡

app.get('/Get/getString/:id', function (req, res) {
    //請求的引數
    var query = req.query;
    for (var key in query) {
        console.log("引數 key is: ", key, " , value is: ", query[key]);
    }
    //請求頭
    var headers = req.headers;
    for (var key in headers) {
        console.log("頭部資訊 key is: ", key, " , value is: ", headers[key]);
    }
    //連結
    console.log("Url:", req.url);

    var id = req.params.id;
    if (id <= 0) {
        resultJson.onParamsError(res);
    } else {
        var data = {};
        data.name = 'leavesC_' + id;
        data.mobile = 123456;
        resultJson.onSuccess(res, data);
    }
});
複製程式碼

客戶端來訪問該介面

	//生成的連結是:/Get/getString/22
    private static void getWithPath() {
        GetService getService = buildRetrofit().create(GetService.class);
        getService.getWithPath(22).enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                if (response.isSuccessful()) {
                    try {
                        //返回的資料: {"code":1,"msg":"success","data":{"name":"leavesC_22","mobile":123456}}
                        System.out.println("onResponse body: " + response.body().string());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("onResponse code: " + response.code());
                }
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                System.out.println("onFailure: " + t.getMessage());
            }
        });
    }
複製程式碼

服務端列印出的日誌資訊如下所示

頭部資訊 key is:  host  , value is:  localhost:1995
頭部資訊 key is:  connection  , value is:  Keep-Alive
頭部資訊 key is:  accept-encoding  , value is:  gzip
頭部資訊 key is:  user-agent  , value is:  okhttp/3.10.0
Url: /Get/getString/22
複製程式碼

2.6、獲取Json陣列

之前的幾種請求方式獲取到的都是 Json 物件,此處來寫一個返回的資料格式是 Josn 陣列的介面,每個 Json 物件對應的是如下的 Java Bean

/**
 * 作者:chenZY
 * 時間:2018/5/26 15:13
 * 描述:
 */
public class User {

    private String name;

    private String mobile;

    public User(String name, String mobile) {
        this.name = name;
        this.mobile = mobile;
    }
    
	···
	
}
複製程式碼

服務端介面如下所示,用於獲取起始 ID 為 startIdnumber 位使用者的使用者資訊

app.get('/Get/getUser/:startId/:number', function (req, res) {
    //請求的引數
    var query = req.query;
    for (var key in query) {
        console.log("引數 key is: ", key, " , value is: ", query[key]);
    }
    //請求頭
    var headers = req.headers;
    for (var key in headers) {
        console.log("頭部資訊 key is: ", key, " , value is: ", headers[key]);
    }
    //連結
    console.log("Url:", req.url);

    //為了防止客戶端帶來的引數是非數值型別,所以此處需要對其型別進行判斷
    var startId = parseInt(req.params.startId);
    var number = parseInt(req.params.number);
    console.log("startId: ", startId);
    console.log("number: ", number);
    if (!isNaN(startId) && !isNaN(number) && startId > 0 && number > 0) {
        var items = [];
        for (var index = 0; index < number; index++) {
            var item = {};
            item.name = 'leavesC_' + (startId + index);
            item.mobile = 123456;
            items.push(item);
        }
        resultJson.onSuccess(res, items);
    } else {
        resultJson.onParamsError(res);
    }
});
複製程式碼

客戶端使用 converter-gson 來對服務端返回的 Json 陣列進行自動解析,由於 resultJson.js 檔案統一了服務端返回的資料格式,為了不每次都寫 codemsg 這兩個引數,此時可以採用泛型來進行封裝

/**
 * 作者:chenZY
 * 時間:2018/5/26 15:10
 * 描述:
 */
public class Response {

    private int code;

    private String msg;

    ···
    
}
複製程式碼

如果服務端返回的 data 是 Json 物件,則使用 EntityResponse,通過泛型傳入實際的 Java Bean

/**
 * 作者:chenZY
 * 時間:2018/5/26 15:11
 * 描述:
 */
public class EntityResponse<T> extends Response {

    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

複製程式碼

如果服務端返回的 data 是 Json 陣列,則使用 ListResponse,通過泛型傳入實際的 Java Bean

/**
 * 作者:chenZY
 * 時間:2018/5/26 15:12
 * 描述:
 */
public class ListResponse<T> extends Response {

    private List<T> data;

    public List<T> getData() {
        return data;
    }

    public void setData(List<T> data) {
        this.data = data;
    }

}
複製程式碼

此時在回撥函式中就可以直接獲取到包含在 List 中的資料了

private static void getWithGsonConverter() {
        GetService getService = buildRetrofit().create(GetService.class);
        getService.getWithGsonConverter(24, 4).enqueue(new Callback<ListResponse<User>>() {
            @Override
            public void onResponse(Call<ListResponse<User>> call, Response<ListResponse<User>> response) {
                if (response.isSuccessful()) {
                    List<User> userList = response.body().getData();
                    if (userList == null) {
                        System.out.println("onResponse: userList == null");
                    } else {
                        for (User user : userList) {
                            System.out.println("onResponse: " + user);
                        }
                    }
                } else {
                    System.out.println("onResponse code: " + response.code());
                }
            }

            @Override
            public void onFailure(Call<ListResponse<User>> call, Throwable t) {
                System.out.println("onFailure: " + t.getMessage());
            }
        });
    }
複製程式碼

客戶端列印出來的日誌如下所示

onResponse: User{name='leavesC_24', mobile='123456'}
onResponse: User{name='leavesC_25', mobile='123456'}
onResponse: User{name='leavesC_26', mobile='123456'}
onResponse: User{name='leavesC_27', mobile='123456'}
複製程式碼

Post請求

服務端 Post 介面的寫法與 Get 介面類似,主要的區別在於客戶端 Post 的引數獲取方式

app.post('/Post/postUser', function (req, res) {
    var body = req.body;
    for (var key in body) {
        console.log("body 引數 key is: ", key, " , value is: ", body[key]);
    }
    //請求頭
    var headers = req.headers;
    for (var key in headers) {
        console.log("headers 頭部資訊 key is: ", key, " , value is: ", headers[key]);
    }
    //連結
    console.log("Url:", req.url);

    var data = {};
    data.name = 'leavesC';
    data.mobile = 123456;
    resultJson.onSuccess(res, data);
});
複製程式碼

客戶端新建 PostService 介面用於宣告訪問 Post 介面的方法,各個方法包含的引數值各不一樣,根據服務端列印出的日誌資訊來區分出各種方式之間的區別

@FormUrlEncoded 註解表示請求頭是一個 Form 表單,對應的是客戶端訪問介面時 key 值為 “content-type” 的請求頭值

/**
 * 作者:chenZY
 * 時間:2018/5/29 18:54
 * 描述:https://github.com/leavesC
 */
public interface PostService {

    @FormUrlEncoded
    @POST("Post/postUser")
    Call<ResponseBody> postWithField(@Field("name") String name, @Field("mobile") String mobile);

    @FormUrlEncoded
    @POST("Post/postUser")
    Call<ResponseBody> postWithFieldMap(@FieldMap Map<String, String> map);

    @POST("Post/postUser")
    Call<ResponseBody> postWithBody(@Body User user);

}
複製程式碼
private static void postWithField() {
        PostService postService = buildRetrofit().create(PostService.class);
        postService.postWithField("czy", "123456").enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                if (response.isSuccessful()) {
                    try {
                        //返回的資料:{"code":1,"msg":"success","data":{"name":"leavesC","mobile":123456}}
                        System.out.println("onResponse body: " + response.body().string());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("onResponse code: " + response.code());
                }
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                System.out.println("onFailure: " + t.getMessage());
            }
        });
    }
複製程式碼

服務端列印出來日誌如下所示,可以看到客戶端攜帶過去的引數值,此外,頭部資訊 "content-type" 的值即對應客戶端介面方法的 @FormUrlEncoded 註解

body 引數 key is:  name  , value is:  czy
body 引數 key is:  mobile  , value is:  123456
headers 頭部資訊 key is:  content-type  , value is:  application/x-www-form-urlencoded
headers 頭部資訊 key is:  content-length  , value is:  22
headers 頭部資訊 key is:  host  , value is:  localhost:1995
headers 頭部資訊 key is:  connection  , value is:  Keep-Alive
headers 頭部資訊 key is:  accept-encoding  , value is:  gzip
headers 頭部資訊 key is:  user-agent  , value is:  okhttp/3.10.0
Url: /Post/postUser
複製程式碼

通過 @FieldMap@Body 註解的方式來傳遞引數的方式與 @Field 相同,Retrofit 會遍歷引數包含的所有欄位,以此來生成要傳遞的引數,這裡就不再贅述

上傳檔案

上傳檔案時攜帶引數

這裡來模擬客戶端上傳圖片到服務端的操作,同時攜帶引數值

app.post('/uploadPhoto', function (req, res) {
    var body = req.body;
    for (var key in body) {
        console.log("body 引數 key is: ", key, " , value is: ", body[key]);
    }
    //請求頭
    var headers = req.headers;
    for (var key in headers) {
        console.log("headers 頭部資訊 key is: ", key, " , value is: ", headers[key]);
    }
    //連結
    console.log("Url:", req.url);

    //生成multiparty物件,並配置上傳目標路徑
    var form = new multiparty.Form({uploadDir: '../public/upload/'});
    //fields 包含了傳遞來了的引數值
    //files 則代表上傳到服務端的檔案物件
    //此處會在後臺自動將客戶端傳來的檔案儲存到指定資料夾下,處理結果通過回撥函式進行通知
    form.parse(req, function (err, fields, files) {
        if (err) {
            resultJson.onSystemError(res);
        } else {
            console.log("fields : ", fields);
            console.log("files : ", files);
            var filesContent = files['photo'][0];
            var data = {};
            data.filePath = filesContent.path;
            resultJson.onSuccess(res, data);
        }
    });
});
複製程式碼

客戶端新建 UploadService 介面用於宣告上傳檔案的方法,@Multipart 註解表示請求體是一個支援檔案上傳的 Form 表單,對應的是客戶端訪問介面時 key 值為 “content-type” 的請求頭

此外,在方法引數中使用到了三個 @Part 註解 ,第一個用於註解要上傳的檔案物件,剩下兩個用於標明在上傳檔案的同時要攜帶的請求引數

/**
 * 作者:chenZY
 * 時間:2018/5/29 18:55
 * 描述:
 */
public interface UploadService {

    @Multipart
    @POST("uploadPhoto")
    Call<ResponseBody> uploadPhoto(@Part MultipartBody.Part photo, @Part("userName") RequestBody username, @Part("password") RequestBody password);

}
複製程式碼

圖片放在工程的 resources 資料夾下

private static void uploadPhoto() {
        UploadService uploadService = buildRetrofit().create(UploadService.class);
        File file = new File("..\\JavaRetrofit\\src\\main\\resources\\images\\lufei.jpg");
        RequestBody photoRequestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
        //設定Content-Disposition:form-data; name="photo"; filename="lufei.jpg"
        MultipartBody.Part photo = MultipartBody.Part.createFormData("photo", file.getName(), photoRequestBody);
        RequestBody userName = RequestBody.create(MediaType.parse("text/plain"), "leavesC");
        RequestBody password = RequestBody.create(MediaType.parse("text/plain"), "123456");
        uploadService.uploadPhoto(photo, userName, password).enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                if (response.isSuccessful()) {
                    try {
                        System.out.println("onResponse body: " + response.body().string());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("onResponse code: " + response.code());
                }
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                System.out.println("onFailure: " + t.getMessage());
            }
        });
    }
複製程式碼

執行上傳檔案的程式碼後,服務端輸出的日誌資訊如下所示

headers 頭部資訊 key is:  content-type  , value is:  multipart/form-data; boundary=3b8bf455-620a-4250-8f3d-8079df43d090
headers 頭部資訊 key is:  content-length  , value is:  224722
headers 頭部資訊 key is:  host  , value is:  localhost:1995
headers 頭部資訊 key is:  connection  , value is:  Keep-Alive
headers 頭部資訊 key is:  accept-encoding  , value is:  gzip
headers 頭部資訊 key is:  user-agent  , value is:  okhttp/3.10.0
Url: /uploadPhoto
fields :  { userName: [ 'leavesC' ], password: [ '123456' ] }
files :  { photo:
   [ { fieldName: 'photo',
       originalFilename: 'lufei.jpg',
       path: '..\\public\\upload\\eKPBTufrJs24ybaoOA2HQ3Aj.jpg',
       headers: [Object],
       size: 224115 } ] }
複製程式碼

服務端返回的資料如下所示

{"code":1,"msg":"success","data":{"filePath":"..\\public\\upload\\lfCMVA2VXLNN8XaRmpl-9nE7.jpg"}}
複製程式碼

此時可以看到服務端工程的 upload 資料夾中多出了一張隨機命名的圖片

Retrofit2與服務端例項講解

多檔案上傳

這裡來實現多個檔案同時上傳

由於此處客戶端在實現多檔案上傳時使用了不同的引數配置,所以服務端需要採用不同的資料解析方式,因為新開了一個介面

app.post('/uploadFileDouble', function (req, res) {
    var body = req.body;
    for (var key in body) {
        console.log("body 引數 key is: ", key, " , value is: ", body[key]);
    }
    //請求頭
    var headers = req.headers;
    for (var key in headers) {
        console.log("headers 頭部資訊 key is: ", key, " , value is: ", headers[key]);
    }
    //連結
    console.log("Url:", req.url);

    //生成multiparty物件,並配置上傳目標路徑
    var form = new multiparty.Form({uploadDir: '../public/upload/'});
    //fields 包含了傳遞來了的引數值
    //files 則代表上傳到服務端的檔案物件
    //此處會在後臺自動將客戶端傳來的檔案儲存到指定資料夾下,處理結果通過回撥函式進行通知
    form.parse(req, function (err, fields, files) {
        if (err) {
            resultJson.onSystemError(res);
        } else {
            console.log("fields : ", fields);
            console.log("files : ", files);
            var filesContent = files['photos'];
            var items = [];
            for (var index in filesContent) {
                var item = {};
                item.filePath = filesContent[index].path;
                items.push(item);
            }
            resultJson.onSuccess(res, items);
        }
    });
});
複製程式碼

客戶端上傳多檔案的介面方法使用 @PartMap 註解進行標記,使用 Map 容納多個需要上傳的檔案表單

/**
 * 作者:chenZY
 * 時間:2018/5/29 18:55
 * 描述:
 */
public interface UploadService {

    @Multipart
    @POST("uploadFileDouble")
    Call<ResponseBody> uploadFileDouble(@PartMap Map<String, RequestBody> files);

}
複製程式碼
private static void uploadFileDouble() {
        UploadService uploadService = buildRetrofit().create(UploadService.class);
        Map<String, RequestBody> photoMap = new HashMap<>();

        File file = new File("..\\JavaRetrofit\\src\\main\\resources\\images\\lufei.jpg");
        RequestBody photoRequestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
        photoMap.put("photos\"; filename=\"" + file.getName(), photoRequestBody);

        file = new File("..\\JavaRetrofit\\src\\main\\resources\\images\\mingren.jpg");
        photoRequestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
        photoMap.put("photos\"; filename=\"" + file.getName(), photoRequestBody);

        uploadService.uploadFileDouble(photoMap).enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                if (response.isSuccessful()) {
                    try {
                        System.out.println("onResponse body: " + response.body().string());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("onResponse code: " + response.code());
                }
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                System.out.println("onFailure: " + t.getMessage());
            }
        });
    }
複製程式碼

執行程式,可以看到 upload 資料夾下多出了兩張不同的圖片,服務端輸出的日誌如下所示

headers 頭部資訊 key is:  content-type  , value is:  multipart/form-data; boundary=5c3fcbbb-dd78-4854-ad12-3c4ae3fd1f02
headers 頭部資訊 key is:  content-length  , value is:  347838
headers 頭部資訊 key is:  host  , value is:  localhost:1995
headers 頭部資訊 key is:  connection  , value is:  Keep-Alive
headers 頭部資訊 key is:  accept-encoding  , value is:  gzip
headers 頭部資訊 key is:  user-agent  , value is:  okhttp/3.10.0
Url: /uploadFileDouble
fields :  {}
files :  { photos:
   [ { fieldName: 'photos',
       originalFilename: 'mingren.jpg',
       path: '..\\public\\upload\\HsvSfjgKtLL3gAqwrxRFk5G-.jpg',
       headers: [Object],
       size: 123255 },
     { fieldName: 'photos',
       originalFilename: 'lufei.jpg',
       path: '..\\public\\upload\\bicNIvOD3ZcBe8EgqmSd9SFf.jpg',
       headers: [Object],
       size: 224115 } ] }

複製程式碼

客戶端接收到的資料如下所示

{"code":1,"msg":"success","data":[{"filePath":"..\\public\\upload\\HsvSfjgKtLL3gAqwrxRFk5G-.jpg"},{"filePath":"..\\public\\upload\\bicNIvOD3ZcBe8EgqmSd9SFf.jpg"}]}
複製程式碼

下載檔案

express 對檔案的下載操作進行了高度封裝,所以伺服器對外提供檔案下載功能的方法可能要比你想的簡單得多

此處直接將待下載的檔案指向了 uplaod 資料夾中的一張圖片

app.get('/downloadFile', function (req, res) {
    //檔案的儲存路徑
    var filePath = '../public/upload/Anoj-VQ-cd_vkw9_O5ErSSG6.jpg';
    //設定檔案下載時顯示的檔名,如不設定則使用原始的檔名
    var fileName = 'leavesC.jpg';
    res.download(filePath, fileName);
});
複製程式碼

客戶端新建 DownloadService 用於宣告提供下載功能的方法。為了支援大檔案下載,此處使用了 @Streaming 註解,避免了將整個檔案讀取進記憶體裡從而在 Android 系統中造成 OOM

/**
 * 作者:chenZY
 * 時間:2018/5/30 13:54
 * 描述:
 */
public interface DownloadService {

    @Streaming
    @GET
    Call<ResponseBody> downloadFile(@Url String fileUrl);

}
複製程式碼

可以看到,此處將下載來的檔案直接寫到了電腦桌面上,使用的檔案讀寫方法是由 okIo 包提供的

private static void downloadFile() {
        DownloadService downloadService = buildRetrofit().create(DownloadService.class);
        Call<ResponseBody> call = downloadService.downloadFile("downloadFile");
        call.enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                if (response.isSuccessful()) {
                    BufferedSink sink = null;
                    try {
                        File file = new File("C:\\Users\\CZY\\Desktop\\Hi.jpg");
                        sink = Okio.buffer(Okio.sink(file));
                        sink.writeAll(response.body().source());
                        System.out.println("onResponse : success");
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        try {
                            if (sink != null) {
                                sink.close();
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                } else {
                    System.out.println("onResponse code: " + response.code());
                }
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                System.out.println("onFailure: " + t.getMessage());
            }
        });
    }
複製程式碼

此外,上述程式碼如果在 Android 系統中執行還有個問題,由於回撥函式 Callback 是在主執行緒中回撥的,所以如果直接在回撥函式中進行長時間的 IO 讀寫操作,可能會造成 ANR,此處需要注意

Retrofit2 與 服務端之間的例項講解到這裡也就結束了,此處除了提供客戶端的原始碼外,我也將服務端整個工程打包在了一起,歡迎下載

我的 GitHub 主頁:leavesC -> https://github.com/leavesC

專案主頁:Retrofit2Samples -> https://github.com/leavesC/Retrofit2Samples

相關文章