ajax後臺返回資料中文亂碼_Android 優雅地處理後臺返回的騷資料
技術標籤:ajax後臺返回資料中文亂碼
本文作者
作者:DylanCai
連結:
https://juejin.im/post/5dadac2ae51d4524c3745219
本文由作者授權釋出。
1 前言Retrofit 是目前主流的網路請求框架,不少用過的小夥伴會遇到這樣的問題,絕大部分介面測試都正常,就個別介面尤其是返回失敗資訊時報了個奇怪的錯誤資訊,而看了自己的程式碼邏輯也沒什麼問題。別的介面都是一樣的寫,卻沒出現這樣的情況,可是後臺人員看了也說不關他們的事。剛遇到時會比較懵,有些人不知道什麼原因也就無從下手。
2 問題原因排查問題也很簡單,把資訊百度一下,會發現是解析異常。那就先看下後臺返回了什麼,用 PostMan 請求一下檢視返回結果,發現是類似下面這樣的:
{
"code":500,
"msg":"登入失敗",
"data":""
}
也可能是這樣的:
{
"code":500,
"msg":"登入失敗",
"data":0
}
或者是這樣的:
{
"code":500,
"msg":"登入失敗",
"data":[]
}
仔細觀察後突然恍然大悟,這不是坑爹嗎?後臺這樣返回解析肯定有問題呀,我要將 data 解析成一個物件,而後臺返回的是一個空字串、整形或空陣列,肯定解析報錯。
嗯,這就是後臺的問題,是後臺寫得不“規範”,所以就跑過去和後臺理論讓他們改。如果後臺是比較好說話,肯配合改還好說。
但有些可能是比較“倔強”的性格,可能會說,“這很簡單呀,知道是失敗狀態不解析 data 不就好了?”,或者說,“為什麼 iOS 可以,你這邊卻不行?你們 Android 有問題就不能自己處理掉嗎?”。如果遇到這樣的同事就會比較尷尬。
其實就算後臺能根據我們要求改,但也不是長遠之計。後臺人員變動或自己換個環境可能還是會遇到同樣的情況,每次都和後臺溝通配合改也麻煩,而且沒準就剛好遇到“倔強”不肯改的。
是後臺人員寫得不規範嗎?我個人認為並不是,因為並沒有約定俗成的規範要這麼寫,其實只是後臺人員不知道這麼返回資料會對 Retrofit 的解析有影響,不知道這麼寫對 Android 不太友好。後臺人員也沒有錯,我們所覺得的“規範”沒人告訴過他呀。可以通過溝通解決問題,不過也建議自己把問題處理了,一勞永逸。
既然是解析報錯了,那麼在 Gson 解析成物件之前,先驗證狀態碼,判斷是錯誤的情況就丟擲異常,這樣就不進行後續的 Gson 解析操作去解析 data,也就沒問題了。
最先想到的當然是從解析的地方入手,而 Retrofit 能進行 Gson 解析是配置了一個 Gson 轉換器。
retrofit=Retrofit.Builder()
//其它配置
.addConverterFactory(GsonConverterFactory.create())
.build()
所以我們修改 GsonConverterFactory 不就好了。
自定義 GsonConverterFactory 處理返回結果
試一下會發現並不能直接繼承 GsonConverterFactory 過載修改相關方法,因為該類用了 final 修飾。所以只好把 GsonConverterFactory 原始碼複製出來改,其中關聯的兩個類 GsonRequestBodyConverter 和 GsonResponseBodyConverter 也要複製修改。
下面給出的是 Kotlin 版本的示例。
classMyGsonConverterFactoryprivateconstructor(privatevalgson:Gson):Converter.Factory(){
overridefunresponseBodyConverter(
type:Type,annotations:Array,
retrofit:Retrofit
):Converter{
valadapter=gson.getAdapter(TypeToken.get(type))
returnMyGsonResponseBodyConverter(gson,adapter)
}
overridefunrequestBodyConverter(
type:Type,
parameterAnnotations:Array,
methodAnnotations:Array,
retrofit:Retrofit
):Converter{
valadapter=gson.getAdapter(TypeToken.get(type))
returnMyGsonRequestBodyConverter(gson,adapter)
}
companionobject{
@JvmStatic
funcreate():MyGsonConverterFactory{
returncreate(Gson())
}
@JvmStatic
funcreate(gson:Gson?):MyGsonConverterFactory{
if(gson==null)throwNullPointerException("gson==null")
returnMyGsonConverterFactory(gson)
}
}
}
classMyGsonRequestBodyConverter<T>(privatevalgson:Gson,privatevaladapter:TypeAdapter<T>
):Converter<T,RequestBody>{
@Throws(IOException::class)overridefunconvert(value:T):RequestBody{
valbuffer=Buffer()
valwriter=OutputStreamWriter(buffer.outputStream(),UTF_8)
valjsonWriter=gson.newJsonWriter(writer)
adapter.write(jsonWriter,value)
jsonWriter.close()
returnbuffer.readByteString().toRequestBody(MEDIA_TYPE)
}
companionobject{
privatevalMEDIA_TYPE="application/json;charset=UTF-8".toMediaType()
privatevalUTF_8=Charset.forName("UTF-8")
}
}
classMyGsonResponseBodyConverter<T>(privatevalgson:Gson,privatevaladapter:TypeAdapter<T>
):Converter<ResponseBody,T>{
@Throws(IOException::class)overridefunconvert(value:ResponseBody):T{
//在這裡通過value拿到json字串進行解析
//判斷狀態碼是失敗的情況,就丟擲異常
valjsonReader=gson.newJsonReader(value.charStream())
value.use{
valresult=adapter.read(jsonReader)
if(jsonReader.peek()!=JsonToken.END_DOCUMENT){
throwJsonIOException("JSONdocumentwasnotfullyconsumed.")
}
returnresult
}
}
}
上面三個類中只需要修改 GsonResponseBodyConverter 的程式碼,因為是在這個類解析資料。可以在上面有註釋的地方加入自己的處理。到底加什麼程式碼,看完後面的內容就知道了。
雖然得到了我們想要的效果,但總感覺並不是很優雅,因為這只是在 gson 解析之前增加一些判斷,而為此多寫了很多和原始碼重複的程式碼。
還有這是針對 Retrofit 進行處理的,如果公司用的是自己封裝的 OkHttp 請求工具,就沒法用這個方案了。
觀察一下發現其實只是對一個 ResponseBody 物件進行解析判斷狀態碼,就是說只需要得到個 ResponseBody 物件而已。那麼還有什麼辦法能在 gson 解析之前拿到 ResponseBody 呢?
自定義攔截器處理返回結果
很容易會想到用攔截器,按道理來說是應該是可行的,通過攔截器處理也不侷限於使用 Retrofit,用 OkHttp 的也能處理。;
想法很美好,但是實際操作起來並沒有想象中的簡單。剛開始可能會想到用 response.body().string() 讀出 json 字串。
publicabstractclassResponseBodyInterceptorimplementsInterceptor{
@NotNull
@Override
publicResponseintercept(@NotNullChainchain)throwsIOException{
Responseresponse=chain.proceed(chain.request());
Stringjson=response.body().string();
//對json進行解析判斷狀態碼是失敗的情況就丟擲異常
returnresponse;
}
}
看著好像沒問題,但是嘗試後發現,狀態碼是失敗的情況確實沒毛病,然而狀態碼是正確的情況卻有問題了。
為什麼會這樣子?有興趣的可以看下這篇文章《為何 response.body().string() 只能呼叫一次?》。
https://juejin.im/post/5a524eef518825732c536025
簡單總結一下就是考慮到應用重複讀取資料的可能性很小,所以將其設計為一次性流,讀取後即關閉並釋放資源。我們在攔截器裡用通常的 Response 使用方法會把資源釋放了,後續解析沒有資源了就會有問題。
那該怎麼辦呢?自己對 Response 的使用又不熟悉,怎麼知道該怎麼讀資料不影響後續的操作。可以參考原始碼呀,OkHttp 也是用了一些攔截器處理響應資料,它卻沒有釋放掉資源。
這裡就不用大家去看原始碼研究怎麼寫的了,我直接封裝好一個工具類提供大家使用,已經把響應資料的字串得到了,大家可以直接編寫自己的業務程式碼,拷貝下面的類使用即可。
abstractclassResponseBodyInterceptor:Interceptor{
overridefunintercept(chain:Interceptor.Chain):Response{
valrequest=chain.request()
valurl=request.url.toString()
valresponse=chain.proceed(request)
response.body?.let{responseBody->
valcontentLength=responseBody.contentLength()
valsource=responseBody.source()
source.request(Long.MAX_VALUE)
varbuffer=source.bufferif("gzip".equals(response.headers["Content-Encoding"],ignoreCase=true)){
GzipSource(buffer.clone()).use{gzippedResponseBody->
buffer=Buffer()
buffer.writeAll(gzippedResponseBody)
}
}
valcontentType=responseBody.contentType()
valcharset:Charset=
contentType?.charset(StandardCharsets.UTF_8)?:StandardCharsets.UTF_8if(contentLength!=0L){
returnintercept(response,url,buffer.clone().readString(charset))
}
}
returnresponse
}
abstractfunintercept(response:Response,url:String,body:String):Response
}
由於 OkHttp 原始碼已經用 Kotlin 語言重寫了,所以只有個 Kotlin 版本的。
但是可能還有很多人還沒有用 Kotlin 寫專案,所以個人又手動翻譯了一個 Java 版本的,方便大家使用,同樣拷貝使用即可。
publicabstractclassResponseBodyInterceptorimplementsInterceptor{
@NotNull
@Override
publicResponseintercept(@NotNullChainchain)throwsIOException{
Requestrequest=chain.request();
Stringurl=request.url().toString();
Responseresponse=chain.proceed(request);
ResponseBodyresponseBody=response.body();
if(responseBody!=null){
longcontentLength=responseBody.contentLength();
BufferedSourcesource=responseBody.source();
source.request(Long.MAX_VALUE);
Bufferbuffer=source.getBuffer();
if("gzip".equals(response.headers().get("Content-Encoding"))){
GzipSourcegzippedResponseBody=newGzipSource(buffer.clone());
buffer=newBuffer();
buffer.writeAll(gzippedResponseBody);
}
MediaTypecontentType=responseBody.contentType();
Charsetcharset;
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){
returnintercept(response,url,buffer.clone().readString(charset));
}
}
returnresponse;
}
abstractResponseintercept(@NotNullResponseresponse,Stringurl,Stringbody)throwsIOException;
}
主要是拿到 source 再獲得 buffer,然後通過 buffer 去讀出字串。說下其中的一段 gzip 相關的程式碼,為什麼需要有這段程式碼的處理,自己看原始碼的話可能會漏掉。
這是因為 OkHttp 請求時會新增支援 gzip 壓縮的預處理,所以如果響應的資料是 gzip 編碼的,需要對 gzip 壓縮資料解包再去讀資料。
好了廢話不多說,到底這個工具類怎麼用,其實和攔截器一樣使用,繼承我封裝好的 ResponseBodyInterceptor 類,在重寫方法里加上自己需要的業務處理程式碼,body 引數就是我們想要的 json 字串資料,可以進行解析判斷狀態碼是失敗情況並丟擲異常。
下面給一個簡單的解析例子參考,json 結構是文章開頭給出的例子,這裡假設狀態碼不是 200 都丟擲一個自定義異常。
classHandleErrorInterceptor:ResponseBodyInterceptor(){
overridefunintercept(response:Response,body:String):Response{
varjsonObject:JSONObject?=null
try{
jsonObject=JSONObject(body)
}catch(e:Exception){
e.printStackTrace()
}
if(jsonObject!=null){
if(jsonObject.optInt("code",-1)!=200&&jsonObject.has("msg")){
throwApiException(jsonObject.getString("msg"))
}
}
returnresponse
}
}
然後在 OkHttpClient 中新增該攔截器就可以了。
valokHttpClient=OkHttpClient.Builder()
//其它配置
.addInterceptor(HandleErrorInterceptor())
.build()
4
萬一後臺返回的是更騷的資料呢?
本人目前只遇到過失敗時 data 型別不一致的情況,下面是一些小夥伴反饋的,如果大家有遇到類似或更騷的,都建議和後臺溝通改成返回方便自己寫業務邏輯程式碼的資料。實在溝通無果,再參考下面的案例看下是否有幫助。
後面所給出的參考方案都是緩兵之計,不能根治問題。想徹底地解決只能和後臺人員溝通一套合適的規範。
資料需要去 msg 裡取
有位小夥伴提到的:騷的時候資料還會去 msg 取。(大家都經歷過了什麼...)
還是強調一下建議讓後臺改,實在沒辦法必須要這麼做的話,再往下看。
假設返回的資料是下面這樣的:
{
"code":200,
"msg":{
"userId":123456,
"userName":"admin"
}
}
通常 msg 返回的是個字串,但這次居然是個物件,而且是我們需要得到的資料。
我們解析的實體類已經定義了 msg 是字串,當然不可能因為一個介面把 msg 改成泛型,所以我們需要偷偷地把資料改成我們想要得到的形式。
{
"code":200,
"msg":"登入成功"
"data":{
"userId":123456,
"userName":"張三"
}
}
那麼該怎麼操作呢?程式碼比較簡單,就不囉嗦了,記得要把該攔截器配置了。
classHandleLoginInterceptor:ResponseBodyInterceptor(){
overridefunintercept(response:Response,url:String,body:String):Response{
varjsonObject:JSONObject?=null
try{
jsonObject=JSONObject(body)
if(url.contains("/login")){//當請求的是登入接口才處理
if(jsonObject.getJSONObject("msg")!=null){
jsonObject.put("data",jsonObject.getJSONObject("msg"))
jsonObject.put("msg","登入成功")
}
}
}catch(e:Exception){
e.printStackTrace()
}
valcontentType=response.body?.contentType()
valresponseBody=jsonObject.toString().toResponseBody(contentType)
returnresponse.newBuilder().body(responseBody).build()//重新生成響應物件
}
}
如果用 Java 的話,是這樣來重新生成響應物件。
MediaTypecontentType=response.body().contentType();
ResponseBodyresponseBody=ResponseBody.create(jsonObject.toString(),contentType);
returnresponse.newBuilder().body(responseBody).build();
直接返回 http 狀態碼,響應報文可能沒有或者不是 json
這是有兩位小夥伴說的情況:後臺直接返回 http 狀態碼,響應報文為空、null、"null"、""、[] 等這些資料。
還是那句話,建議讓後臺改。如果不肯改,其實這個處理起來也還好。
大概瞭解下後臺返回的 http 狀態碼是一個 600 以上的數字,一個狀態碼對應著一個沒有返回資料的操作。響應報文可能沒有,可能不是 json。
看起來像是不同型別的響應報文,比資料型別不同更難處理。其實這比之前兩個例子簡單很多,因為不用考慮讀資料。具體處理是判斷一下狀態碼是多少,然後丟擲對應的自定義異常,請求時對該的異常進行處理。響應報文都是些“空代表”處理起來好像挺麻煩,但我們沒必要去管,拋了異常就不會進行解析。
classHandleHttpCodeInterceptor:ResponseBodyInterceptor(){
overridefunintercept(response:Response,url:String,body:String):Response{
when(response.code){
600,601,602->{
throwApiException(response.code,"msg")
}
else->{
}
}
returnresponse
}
}
在 header 裡取 data 資料
居然還有這種騷操作,漲見識了...
建議先讓後臺改。後臺不改自己再手動把 header 裡的資料提取出來,轉成自己想要的 json 資料。
classConvertDataInterceptor:ResponseBodyInterceptor(){
overridefunintercept(response:Response,url:String,body:String):Response{
valjson="{\"code\":200}"//建立自己需要的資料結構
valjsonObject=JSONObject(json)
jsonObject.put("data",response.headers["Data"])//將header裡的資料設定到json裡
valcontentType=response.body?.contentType()
valresponseBody=jsonObject.toString().toResponseBody(contentType)
returnresponse.newBuilder().body(responseBody).build()//重新生成響應物件
}
}
5
總結
大家遇到這些情況建議先與後臺人員溝通。
剛開始說的失敗時 data 型別不一致的情況有不少人遇到過,有需要的可以提前處理預防一下。
至於那些更騷的操作最好還是和後臺溝通一個合適的規範,實在溝通無果再參考文中部分案例的處理思路。
自定義 GsonConverter 與原始碼有不少冗餘程式碼,並不推薦。
而且如果想對某個介面的結果進行處理,不好拿到該地址。攔截器的方式難點主要是該怎麼寫,所以封裝好了工具類供大家使用。
文中提到了用攔截器將資料轉換成方便我們編寫邏輯的結構,並不是鼓勵大家幫後臺擦屁股。這種用法或許對某些複雜的介面來說會有奇效。
剛開始只是打算分享自己封裝好的類,說一下怎麼使用來解決問題。不過後來還是花了很多篇幅詳細描述了我解決問題的整個心路歷程,主要是見過太多人求助這類問題,所以就寫詳細一點,後續如果還有人問就直接發文章過去,應該能有效解決他的疑惑。
另外如果公司用的請求框架即不是 Retrofit 也不是基於 OkHttp 封裝的框架的話,通過本文章的解決問題思路應該也能尋找到相應的解決方案。
推薦閱讀:
進階實戰:App執行時大圖監控,你怎麼做? Android MotionLayout動畫:續寫ConstraintLayout新篇章 直面底層:WindowManager 檢視繫結以及體系結構掃一掃關注我的公眾號
如果你想要跟大家分享你的文章,歡迎投稿~
┏(^0^)┛明天見!