Codeegg’s 791st tweet

Author: DylanCai

Blog: https://juejin.cn/post/6844903975028785159

Size girls see the world

Does anyone recognize it?

preface

Retrofit is currently the mainstream network request framework, and many people who have used Retrofit have encountered this problem. The majority of the interface tests are normal, and there is a strange error message about a particular interface, especially if it returns a failure message, and there is no problem with looking at the code logic. Other interfaces are the same to write, but there was no such situation, but the background personnel looked at it also said that it was not their business. It can be confusing at first, and some people don’t know why they can’t start.

Question why

Troubleshooting problems are also very simple, the information baidu, will find that it is parsing exceptions. PostMan = PostMan; PostMan = PostMan; PostMan = PostMan;

{"code": 500,"MSG ":" login failed ","data": ""}Copy the code

It could also go something like this:

{"code": 500,"MSG ":" login failed ","data": 0}Copy the code

Or something like this:

{  "code": 500,"MSG ":" login failed ",  "data": []}Copy the code

After careful observation suddenly suddenly realized, this is not pit dad? I’m going to parse data into an object, and I’m going to return an empty string, integer, or array.

Well, that’s the problem with the backend, it’s that the backend doesn’t write “properly”, so I go and argue with the backend and ask them to change it. If the background is more easy to talk, willing to cooperate with the change is easy to say. But some might be more stubborn and say, “That’s easy. Why not parse data when you know it’s a failed state?” “Or,” Why is it okay on iOS but not on yours? Can’t you fix your Android problems yourself?” . It can be awkward if you encounter such a colleague.

In fact, even if the backend can change according to our requirements, but it is not a long-term solution. Background personnel change or change their own environment may still encounter the same situation, every time and the background communication with change is also troublesome, and maybe just encounter “stubborn” refused to change.

Is it something that the backstage people didn’t write properly? Personally, I don’t think so, because there’s no convention to write it this way, it’s just that the backend people don’t know that returning data this way will affect Retrofit parsing, and it’s not android-friendly. The backstage people are not wrong, we think of “norms” no one told him. You can solve the problem through communication, but you are also advised to deal with it once and for all.

The solution

Since the parsing error is reported, the status code should be verified before Gson parses the object. If the status code is incorrect, an exception will be thrown. In this way, the subsequent Gson parsing operation will not be carried out to parse data, and there is no problem.

The first thing to think about, of course, is parsing, and Retrofit can do Gson parsing by configuring a Gson converter.

retrofit = Retrofit.Builder()// Other configurations  .addConverterFactory(GsonConverterFactory.create())  .build()Copy the code

So let’s just modify GsonConverterFactory.

Custom GsonConverterFactory processing returns the result

If you try it out, you can’t directly inherit the GsonConverterFactory overload to modify the related methods, because the class uses final decoration. So I had to change my GsonConverterFactory source copy out, of which the correlation of two classes GsonRequestBodyConverter and GsonResponseBodyConverter also want to copy. An example of Kotlin’s version is shown below.

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())    }    @JvmStaticfun create(gson: Gson?) : MyGsonConverterFactory {      if (gson == null) throw NullPointerException("gson == null")      return MyGsonConverterFactory(gson)    }  }}Copy the code

MyGsonRequestBodyConverter

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")  }}Copy the code

class MyGsonResponseBodyConverter

