Android 健壯地處理後臺返回的數據

1. 前言

最近工作中,和後臺交互數據比較多,遇到一些後臺返回數據不規範或者是錯誤,但是 leader 要客戶端來處理。在這裏記錄一下解決問題的過程,希望對大家有所幫助。

2. 正文

2.1 返回 json 字段不統一

接口文檔中的格式如下:

{
	"result": "結果數據",
	"status": 1,
	"message": "success"
}

這時在代碼中對應的數據解析類是:

data class Response (
        val message: String,
        val result: String,
        val status: Int
)

但是,後臺返回的數據卻是這樣的:

{
	"data": "結果數據",
	"code": 1,
	"info": "success"
}

還有這樣的:

{
	"datas": "結果數據",
	"state": 1,
	"info": "success"
}

如果還使用上面的 Response來解析,那麼三個字段都無法被賦值,這肯定出問題的。

如果是使用 gson 來解析的話,可以把 Response 修改成下面這樣,就可以解決問題:

data class Response (
        @SerializedName(value = "message", alternate = ["info"])
        val message: String,
        @SerializedName(value = "result", alternate = ["data"])
        val result: String,
        @SerializedName(value = "status", alternate = ["code", "state"])
        val status: Int
) 

上面我們使用了 gson 中的 @SerializedName 註解來解決了問題。

我們來進一步瞭解一下 @SerializedName的用法。

這個註解可以解決解析類的字段與 json 中的字段不匹配的問題,比如後臺可能使用下劃線的字段命名方式,而代碼裏的字段一般會採用駝峯命名,

{
	"curr_page": 1,
	"total_page": 20
}

直接轉爲數據解析類是:

data class DataBean(
	    val curr_page: Int,
	    val total_page: Int
)

而我們希望的數據解析類是這樣的:

data class DataBean(
	    val currPage: Int,
	    val totalPage: Int
)

但是,直接寫成上面的樣子,是解析不到數據的,測試代碼如下:

fun main() {
    val gson = Gson()
    val dataBean = gson.fromJson("{\"curr_page\":1,\"total_page\":20}", DataBean::class.java)
    println(dataBean) // 打印結果:DataBean(currPage=0, totalPage=0)
}

這時就要用到 @SerializedName 註解:

data class DataBean(
        @SerializedName("curr_page")
        val currPage: Int,
        @SerializedName("total_page")
        val totalPage: Int
)

再次運行測試代碼,可以實現正常的反序列化,打印結果如下:

DataBean(currPage=1, totalPage=20)

實際上,@SerializedName 可以接收兩個參數,valuealternate。其中,valueString 類型,alternateArray<String>類型。例如,本小節開頭的 @SerializedName(value = "message", alternate = ["info"])message 賦值給了 value 參數,["info"] 賦值給了 alternate 參數,這裏使用了Kotlin 中的命名參數的寫法,這可以增加代碼的可讀性。

@SerializedName("curr_page") 中的 curr_page 是給 value參數賦值的。

alternate 參數的作用是把 json 轉爲對象的反序列過程中,給 json 中的屬性提供備選。我們通過本小節開頭的例子來說明:

fun main() {
    val gson = Gson()
    val normalJson = "{\"result\":\"結果數據\",\"status\":1,\"message\":\"success\"}"
    val response = gson.fromJson(normalJson, Response::class.java)
    println(response) // 打印結果:Response(message=success, result=結果數據, status=1)
    val json1 = "{\"data\":\"結果數據\",\"code\":-1,\"info\":\"error\"}"
    val response1 = gson.fromJson(json1, Response::class.java)
    println(response1) // 打印結果:Response(message=error, result=結果數據, status=-1)
}

可以看到,即便後臺使用了多個不同的字段名,通過 alternate 參數指定它們,一樣可以把它們統一映射爲數據類中的一個字段。

那麼,有同學可能會問:既然有了 alternate 參數,爲什麼還要 value 參數呢?

value 參數用於指定序列化的字段的名字。我們還是接着上面的例子來說明:

    // 序列化 response 和 response1
    val responseToJson = gson.toJson(response)
    println(responseToJson)
    val response1ToJson = gson.toJson(response1)
    println(response1ToJson)

打印結果如下:

{"message":"success","result":"結果數據","status":1}
{"message":"error","result":"結果數據","status":-1}

可以看到,序列化的名字由 value 參數來規定。

2.2 返回 json 數據中包含轉義字符

返回的數據包含轉義字符:

Mr. Anthony&#39;s Love Clinic 1

這樣直接設置給 TextView 後,會照原樣顯示,這樣的顯示就會很難看了。如下圖所以,

