这个问题我相信很多人都会遇到,那就是本来好好的代码,莫名其妙就崩溃了,一番查询发现,后端返回的json格式变了,心里顿时一万只草泥马在奔腾,然后怒气冲冲找到后端,去质问。这个时候博弈就开始了,遇到好说话的后端,他们会立马修复,但遇到比较强势的后端同学,他们不但不修改,还把锅甩给客户端同学,真是无语。

这种事情我经历了好多次,大部分后端都主动修复了,但确实也遇到过犯二的人,各种找借口不改,要客户端改,这个时候也没办法,为了尽快解决问题,也只能客户端做紧急修复了。

除此之外,再给大家推荐一篇DylanCai的文章,这篇文章提供了一些方法,可以增强客户端的容错性,没办法,为了不接锅,能多做一点就多做一点吧。

文章作者:DylanCaihttps://juejin.im/post/5dadac2ae51d4524c3745219

下面是正文:

前言

Retrofit 是目前主流的网络请求框架,不少用过的小伙伴会遇到这样的问题,绝大部分接口测试都正常,就个别接口尤其是返回失败信息时报了个奇怪的错误信息,而看了自己的代码逻辑也没什么问题。别的接口都是一样的写,却没出现这样的情况,可是后台人员看了也说不关他们的事。刚遇到时会比较懵,有些人不知道什么原因也就无从下手。

问题原因