class MyGsonResponseBodyConverter<T>(  private val gson: Gson,  private val adapter: TypeAdapter<T>) : Converter<ResponseBody, T> {  @Throws(IOException::class)  override fun convert(value: ResponseBody): T {  // Get the JSON string parsed by value// If the status code fails, throw an exception        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    }  }}Copy the code

Only need to modify the above three classes GsonResponseBodyConverter code, because it is in this kind of analytical data. You can add your own processing where there are comments above. What code to add in the end, after reading the content will know.

We got what we wanted, but it didn’t feel very elegant because it just added some judgment before Gson parsed, and we wrote a lot of code that duplicated the source code. Also, this is targeted at Retrofit, which would not work if the company was using its own packaged OkHttp request tool.

And if you look at it, it’s really just a ResponseBody object that’s being parsed, so all you need is a ResponseBody object. Is there any other way to get the ResponseBody before Gson parses it?

Custom interceptor processing returns results

It’s easy to think that using interceptors should be possible, and processing through interceptors is not limited to using Retrofit, but OkHttp.

The idea is nice, but the practice is not as easy as it should be. The initial thought might be to read out the JSON string with response.body().string().

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();// If the status code fails, throw an exception    return response;  }}Copy the code

It looks like it’s okay, but when you try it out, it’s okay if the status code is failed, but it’s not okay if the status code is correct.

Why is that? For those interested, check out the article why Response.body ().string() can only be called once. (https://juejin.cn/post/6844903545628524551). A simple summary is that it is very unlikely to apply repeated reads, so it is designed to be a one-time stream that closes after reading and frees resources. If we use the usual Response method in the interceptor, the resource will be freed, and the subsequent parsing will be problematic if there is no resource.

So what to do? I am not familiar with the use of Response, so how to know how to read data will not affect subsequent operations. According to the source code, OkHttp also uses interceptors to process the response data, but it does not release resources.

Here we don’t have to look at the source code to study how to write, I directly encapsulated a tool class to provide you to use, has been the response data string, you can directly write their own business code, copy the following class can be used.

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

Since the OkHttp source code has been rewritten in Kotlin, there is only a Kotlin version. However, there may be many people who have not used Kotlin to write projects, so I have manually translated a Java version, convenient for everyone to use, the same copy can be used.

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);}Copy the code

The main thing is to get the source and then get the buffer, and then read the string through the buffer. Say one of the gZIP related code, why need to have this code processing, if you look at the source code may miss. This is because OkHttp requests are preprocessed to support GZIP compression, so if the response data is gZIP-encoded, you need to unpack the gZIP-compressed data before reading the data.

ResponseBodyInterceptor (ResponseBodyInterceptor, ResponseBodyInterceptor, ResponseBodyInterceptor, ResponseBodyInterceptor, ResponseBodyInterceptor, ResponseBodyInterceptor) You can parse to determine if the status code is a failure and throw an exception. For a simple parse example, the json structure is the example given at the beginning of this article, where a custom exception is thrown if the status code is not 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  }}Copy the code

You can then add the interceptor to the OkHttpClient.

val okHttpClient = OkHttpClient.Builder()// Other configurations  .addInterceptor(HandleErrorInterceptor())  .build()Copy the code

What if the background returns something sluttier?

At present, I have only encountered inconsistent data types when failure occurs. The following is the feedback from some partners. If you encounter similar or worse, you are advised to communicate with the background to return data that is convenient for you to write business logic code. Communication is really fruitless, refer to the case below again to see whether it is helpful.

The reference schemes given after are delaying tactics, can not solve the problem. Want to solve thoroughly can only communicate with backstage personnel a set of appropriate norms.

The data needs to be fetched from MSG

A friend mentioned that data will be fetched from MSG when I am in SAO. (What did everyone go through…) I would like to emphasize the suggestion to change the background, if there is no way to do so, read on.

Suppose the returned data looks like this:

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

MSG normally returns a string, but this time it’s an object, and it’s the data we need to get. The entity class we parse already defines MSG as a string, but of course it’s not possible to change MSG to a generic because of an interface, so we need to sneak the data into the form we want.

{  "code": 200,"MSG ":" Login successful"  "data": {    "userId": 123456,"UserName ": "userName"  }}Copy the code

So how do you do that? The code is relatively simple, so I won’t be verbose, remember to configure the interceptor.