解決辦法一:

使用 Html.fromHtml() 方法

tv.text = Html.fromHtml("Mr. Anthony&#39;s Love Clinic 1")

這樣以後,顯示在屏幕上就正常了:

Mr. Anthony's Love Clinic 1

如圖所示:

但是,這種辦法需要在每個給 TextView 設置文本的地方都這樣處理。

可以使用下面的擴展方法:

fun String.fromHtml(): Spanned {
    return Html.fromHtml(this)
}

上面就可以寫成這樣,實現鏈式調用:

tv.text = "Mr. Anthony&#39;s Love Clinic 1".fromHtml()

解決辦法二

添加依賴:

implementation group: 'org.apache.commons', name: 'commons-text', version: '1.3'

使用 StringEscapeUtils.unescapeHtml4(s) 進行反轉義。

這種方法可以在攔截器裏先拿到返回的 json 數據,再使用上面的方法反轉義。

但是,我們客戶端不得不添加這個依賴,會略微增加包體的大小。

另外,關於如何在攔截器裏拿到返回的 json 數據,會在下邊進行說明。

2.3 返回 json 數據中包含雙引號(")

這種情況不做處理,直接去解析的話,無法解析成功。因爲這不是合法的 json。

請看不合法的示例,這裏的截圖是把 json 放在 bejson 網站裏進行格式化校驗的:

本來應該服務端處理掉這個問題,但是那邊想讓客戶端處理掉。

我的想法是這樣的:

  • 先在一個地方獲取到服務端返回的那一串 json;
  • 然後查找到那些導致不合法的雙引號(")並把它們替換成單引號(')。

關鍵是第一步:先在一個地方獲取到服務端返回的那一串 json

首先想到的是在 GsonConverterFactory.create()create方法中傳入一個自己構建的 Gson 對象;希望在構建這個 Gson 對象時,可以拿到後臺返回的 json 串。但是,這裏有些麻煩。

然後,想到的是攔截器 Interceptor,希望在攔截器裏拿到後端返回的 json 數據。

但是 Interceptor 是一個包含了 intercept 方法以及 Chain 接口的接口,真正的實現是要自己去完成的。那麼,如何去定義一個攔截器呢?

這裏,我們去查看一下 okhttp 給我們提供的 Interceptor 實現類,借鑑其實現思路。

大家使用過 HttpLoggingInterceptor 這個攔截器吧,它的作用就是打印請求和響應信息的。

先創建一個 HttpLoggingInterceptor 對象:

private val logInterceptor = HttpLoggingInterceptor {
    Timber.d(it) // 使用 Timber 日誌類來打印
}

logInterceptor 添加到 OkHttpClient.Builder() 中:

OkHttpClient.Builder()
.addInterceptor(logInterceptor)

查看一下打印信息:

我在圖中作了比較詳細的標註,希望同學們明白的是HttpLoggingInterceptor確實具備提供 json 信息的能力。

所以,我們可以借鑑 HttpLoggingInterceptor 的實現方式來實現自己的攔截器也具備獲取 json 信息的能力。

打開 HttpLoggingInterceptor的源碼,不難定位到打印json信息的代碼如下:

if (contentLength != 0) {
          logger.log("");
          logger.log(buffer.clone().readString(charset)); // 這行就是得到 json 信息的代碼
 }

現在我們開始自定義攔截器,創建一個實現了 Interceptor 接口的 MyInterceptor 類,並重寫 intercept 方法:

public class MyInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        return null;
    }
}

把我們在HttpLoggingInterceptor找到的獲取 json 信息的代碼buffer.clone().readString(charset),添加到自定義攔截器的 intercept方法中:

public class MyInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        String json = buffer.clone().readString(charset);
        return null;
    }
}

但是,目前我們沒有 buffer 變量和 charset 變量,所以上面的兩處變量是報錯的。

接着,我們參照 HttpLoggingInterceptor 的代碼,把獲取上面兩個變量的代碼,以及其他需要的代碼,都添加到自定義的攔截器裏面。不要忘了,應該 return 的是 response

最終得到的 MyInterceptor 是這樣的:

public class MyInterceptor implements Interceptor {
    private static final Charset UTF8 = Charset.forName("UTF-8");

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        Response response;
        try {
            response = chain.proceed(request);
        } catch (Exception e) {
            throw e;
        }
        Headers headers = response.headers();
        ResponseBody responseBody = response.body();

        BufferedSource source = responseBody.source();
        source.request(Long.MAX_VALUE); // Buffer the entire body.
        Buffer buffer = source.buffer();

