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
可以接收兩個參數,value
和alternate
。其中,value
是 String
類型,alternate
是Array<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's Love Clinic 1
這樣直接設置給 TextView
後,會照原樣顯示,這樣的顯示就會很難看了。如下圖所以,
解決辦法一:
使用 Html.fromHtml()
方法
tv.text = Html.fromHtml("Mr. Anthony's Love Clinic 1")
這樣以後,顯示在屏幕上就正常了:
Mr. Anthony's Love Clinic 1
如圖所示:
但是,這種辦法需要在每個給 TextView
設置文本的地方都這樣處理。
可以使用下面的擴展方法:
fun String.fromHtml(): Spanned {
return Html.fromHtml(this)
}
上面就可以寫成這樣,實現鏈式調用:
tv.text = "Mr. Anthony'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 字符串的思路,以及幾種日常開發中遇到的實際案例。希望對大家有所幫助。