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())
}
@JvmStatic
fun 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_8
if (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