        if ("gzip".equalsIgnoreCase(headers.get("Content-Encoding"))) {
            GzipSource gzippedResponseBody = null;
            try {
                gzippedResponseBody = new GzipSource(buffer.clone());
                buffer = new Buffer();
                buffer.writeAll(gzippedResponseBody);
            } finally {
                if (gzippedResponseBody != null) {
                    gzippedResponseBody.close();
                }
            }
        }

        Charset charset = UTF8;
        MediaType contentType = responseBody.contentType();
        if (contentType != null) {
            charset = contentType.charset(UTF8);
        }
        String json = buffer.clone().readString(charset);
        Timber.d(json); // 打印獲取到的 json 信息。
        return response;
    }
}

現在把我們自定義的攔截器添加給OkHttpClient

OkHttpClient.Builder()
                .addInterceptor(MyInterceptor())

運行應用,獲取打印信息如下圖:
與上圖綠框中的內容進行比較:它們是完全一樣的。

好了,第一步已經完成了。

接着是第二步,查找到那些導致不合法的雙引號(")並把它們替換成單引號('

public static String toJsonString(String s) {
    char[] tempArr = s.toCharArray();
    int tempLength = tempArr.length;
    for (int i = 0; i < tempLength; i++) {
        if (tempArr[i] == ':' && tempArr[i + 1] == '"') {
            for (int j = i + 2; j < tempLength; j++) {
                if (tempArr[j] == '"') {
                    if (tempArr[j + 1] != ',' && tempArr[j + 1] != '}') {
                        tempArr[j] = '\''; // 將value中的 雙引號替換爲單引號
                    } else if (tempArr[j + 1] == ',' || tempArr[j + 1] == '}') {
                        break;
                    }
                }
            }
        }
    }
    return new String(tempArr);
}

創建一個新的 Response 對象並返回:

// 創建一個新的response 對象並返回
MediaType type = response.body().contentType();
ResponseBody newRepsoneBody = ResponseBody.create(type, newJson);
Response newResponse = response.newBuilder().body(newRepsoneBody).build();

完整的自定義攔截器 MyInterceptor 代碼如下:

public class MyInterceptor implements Interceptor {
    private static final Charset UTF8 = Charset.forName("UTF-8");

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        Response response;
        try {
            response = chain.proceed(request);
        } catch (Exception e) {
            throw e;
        }
        Headers headers = response.headers();
        ResponseBody responseBody = response.body();

        BufferedSource source = responseBody.source();
        source.request(Long.MAX_VALUE); // Buffer the entire body.
        Buffer buffer = source.buffer();

        if ("gzip".equalsIgnoreCase(headers.get("Content-Encoding"))) {
            GzipSource gzippedResponseBody = null;
            try {
                gzippedResponseBody = new GzipSource(buffer.clone());
                buffer = new Buffer();
                buffer.writeAll(gzippedResponseBody);
            } finally {
                if (gzippedResponseBody != null) {
                    gzippedResponseBody.close();
                }
            }
        }

        Charset charset = UTF8;
        MediaType contentType = responseBody.contentType();
        if (contentType != null) {
            charset = contentType.charset(UTF8);
        }
        String json = buffer.clone().readString(charset);
        Timber.d(json);
        String newJson = Utils.toJsonString(json);
        // 創建一個新的response 對象並返回
        MediaType type = response.body().contentType();
        ResponseBody newRepsoneBody = ResponseBody.create(type, newJson);
        Response newResponse = response.newBuilder().body(newRepsoneBody).build();
        return newResponse;
    }
}

再次運行程序,可以正常運行。

2.4 返回 json 數據格式不對

期望的 json 數據是:

{
	"result": {
		"name": "All Around Weekly"
	},
	"status": 1,
	"message": "success"
}

實際得到的卻是這樣的:

{
	"result": {
		"name": "All Around Weekly",
		"status": 1,
		"message": "success"
	}
}

這種問題,仍然可以用 2.3 節中自定義攔截器的思路來處理。
拿到 json 字符串後,作如下處理:

try {
    JSONObject jsonObject = new JSONObject(json);
    JSONObject result = jsonObject.getJSONObject("result");
    if (result != null) {
        if (result.has("status")) {
            jsonObject.put("status", result.get("status"));
            result.remove("status");
        }
        if (result.has("message")) {
            jsonObject.put("message", result.get("message"));
            result.remove("message");
        }
    }
    System.out.println(jsonObject.toString());
} catch (JSONException e) {
    e.printStackTrace();
}

3. 最後

本文重點介紹了參考源碼來自定義攔截器獲取 json 字符串的思路,以及幾種日常開發中遇到的實際案例。希望對大家有所幫助。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章