大家好,欢迎来到IT知识分享网。
前言
Retrofit 是目前主流的网络请求框架,不少用过的小伙伴会遇到这样的问题,绝大部分接口测试都正常,就个别接口尤其是返回失败信息时报了个奇怪的错误信息,而看了自己的代码逻辑也没什么问题。别的接口都是一样的写,却没出现这样的情况,可是后台人员看了也说不关他们的事。刚遇到时会比较懵,有些人不知道什么原因也就无从下手。
问题原因
排查问题也很简单,把信息百度一下,会发现是解析异常。那就先看下后台返回了什么,用 PostMan 请求一下查看返回结果,发现是类似下面这样的:
{
"code": 500,
"msg": "登录失败",
"data": ""
}
IT知识分享网
也可能是这样的:
IT知识分享网{
"code": 500,
"msg": "登录失败",
"data": 0
}
或者是这样的:
{
"code": 500,
"msg": "登录失败",
"data": []
}
仔细观察后突然恍然大悟,这不是坑爹吗?后台这样返回解析肯定有问题呀,我要将 data 解析成一个对象,而后台返回的是一个空字符串、整形或空数组,肯定解析报错。
嗯,这就是后台的问题,是后台写得不“规范”,所以就跑过去和后台理论让他们改。如果后台是比较好说话,肯配合改还好说。但有些可能是比较“倔强”的性格,可能会说,“这很简单呀,知道是失败状态不解析 data 不就好了?”,或者说,“为什么 iOS 可以,你这边却不行?你们 Android 有问题就不能自己处理掉吗?”。如果遇到这样的同事就会比较尴尬。
其实就算后台能根据我们要求改,但也不是长远之计。后台人员变动或自己换个环境可能还是会遇到同样的情况,每次都和后台沟通配合改也麻烦,而且没准就刚好遇到“倔强”不肯改的。
是后台人员写得不规范吗?我个人认为并不是,因为并没有约定俗成的规范要这么写,其实只是后台人员不知道这么返回数据会对 Retrofit 的解析有影响,不知道这么写对 Android 不太友好。后台人员也没有错,我们所觉得的“规范”没人告诉过他呀。最好和后台人员沟通解决问题,不过有的时候不得不自己处理,那就请往下看吧。
解决方案
既然是解析报错了,那么在 Gson 解析成对象之前,先验证状态码,判断是错误的情况就抛出异常,这样就不进行后续的 Gson 解析操作去解析 data,也就没问题了。
最先想到的当然是从解析的地方入手,而 Retrofit 能进行 Gson 解析是配置了一个 Gson 转换器。
IT知识分享网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 {
@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 字符串进行解析
// 判断状态码是失败的情况,就抛出异常
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 的代码,因为是在这个类解析数据。可以在上面有注释的地方加入自己的处理。到底加什么代码,看完后面的内容就知道了。
虽然能达到我们想要得效果,但是也有点弊端:
- 由于 GsonResponseBodyConverter 有 final 修饰不能被继承修改,被迫拷贝出 3 个类来修改其中一个类的代码,那另外两个类有点冗余。
- 这是针对 Retrofit 进行处理的,如果公司用的是自己封装的 OkHttp 请求工具,就没法用这个方案了。
观察一下发现其实只是对一个 ResponseBody 对象进行解析判断状态码,就是说只需要得到个 ResponseBody 对象而已。那么还有什么办法能在 gson 解析之前拿到 ResponseBody 呢?
自定义拦截器处理返回结果
很容易会想到用拦截器,按道理来说是应该是可行的,通过拦截器处理也不局限于使用 Retrofit,用 OkHttp 的也能处理。
想法很美好,但是实际操作起来并没有想象中的简单。刚开始可能会想到用 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 进行解析判断状态码是失败的情况就抛出异常
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.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 源码已经用 Kotlin 语言重写了,所以只有个 Kotlin 版本的。但是可能还有很多人还没有用 Kotlin 写项目,所以个人又手动翻译了一个 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) throws IOException;
}
主要是拿到 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? = 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()
万一后台返回的是更骚的数据呢?
本人目前只遇到过失败时 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? = 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()
}
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"
}
]
}
具体的代码就不给出了,实现是类似上一个例子,主要是提供思路给大家参考。
直接返回 http 状态码,响应报文可能没有或者不是 json
这是有两位小伙伴说的情况:后台直接返回 http 状态码,响应报文为空、null、”null”、””、[] 等这些数据。
还是那句话,建议让后台改。如果不肯改,其实这个处理起来也还好。
大概了解下后台返回的 http 状态码是一个 600 以上的数字,一个状态码对应着一个没有返回数据的操作。响应报文可能没有,可能不是 json。
看起来像是不同类型的响应报文,比数据类型不同更难处理。其实这比之前两个例子简单很多,因为不用考虑读数据。具体处理是判断一下状态码是多少,然后抛出对应的自定义异常,请求时对该的异常进行处理。响应报文都是些“空代表”处理起来好像挺麻烦,但我们没必要去管,抛了异常就不会进行解析。
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 数据
居然还有这种骚操作,涨见识了…
建议先让后台改。后台不改自己再手动把 header 里的数据提取出来,转成自己想要的 json 数据。
class ConvertDataInterceptor : ResponseBodyInterceptor() {
override fun intercept(response: Response, url: String, body: String): Response {
val json = "{\"code\": 200}" // 创建自己需要的数据结构
val jsonObject = JSONObject(json)
jsonObject.put("data", response.headers["Data"]) // 将 header 里的数据设置到 json 里
val contentType = response.body?.contentType()
val responseBody = jsonObject.toString().toResponseBody(contentType)
return response.newBuilder().body(responseBody).build() // 重新生成响应对象
}
}
总结
大家遇到这些情况建议先与后台人员沟通。刚开始说的失败时 data 类型不一致的情况有不少人遇到过,有需要的可以提前处理预防一下。至于那些更骚的操作最好还是和后台沟通一个合适的规范,实在沟通无果再参考文中部分案例的处理思路。
自定义 GsonConverter 与源码有不少冗余代码,并不推荐。而且如果想对某个接口的结果进行处理,不好拿到该地址。拦截器的方式难点主要是该怎么写,所以封装好了工具类供大家使用。
文中提到了用拦截器将数据转换成方便我们编写逻辑的结构,并不是鼓励大家帮后台擦屁股。这种用法或许对某些复杂的接口来说会有奇效。
刚开始只是打算分享自己封装好的类,说一下怎么使用来解决问题。不过后来还是花了很多篇幅详细描述了我解决问题的整个心路历程,主要是见过太多人求助这类问题,所以就写详细一点,后续如果还有人问就直接发文章过去,应该能有效解决他的疑惑。另外如果公司用的请求框架即不是 Retrofit 也不是基于 OkHttp 封装的框架的话,通过本文章的解决问题思路应该也能寻找到相应的解决方案。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/13314.html