排查问题也很简单,把信息百度一下,会发现是解析异常。那就先看下后台返回了什么,用 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 版本的示例。

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 {@JvmStaticfun create(): MyGsonConverterFactory {return create(Gson())}@JvmStaticfun 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 字符串进行解析// 判断状态码是失败的情况,就抛出异常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 的代码,因为是在这个类解析数据。可以在上面有注释的地方加入自己的处理。到底加什么代码,看完后面的内容就知道了。

虽然得到了我们想要的效果,但总感觉并不是很优雅,因为这只是在 gson 解析之前增加一些判断,而为此多写了很多和源码重复的代码。还有这是针对 Retrofit 进行处理的,如果公司用的是自己封装的 OkHttp 请求工具,就没法用这个方案了。

观察一下发现其实只是对一个 ResponseBody 对象进行解析判断状态码,就是说只需要得到个 ResponseBody 对象而已。那么还有什么办法能在 gson 解析之前拿到 ResponseBody 呢?

自定义拦截器处理返回结果

很容易会想到用拦截器,按道理来说是应该是可行的,通过拦截器处理也不局限于使用 Retrofit,用 OkHttp 的也能处理。

想法很美好,但是实际操作起来并没有想象中的简单。刚开始可能会想到用 response.body().string() 读出 json 字符串。

public abstract class ResponseBodyInterceptor implements Interceptor {@NotNull@Overridepublic Response intercept(@NotNull Chain chain) throws IOException {Response response = chain.proceed(chain.request());String json = response.body().string();// 对 json 进行解析判断状态码是失败的情况就抛出异常return response;}
}

看着好像没问题,但是尝试后发现,状态码是失败的情况确实没毛病,然而状态码是正确的情况却有问题了。

为什么会这样子?有兴趣的可以看下这篇文章《为何 response.body().string() 只能调用一次?》。简单总结一下就是考虑到应用重复读取数据的可能性很小,所以将其设计为一次性流,读取后即关闭并释放资源。我们在拦截器里用通常的 Response 使用方法会把资源释放了,后续解析没有资源了就会有问题。

那该怎么办呢?自己对 Response 的使用又不熟悉,怎么知道该怎么读数据不影响后续的操作。可以参考源码呀,OkHttp 也是用了一些拦截器处理响应数据,它却没有释放掉资源。

这里就不用大家去看源码研究怎么写的了,我直接封装好一个工具类提供大家使用,已经把响应数据的字符串得到了,大家可以直接编写自己的业务代码,拷贝下面的类使用即可。

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.bufferif ("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_8if (contentLength != 0L) {return intercept(response,url, buffer.clone().readString(charset))}}return response}abstract fun intercept(response: Response, url: String, body: String): Response
}

由于 OkHttp 源码已经用 Kotlin 语言重写了,所以只有个 Kotlin 版本的。但是可能还有很多人还没有用 Kotlin 写项目,所以个人又手动翻译了一个 Java 版本的,方便大家使用,同样拷贝使用即可。

public abstract class ResponseBodyInterceptor implements Interceptor {@NotNull@Overridepublic 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 相关的代码,为什么需要有这段代码的处理,自己看源码的话可能会漏掉。这是因为 OkHttp 请求时会添加支持 gzip 压缩的预处理,所以如果响应的数据是 gzip 编码的,需要对 gzip 压缩数据解包再去读数据。

好了废话不多说,到底这个工具类怎么用,其实和拦截器一样使用,继承我封装好的 ResponseBodyInterceptor 类,在重写方法里加上自己需要的业务处理代码,body 参数就是我们想要的 json 字符串数据,可以进行解析判断状态码是失败情况并抛出异常。下面给一个简单的解析例子参考,json 结构是文章开头给出的例子,这里假设状态码不是 200 都抛出一个自定义异常。

class HandleErrorInterceptor : ResponseBodyInterceptor() {override fun intercept(response: Response, body: String): Response {var jsonObject: JSONObject? = nulltry {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()

万一后台返回的是更骚的数据呢?

本人目前只遇到过失败时 data 类型不一致的情况,下面是一些小伙伴反馈的,如果大家有遇到类似或更骚的,都建议和后台沟通改成返回方便自己写业务逻辑代码的数据。实在沟通无果,再参考下面的案例看下是否有帮助。其实就是手动通过拦截器转成方便自己写逻辑的数据,这只是缓兵之策,太复杂的情况还是想办法和后台沟通吧。

数据需要去 msg 里取

有位小伙伴提到的:骚的时候数据还会去 msg 取。(大家都经历过了什么…)

还是强调一下建议让后台改,实在没办法必须要这么做的话,再往下看。

假设返回的数据是下面这样的:

{"code": 200,"msg": {"userId": 123456,"userName": "admin"}
}

通常 msg 返回的是个字符串,但这次居然是个对象,而且是我们需要得到的数据。我们解析的实体类已经定义了 msg 是字符串,当然不可能因为一个接口把 msg 改成泛型,所以我们需要偷偷地把数据改成我们想要得到的形式。

{"code": 200,"msg": "登录成功""data": {"userId": 123456,"userName": "张三"}
}

那么该怎么操作呢?代码比较简单,就不啰嗦了,记得要把该拦截器配置了。

class HandleLoginInterceptor: ResponseBodyInterceptor() {override fun intercept(response: Response, url: String, body: String): Response {var jsonObject: JSONObject? = nulltry {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()}val contentType = response.body?.contentType()val responseBody = jsonObject.toString().toResponseBody(contentType)return response.newBuilder().body(responseBody).build() // 重新生成响应对象}
}

如果用 Java 的话,是这样来重新生成响应对象。

MediaType contentType = response.body().contentType();
ResponseBody responseBody = ResponseBody.create(jsonObject.toString(), contentType);
return response.newBuilder().body(responseBody).build();

数据多和少返回的类型不一样

又有位小伙伴说道:数据少给你返回 JSONObject,数据多给你返回 JSONArray,数据没有给你返回 “null”,null,“”。(这真的不会被打吗…)

再强调一次,建议让后台改。如果硬要这么做,再参考下面思路。

小伙伴没给具体的例子,这里我自己假设数据的几种情况。

{"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 的类型会有多种,我们直接请求的话,应该只能将 data 定义成 String,然后解析判断到底是哪种情况,再写逻辑代码,这样处理起来麻烦很多。个人建议用拦截器手动将 data 统一转成 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"}]
}

具体的代码就不给出了,实现类似于上一个例子,主要是提供思路给大家参考。

总结

大家遇到这些情况建议先与后台人员沟通。刚开始说的失败时 data 类型不一致的情况有不少人遇到过,有需要的可以提前处理预防一下。至于那些更骚的操作最好还是和后台沟通一个合适的规范,实在沟通无果再参考文中部分案例的处理思路。

自定义 GsonConverter 与源码有不少冗余代码,并不推荐。而且如果想对某个接口的结果进行处理,不好拿到该地址。拦截器的方式难点主要是该怎么写,所以封装好了工具类供大家使用。

文中提到了用拦截器将数据转换成方便我们编写逻辑的结构,并不是鼓励大家帮后台擦屁股。这种用法或许对某些复杂的接口来说会有奇效。

刚开始只是打算分享自己封装好的类,说一下怎么使用来解决问题。不过后来还是花了很多篇幅详细描述了我解决问题的整个心路历程,主要是见过太多人求助这类问题,所以就写详细一点,后续如果还有人问就直接发文章过去,应该能有效解决他的疑惑。另外如果公司用的请求框架即不是 Retrofit 也不是基于 OkHttp 封装的框架的话,通过本文章的解决问题思路应该也能寻找到相应的解决方案。

说一个头疼的问题:后端瞎返回数据导致APP崩溃,你会怎么办?相关推荐

  1. 前端console.log打印内容与后端请求返回数据不一致

    下面是请求接口返回的数据: 目的是为了获取list列表 然后前端打印的时候出现问题,list列表始终为空. 这是修改正确之后的,数据已正常显示. 修改如下: var obj = JSON.parse( ...

  2. nodejs调用后端接口返回excel文件流,nodejs再转发前端实现下载excel

    今天在项目上遇到了一个问题,就是后端接口返回excel文件流,然后前端实现下载excel,前端下载的excel文件居然无法打开! 在网上搜索了很多对应的资料,也尝试了很多办法,还是没能解决,最后在这篇 ...

  3. 解决后端返回数据中的大数字问题(使用第三方包json-bigint )

    JavaScript 能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值,这使得 JavaScript 不适合进行科学和金融方面的精确计算. Math. ...

  4. 8 种方案机智应对后端一次性返回 10万 条数据

    大厂技术 高级前端 Node进阶点击上方 程序员成长指北,关注公众号 回复1,加入高级Node交流群 问题描述 面试官:后端一次性返回10万条数据给你,你如何处理? 我:歪嘴一笑,what the f ...

  5. Fiddler中篡改后端返回数据

    使用fiddler篡改返回数据,影响前端返回 最近在验证一个优化需求,前端根据后端返回的身份证倒数第二位进行判断男女,如果是男生,则展示男生的图片:如果判断是女生,则展示女生的照片:如不能判断男女时, ...

  6. Echarts异步加载后端接口返回的Json数据生成图表

         一.Echarts 是一款很好用的前端报表制作工具,根据官网的开发文档,我们可以导入假数据制作各种分析的图表.详情看官网:Echarts官网案例.但是如果要根据实际情况导入真实的数据呢?怎么 ...

  7. php主动推送弹幕_【转载】如何使用PHP构建一个高性能的弹幕后端服务

    随着WEB2.0的流行,现在很多网站都流行使用"弹幕"这种形式来实现互动. 弹幕(barrage),中文流行词语,原意指用大量或少量火炮提供密集炮击.大量以字幕弹(dàn)出形式显 ...

  8. 加密前端传参和后端结果返回加密

    好的,我分别给出一个简单的前端代码样例和后端 Node.js 代码样例,示范如何加密前端传参和后端结果返回的加密. 前端代码示例: import CryptoJS from 'crypto-js';c ...

  9. 【Go语言】实现一个简单的纯后端学员管理系统,入门级别练手项目,练习结构体,接口,构造函数

    GO语言实现一个简单的纯后端学员管理系统 项目总览: 一.项目开发日志 二.前情知识补充 1.构造函数:用来在创建对象时初始化对象 2.方法和接收者 三.主函数中的内容 四.逐个自定义函数拆解分析 1 ...

最新文章

  1. JavaScript 输出
  2. Xilinx zynq的资料获取总结
  3. CACTI安装后不出图问题解决方法(由于时间问题引起的)
  4. linux运维实战练习-2015年9月13日-9月15日课程作业(练习)安排
  5. MSSQL数据库全库批量替换
  6. cocoaChina中整理的知识点-对文件重新命名-利于查阅
  7. AI+遥感:释放每个像元价值(人工智能应用案例)
  8. 熵权法求权重python代码
  9. TwinCAT软件编码器参数介绍
  10. 【转载】Linux查看日志
  11. 基于kettle的可视化数据集成平台
  12. 实用思维导图、流程图模板合集,让你的头脑灵活起来
  13. 查看oracle数据库防火墙设置,用三个方法设置Oracle数据库穿越防火墙
  14. wamp mysql 没有启动,WAMP中mysql服务突然无法启动 解决方法
  15. 天龙八部服务端数据传递
  16. MSCKF(一)——四元数的两种表示
  17. 一个请求结束之后再发送另外一个请求,需要连着发很多请求的方法-promise
  18. HTML5+CSS期末大作业:环保网站设计——环境保护(10页) 含设计报告 HTML+CSS+JavaScript 静态HTML环境保护网页制作下载 DIV+CSS环保网页设计代码
  19. 注意办公室的言行礼仪
  20. 蜀门锻造系统c语言原理,蜀门系统装备锻造成功的原理解析 增加锻造成功率心得分享...

热门文章

  1. python长度单位换算用def_【每天学点Python】案例一:汇率换算
  2. 后端做app连续会员包月功能 -- IOS连续订阅 支付宝周期扣款
  3. 苹果手机量体温_手机记录西藏之旅和美食
  4. 如何获取AK/SK?(IAM用户)
  5. 关于未来计算机的英语作文,《 未来机器人》英语作文
  6. 联想服务器ghost系统进不去系统,联想电脑台式机开机后一直在这个界面进不了系统是为什么。...
  7. 公主连结显示服务器内部错误,公主连结进不去怎么办 进不去解决办法
  8. Centos 7 环境下将修改后的MAC地址永久保存
  9. 响应对象转json时属性名大小写问题完美解决
  10. QPython+uiautomator2安卓手机自动化脚本编写