class HandleLoginInterceptor: ResponseBodyInterceptor() {  override fun intercept(response: Response, url: String, body: String): Response {    var jsonObject: JSONObject? = null    try {      jsonObject = JSONObject(body)Contains ("/login")) {if (url.contains("/login"))if (jsonObject.getJSONObject("msg") ! = null) {          jsonObject.put("data", jsonObject.getJSONObject("msg"))Jsonobject.put (" MSG ", "login successful ")        }      }    } catch (e: Exception) {      e.printStackTrace()    }val contentType = response.body? .contentType()    val responseBody = jsonObject.toString().toResponseBody(contentType)Return response.newBuilder().body(responseBody).build() // rebuild the response object  }}Copy the code

In Java, this is how the response object is regenerated.

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

More data and less data return different types

Data returns JSONObject, data returns JSONArray, data does not return “null”, “null”. (This really can’t be hit…)

Once again, I suggest that the backend change. If you do, consider the following ideas.

My friend did not give specific examples. Here I assume several situations of data.

{  "code": 200,  "msg": "",  "data": "null"}Copy the code

{  "code": 200,  "msg": "",  "data": {    "key1": "value1",    "key2": "value2"  }}Copy the code

{  "code": 200,  "msg": "",  "data": [    {      "key1": "value1",      "key2": "value2"    },    {      "key1": "value3",      "key2": "value4"    }  ]}Copy the code

There are many types of data. If we request data directly, we can only define data as String, and then parse to determine which case it is, and then write logical code, which is a lot of trouble to deal with. Personally, I recommend using interceptors to manually convert data into JSONArray form. In this way, there is only one data type, which is easier to handle and clearer code logic.

{  "code": 200,  "msg": "",  "data": []}Copy the code

     
{Copy the code  "code": 200,Copy the code  "msg": "",Copy the code  "data": [Copy the code    {Copy the code      "key1": "value1",Copy the code      "key2": "value2"Copy the code    }Copy the code  ]Copy the code}Copy the code
{Copy the code  "code": 200,Copy the code  "msg": "",Copy the code  "data": [Copy the code    {Copy the code      "key1": "value1",Copy the code      "key2": "value2"Copy the code    },Copy the code    {Copy the code      "key1": "value3",Copy the code      "key2": "value4"Copy the code    }Copy the code  ]Copy the code}Copy the code

Copy the code

Specific code is not given, the implementation is similar to the previous example, mainly to provide ideas for your reference.

Returns the HTTP status code directly. The response message may not have or be JSON

HTTP status code is returned directly from the background, and the response message is null, “NULL”, “”, [], etc.

Again, I suggest the backstage change. If you don’t change it, it actually works fine.

The HTTP status code returned in the background is a number of more than 600. A status code corresponds to an operation that does not return data. The response packet may not exist and may not be JSON.

It looks like different types of response messages and is more difficult to process than different data types. This is actually a lot easier than the previous two examples because you don’t have to worry about reading the data. The specific processing is to determine how much the status code is, and then throw the corresponding custom exception, the request to handle the exception. The response messages are all “empty representative” processing seems to be a lot of trouble, but we don’t need to deal with, throw the exception will not parse.

class HandleHttpCodeInterceptor : ResponseBodyInterceptor() {  override fun intercept(response: Response, url: String, body: String): Response {    when (response.code) {600601602 - > {        throw ApiException(response.code, "msg")      }      else -> {      }    }    return response  }}Copy the code

Get the data in the header

There is such a SAO operation, expand my knowledge…

It is suggested to let the background change first. The background does not change itself and then manually extract the data in the header and convert it to the JSON data you want.

class ConvertDataInterceptor : ResponseBodyInterceptor() {  override fun intercept(response: Response, url: String, body: String): Response {Val json = "{\"code\": 200}" // Create your own data structure    val jsonObject = JSONObject(json)Jsonobject. put("data", response.headers[" data"]) // Set the headers to json    val contentType = response.body? .contentType()    val responseBody = jsonObject.toString().toResponseBody(contentType)Return response.newBuilder().body(responseBody).build() // rebuild the response object  }}Copy the code

conclusion

We encounter these situations recommended to communicate with the background staff first. Many people have encountered inconsistent data types when they failed at the beginning. If necessary, they can deal with them in advance and prevent them. As for those more SAO operation is best to communicate with the background of a proper specification, it is useless to communicate and then refer to the processing ideas of some cases in the article.

Custom GsonConverter and source code has a lot of redundant code, is not recommended. And if you want to process the results of an interface, you can’t get that address. The main difficulty of interceptor is how to write, so the package of utility classes for everyone to use.

The article mentions using interceptors to turn data into structures that make it easier to write logic, not to encourage you to clean up the mess behind the scenes. This usage may work wonders for some complex interfaces.

At first I just wanted to share my encapsulated classes and explain how to use them to solve the problem. But later I still spent a lot of length to describe in detail the whole mental process of solving the problem, mainly because I have seen too many people asking for help with this kind of problem, so I wrote a little more detailed, if there are people who ask directly to send the article in the past, it should be able to effectively solve his doubts. In addition, if the company is using a request framework that is neither Retrofit nor OkHttp based, this article should be able to find a solution to the problem.

Related articles:

  • I bet you haven’t figured out how to start your Activity

  • Jack: What if you learn and forget?

  • Hot updates are no different from incremental Android updates

Question of the day:

What weird data have you come across?

Exclusive upgrade community: I Finally Figured it out