前言
Retrofit 是目前主流的網(wǎng)絡(luò)請求框架,不少用過的小伙伴會遇到這樣的問題嚼摩,絕大部分接口測試都正常久脯,就個別接口尤其是返回失敗信息時報了個奇怪的錯誤信息,而看了自己的代碼邏輯也沒什么問題挂捅。別的接口都是一樣的寫,卻沒出現(xiàn)這樣的情況堂湖,可是后臺人員看了也說不關(guān)他們的事闲先。剛遇到時會比較懵,有些人不知道什么原因也就無從下手无蜂。
問題原因
排查問題也很簡單伺糠,把信息百度一下,會發(fā)現(xiàn)是解析異常酱讶。那就先看下后臺返回了什么退盯,用 PostMan 請求一下查看返回結(jié)果彼乌,發(fā)現(xiàn)是類似下面這樣的:
{
"code": 500,
"msg": "登錄失敗",
"data": ""
}
也可能是這樣的:
{
"code": 500,
"msg": "登錄失敗",
"data": 0
}
或者是這樣的:
{
"code": 500,
"msg": "登錄失敗",
"data": []
}
仔細(xì)觀察后突然恍然大悟泻肯,這不是坑爹嗎渊迁?后臺這樣返回解析肯定有問題呀,我要將 data 解析成一個對象灶挟,而后臺返回的是一個空字符串琉朽、整形或空數(shù)組,肯定解析報錯稚铣。
嗯箱叁,這就是后臺的問題,是后臺寫得不“規(guī)范”惕医,所以就跑過去和后臺理論讓他們改。如果后臺是比較好說話,肯配合改還好說浇借。但有些可能是比較“倔強(qiáng)”的性格读慎,可能會說,“這很簡單呀峡钓,知道是失敗狀態(tài)不解析 data 不就好了妓笙?”,或者說能岩,“為什么 iOS 可以寞宫,你這邊卻不行?你們 Android 有問題就不能自己處理掉嗎拉鹃?”辈赋。如果遇到這樣的同事就會比較尷尬。
其實(shí)就算后臺能根據(jù)我們要求改膏燕,但也不是長遠(yuǎn)之計炭庙。后臺人員變動或自己換個環(huán)境可能還是會遇到同樣的情況,每次都和后臺溝通配合改也麻煩煌寇,而且沒準(zhǔn)就剛好遇到“倔強(qiáng)”不肯改的焕蹄。
是后臺人員寫得不規(guī)范嗎?我個人認(rèn)為并不是阀溶,因?yàn)椴]有約定俗成的規(guī)范要這么寫腻脏,其實(shí)只是后臺人員不知道這么返回數(shù)據(jù)會對 Retrofit 的解析有影響,不知道這么寫對 Android 不太友好银锻。后臺人員也沒有錯永品,我們所覺得的“規(guī)范”沒人告訴過他呀』魑常可以通過溝通解決問題鼎姐,不過也建議自己把問題處理了,一勞永逸。
解決方案
既然是解析報錯了炕桨,那么在 Gson 解析成對象之前饭尝,先驗(yàn)證狀態(tài)碼,判斷是錯誤的情況就拋出異常献宫,這樣就不進(jìn)行后續(xù)的 Gson 解析操作去解析 data钥平,也就沒問題了。
最先想到的當(dāng)然是從解析的地方入手姊途,而 Retrofit 能進(jìn)行 Gson 解析是配置了一個 Gson 轉(zhuǎn)換器涉瘾。
retrofit = Retrofit.Builder()
// 其它配置
.addConverterFactory(GsonConverterFactory.create())
.build()
所以我們修改 GsonConverterFactory 不就好了。
自定義 GsonConverterFactory 處理返回結(jié)果
試一下會發(fā)現(xiàn)并不能直接繼承 GsonConverterFactory 重載修改相關(guān)方法捷兰,因?yàn)樵擃愑昧?final 修飾立叛。所以只好把 GsonConverterFactory 源碼復(fù)制出來改,其中關(guān)聯(lián)的兩個類 GsonRequestBodyConverter 和 GsonResponseBodyConverter 也要復(fù)制修改贡茅。下面給出的是 Kotlin 版本的示例囚巴。
class MyGsonConverterFactory private constructor(private val gson: Gson) : Converter.Factory() {
override fun responseBodyConverter(
type: Type, annotations: Array<Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *> {
val adapter = gson.getAdapter(TypeToken.get(type))
return MyGsonResponseBodyConverter(gson, adapter)
}
override fun requestBodyConverter(
type: Type,
parameterAnnotations: Array<Annotation>,
methodAnnotations: Array<Annotation>,
retrofit: Retrofit
): Converter<*, RequestBody> {
val adapter = gson.getAdapter(TypeToken.get(type))
return MyGsonRequestBodyConverter(gson, adapter)
}
companion object {
@JvmStatic
fun create(): MyGsonConverterFactory {
return create(Gson())
}
@JvmStatic
fun create(gson: Gson?): MyGsonConverterFactory {
if (gson == null) throw NullPointerException("gson == null")
return MyGsonConverterFactory(gson)
}
}
}
class MyGsonRequestBodyConverter<T>(
private val gson: Gson,
private val adapter: TypeAdapter<T>
) :
Converter<T, RequestBody> {
@Throws(IOException::class)
override fun convert(value: T): RequestBody {
val buffer = Buffer()
val writer = OutputStreamWriter(buffer.outputStream(), UTF_8)
val jsonWriter = gson.newJsonWriter(writer)
adapter.write(jsonWriter, value)
jsonWriter.close()
return buffer.readByteString().toRequestBody(MEDIA_TYPE)
}
companion object {
private val MEDIA_TYPE = "application/json; charset=UTF-8".toMediaType()
private val UTF_8 = Charset.forName("UTF-8")
}
}
class MyGsonResponseBodyConverter<T>(
private val gson: Gson,
private val adapter: TypeAdapter<T>
) : Converter<ResponseBody, T> {
@Throws(IOException::class)
override fun convert(value: ResponseBody): T {
// 在這里通過 value 拿到 json 字符串進(jìn)行解析
// 判斷狀態(tài)碼是失敗的情況,就拋出異常
val jsonReader = gson.newJsonReader(value.charStream())
value.use {
val result = adapter.read(jsonReader)
if (jsonReader.peek() != JsonToken.END_DOCUMENT) {
throw JsonIOException("JSON document was not fully consumed.")
}
return result
}
}
}
上面三個類中只需要修改 GsonResponseBodyConverter 的代碼友扰,因?yàn)槭窃谶@個類解析數(shù)據(jù)彤叉。可以在上面有注釋的地方加入自己的處理村怪。到底加什么代碼秽浇,看完后面的內(nèi)容就知道了。
雖然得到了我們想要的效果甚负,但總感覺并不是很優(yōu)雅柬焕,因?yàn)檫@只是在 gson 解析之前增加一些判斷,而為此多寫了很多和源碼重復(fù)的代碼梭域。還有這是針對 Retrofit 進(jìn)行處理的斑举,如果公司用的是自己封裝的 OkHttp 請求工具,就沒法用這個方案了病涨。
觀察一下發(fā)現(xiàn)其實(shí)只是對一個 ResponseBody 對象進(jìn)行解析判斷狀態(tài)碼富玷,就是說只需要得到個 ResponseBody 對象而已。那么還有什么辦法能在 gson 解析之前拿到 ResponseBody 呢既穆?
自定義攔截器處理返回結(jié)果
很容易會想到用攔截器赎懦,按道理來說是應(yīng)該是可行的,通過攔截器處理也不局限于使用 Retrofit幻工,用 OkHttp 的也能處理励两。
想法很美好,但是實(shí)際操作起來并沒有想象中的簡單囊颅。剛開始可能會想到用 response.body().string()
讀出 json 字符串当悔。
public abstract class ResponseBodyInterceptor implements Interceptor {
@NotNull
@Override
public Response intercept(@NotNull Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
String json = response.body().string();
// 對 json 進(jìn)行解析判斷狀態(tài)碼是失敗的情況就拋出異常
return response;
}
}
看著好像沒問題傅瞻,但是嘗試后發(fā)現(xiàn),狀態(tài)碼是失敗的情況確實(shí)沒毛病盲憎,然而狀態(tài)碼是正確的情況卻有問題了嗅骄。
為什么會這樣子?有興趣的可以看下這篇文章《為何 response.body().string() 只能調(diào)用一次焙畔?》。簡單總結(jié)一下就是考慮到應(yīng)用重復(fù)讀取數(shù)據(jù)的可能性很小串远,所以將其設(shè)計為一次性流宏多,讀取后即關(guān)閉并釋放資源。我們在攔截器里用通常的 Response 使用方法會把資源釋放了澡罚,后續(xù)解析沒有資源了就會有問題伸但。
那該怎么辦呢?自己對 Response 的使用又不熟悉留搔,怎么知道該怎么讀數(shù)據(jù)不影響后續(xù)的操作更胖。可以參考源碼呀隔显,OkHttp 也是用了一些攔截器處理響應(yīng)數(shù)據(jù)却妨,它卻沒有釋放掉資源。
這里就不用大家去看源碼研究怎么寫的了括眠,我直接封裝好一個工具類提供大家使用彪标,已經(jīng)把響應(yīng)數(shù)據(jù)的字符串得到了,大家可以直接編寫自己的業(yè)務(wù)代碼掷豺,拷貝下面的類使用即可捞烟。
abstract class ResponseBodyInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url.toString()
val response = chain.proceed(request)
response.body?.let { responseBody ->
val contentLength = responseBody.contentLength()
val source = responseBody.source()
source.request(Long.MAX_VALUE)
var buffer = source.buffer
if ("gzip".equals(response.headers["Content-Encoding"], ignoreCase = true)) {
GzipSource(buffer.clone()).use { gzippedResponseBody ->
buffer = Buffer()
buffer.writeAll(gzippedResponseBody)
}
}
val contentType = responseBody.contentType()
val charset: Charset =
contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8
if (contentLength != 0L) {
return intercept(response,url, buffer.clone().readString(charset))
}
}
return response
}
abstract fun intercept(response: Response, url: String, body: String): Response
}
由于 OkHttp 源碼已經(jīng)用 Kotlin 語言重寫了,所以只有個 Kotlin 版本的当船。但是可能還有很多人還沒有用 Kotlin 寫項(xiàng)目题画,所以個人又手動翻譯了一個 Java 版本的,方便大家使用德频,同樣拷貝使用即可苍息。
public abstract class ResponseBodyInterceptor implements Interceptor {
@NotNull
@Override
public Response intercept(@NotNull Chain chain) throws IOException {
Request request = chain.request();
String url = request.url().toString();
Response response = chain.proceed(request);
ResponseBody responseBody = response.body();
if (responseBody != null) {
long contentLength = responseBody.contentLength();
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE);
Buffer buffer = source.getBuffer();
if ("gzip".equals(response.headers().get("Content-Encoding"))) {
GzipSource gzippedResponseBody = new GzipSource(buffer.clone());
buffer = new Buffer();
buffer.writeAll(gzippedResponseBody);
}
MediaType contentType = responseBody.contentType();
Charset charset;
if (contentType == null || contentType.charset(StandardCharsets.UTF_8) == null) {
charset = StandardCharsets.UTF_8;
} else {
charset = contentType.charset(StandardCharsets.UTF_8);
}
if (charset != null && contentLength != 0L) {
return intercept(response,url, buffer.clone().readString(charset));
}
}
return response;
}
abstract Response intercept(@NotNull Response response,String url, String body);
}
主要是拿到 source 再獲得 buffer,然后通過 buffer 去讀出字符串壹置。說下其中的一段 gzip
相關(guān)的代碼档叔,為什么需要有這段代碼的處理,自己看源碼的話可能會漏掉蒸绩。這是因?yàn)?OkHttp 請求時會添加支持 gzip
壓縮的預(yù)處理衙四,所以如果響應(yīng)的數(shù)據(jù)是 gzip
編碼的,需要對 gzip
壓縮數(shù)據(jù)解包再去讀數(shù)據(jù)患亿。
好了廢話不多說传蹈,到底這個工具類怎么用押逼,其實(shí)和攔截器一樣使用,繼承我封裝好的 ResponseBodyInterceptor
類惦界,在重寫方法里加上自己需要的業(yè)務(wù)處理代碼挑格,body 參數(shù)就是我們想要的 json 字符串?dāng)?shù)據(jù),可以進(jìn)行解析判斷狀態(tài)碼是失敗情況并拋出異常沾歪。下面給一個簡單的解析例子參考漂彤,json 結(jié)構(gòu)是文章開頭給出的例子,這里假設(shè)狀態(tài)碼不是 200 都拋出一個自定義異常灾搏。
class HandleErrorInterceptor : ResponseBodyInterceptor() {
override fun intercept(response: Response, body: String): Response {
var jsonObject: JSONObject? = null
try {
jsonObject = JSONObject(body)
} catch (e: Exception) {
e.printStackTrace()
}
if (jsonObject != null) {
if (jsonObject.optInt("code", -1) != 200 && jsonObject.has("msg")) {
throw ApiException(jsonObject.getString("msg"))
}
}
return response
}
}
然后在 OkHttpClient 中添加該攔截器就可以了挫望。
val okHttpClient = OkHttpClient.Builder()
// 其它配置
.addInterceptor(HandleErrorInterceptor())
.build()
萬一后臺返回的是更騷的數(shù)據(jù)呢?
本人目前只遇到過失敗時 data 類型不一致的情況狂窑,下面是一些小伙伴反饋的媳板,如果大家有遇到類似或更騷的,都建議和后臺溝通改成返回方便自己寫業(yè)務(wù)邏輯代碼的數(shù)據(jù)泉哈。實(shí)在溝通無果蛉幸,再參考下面的案例看下是否有幫助。
后面所給出的參考方案都是緩兵之計丛晦,不能根治問題奕纫。想徹底地解決只能和后臺人員溝通一套合適的規(guī)范。
數(shù)據(jù)需要去 msg 里取
有位小伙伴提到的:騷的時候數(shù)據(jù)還會去 msg 取烫沙。(大家都經(jīng)歷過了什么...)
還是強(qiáng)調(diào)一下建議讓后臺改若锁,實(shí)在沒辦法必須要這么做的話,再往下看斧吐。
假設(shè)返回的數(shù)據(jù)是下面這樣的:
{
"code": 200,
"msg": {
"userId": 123456,
"userName": "admin"
}
}
通常 msg 返回的是個字符串又固,但這次居然是個對象,而且是我們需要得到的數(shù)據(jù)煤率。我們解析的實(shí)體類已經(jīng)定義了 msg 是字符串仰冠,當(dāng)然不可能因?yàn)橐粋€接口把 msg 改成泛型,所以我們需要偷偷地把數(shù)據(jù)改成我們想要得到的形式蝶糯。
{
"code": 200,
"msg": "登錄成功"
"data": {
"userId": 123456,
"userName": "張三"
}
}
那么該怎么操作呢洋只?代碼比較簡單,就不啰嗦了昼捍,記得要把該攔截器配置了识虚。
class HandleLoginInterceptor: ResponseBodyInterceptor() {
override fun intercept(response: Response, url: String, body: String): Response {
var jsonObject: JSONObject? = null
try {
jsonObject = JSONObject(body)
if (url.contains("/login")) { // 當(dāng)請求的是登錄接口才處理
if (jsonObject.getJSONObject("msg") != null) {
jsonObject.put("data", jsonObject.getJSONObject("msg"))
jsonObject.put("msg", "登錄成功")
}
}
} catch (e: Exception) {
e.printStackTrace()
}
val contentType = response.body?.contentType()
val responseBody = jsonObject.toString().toResponseBody(contentType)
return response.newBuilder().body(responseBody).build() // 重新生成響應(yīng)對象
}
}
如果用 Java 的話,是這樣來重新生成響應(yīng)對象妒茬。
MediaType contentType = response.body().contentType();
ResponseBody responseBody = ResponseBody.create(jsonObject.toString(), contentType);
return response.newBuilder().body(responseBody).build();
數(shù)據(jù)多和數(shù)據(jù)少返回的類型不一樣
又有位小伙伴說道:數(shù)據(jù)少給你返回 JSONObject担锤,數(shù)據(jù)多給你返回 JSONArray,數(shù)據(jù)沒有給你返回 “null”乍钻,null肛循,“”铭腕。(這真的不會被打嗎...)
再強(qiáng)調(diào)一次,建議讓后臺改多糠。如果硬要這么做累舷,再參考下面思路。
小伙伴沒給具體的例子夹孔,這里我自己假設(shè)數(shù)據(jù)的幾種情況被盈。
{
"code": 200,
"msg": "",
"data": "null"
}
{
"code": 200,
"msg": "",
"data": {
"key1": "value1",
"key2": "value2"
}
}
{
"code": 200,
"msg": "",
"data": [
{
"key1": "value1",
"key2": "value2"
},
{
"key1": "value3",
"key2": "value4"
}
]
}
data 的類型會有多種,我們直接請求的話搭伤,應(yīng)該只能將 data 定義成 String只怎,然后解析判斷到底是哪種情況,再寫邏輯代碼闷畸,這樣處理起來麻煩很多尝盼。個人建議用攔截器手動將 data 統(tǒng)一轉(zhuǎn)成 JSONArray 的形式吞滞,這樣 data 類型只有一種佑菩,處理起來更加方便,代碼邏輯也更清晰裁赠。
{
"code": 200,
"msg": "",
"data": []
}
{
"code": 200,
"msg": "",
"data": [
{
"key1": "value1",
"key2": "value2"
}
]
}
{
"code": 200,
"msg": "",
"data": [
{
"key1": "value1",
"key2": "value2"
},
{
"key1": "value3",
"key2": "value4"
}
]
}
具體的代碼就不給出了殿漠,實(shí)現(xiàn)是類似上一個例子,主要是提供思路給大家參考佩捞。
直接返回 http 狀態(tài)碼绞幌,響應(yīng)報文可能沒有或者不是 json
這是有兩位小伙伴說的情況:后臺直接返回 http 狀態(tài)碼,響應(yīng)報文為空一忱、null莲蜘、"null"、""帘营、[] 等這些數(shù)據(jù)票渠。
還是那句話,建議讓后臺改芬迄。如果不肯改问顷,其實(shí)這個處理起來也還好。
大概了解下后臺返回的 http 狀態(tài)碼是一個 600 以上的數(shù)字禀梳,一個狀態(tài)碼對應(yīng)著一個沒有返回數(shù)據(jù)的操作杜窄。響應(yīng)報文可能沒有,可能不是 json算途。
看起來像是不同類型的響應(yīng)報文塞耕,比數(shù)據(jù)類型不同更難處理。其實(shí)這比之前兩個例子簡單很多嘴瓤,因?yàn)椴挥每紤]讀數(shù)據(jù)荷科。具體處理是判斷一下狀態(tài)碼是多少唯咬,然后拋出對應(yīng)的自定義異常,請求時對該的異常進(jìn)行處理畏浆。響應(yīng)報文都是些“空代表”處理起來好像挺麻煩胆胰,但我們沒必要去管,拋了異常就不會進(jìn)行解析刻获。
class HandleHttpCodeInterceptor : ResponseBodyInterceptor() {
override fun intercept(response: Response, url: String, body: String): Response {
when (response.code) {
600,601,602 -> {
throw ApiException(response.code, "msg")
}
else -> {
}
}
return response
}
}
在 header 里取 data 數(shù)據(jù)
居然還有這種騷操作蜀涨,漲見識了...
建議先讓后臺改。后臺不改自己再手動把 header 里的數(shù)據(jù)提取出來蝎毡,轉(zhuǎn)成自己想要的 json 數(shù)據(jù)厚柳。
class ConvertDataInterceptor : ResponseBodyInterceptor() {
override fun intercept(response: Response, url: String, body: String): Response {
val json = "{\"code\": 200}" // 創(chuàng)建自己需要的數(shù)據(jù)結(jié)構(gòu)
val jsonObject = JSONObject(json)
jsonObject.put("data", response.headers["Data"]) // 將 header 里的數(shù)據(jù)設(shè)置到 json 里
val contentType = response.body?.contentType()
val responseBody = jsonObject.toString().toResponseBody(contentType)
return response.newBuilder().body(responseBody).build() // 重新生成響應(yīng)對象
}
}
總結(jié)
大家遇到這些情況建議先與后臺人員溝通。剛開始說的失敗時 data 類型不一致的情況有不少人遇到過沐兵,有需要的可以提前處理預(yù)防一下别垮。至于那些更騷的操作最好還是和后臺溝通一個合適的規(guī)范,實(shí)在溝通無果再參考文中部分案例的處理思路扎谎。
自定義 GsonConverter 與源碼有不少冗余代碼碳想,并不推薦。而且如果想對某個接口的結(jié)果進(jìn)行處理毁靶,不好拿到該地址胧奔。攔截器的方式難點(diǎn)主要是該怎么寫,所以封裝好了工具類供大家使用预吆。
文中提到了用攔截器將數(shù)據(jù)轉(zhuǎn)換成方便我們編寫邏輯的結(jié)構(gòu)龙填,并不是鼓勵大家?guī)秃笈_擦屁股。這種用法或許對某些復(fù)雜的接口來說會有奇效拐叉。
剛開始只是打算分享自己封裝好的類岩遗,說一下怎么使用來解決問題。不過后來還是花了很多篇幅詳細(xì)描述了我解決問題的整個心路歷程凤瘦,主要是見過太多人求助這類問題宿礁,所以就寫詳細(xì)一點(diǎn),后續(xù)如果還有人問就直接發(fā)文章過去廷粒,應(yīng)該能有效解決他的疑惑窘拯。另外如果公司用的請求框架即不是 Retrofit 也不是基于 OkHttp 封裝的框架的話,通過本文章的解決問題思路應(yīng)該也能尋找到相應(yīng)的解決方案坝茎。