Android 健壮地处理后台返回的数据

1. 前言

最近工作中,和后台交互数据比较多,遇到一些后台返回数据不规范或者是错误,但是 leader 要客户端来处理。在这里记录一下解决问题的过程,希望对大家有所帮助。

2. 正文

2.1 返回 json 字段不统一

接口文档中的格式如下:

{
	"result": "结果数据",
	"status": 1,
	"message": "success"
}

这时在代码中对应的数据解析类是:

data class Response (
        val message: String,
        val result: String,
        val status: Int
)

但是,后台返回的数据却是这样的:

{
	"data": "结果数据",
	"code": 1,
	"info": "success"
}

还有这样的:

{
	"datas": "结果数据",
	"state": 1,
	"info": "success"
}

如果还使用上面的 Response来解析,那么三个字段都无法被赋值,这肯定出问题的。

如果是使用 gson 来解析的话,可以把 Response 修改成下面这样,就可以解决问题:

data class Response (
        @SerializedName(value = "message", alternate = ["info"])
        val message: String,
        @SerializedName(value = "result", alternate = ["data"])
        val result: String,
        @SerializedName(value = "status", alternate = ["code", "state"])
        val status: Int
) 

上面我们使用了 gson 中的 @SerializedName 注解来解决了问题。

我们来进一步了解一下 @SerializedName的用法。

这个注解可以解决解析类的字段与 json 中的字段不匹配的问题,比如后台可能使用下划线的字段命名方式,而代码里的字段一般会采用驼峰命名,

{
	"curr_page": 1,
	"total_page": 20
}

直接转为数据解析类是:

data class DataBean(
	    val curr_page: Int,
	    val total_page: Int
)

而我们希望的数据解析类是这样的:

data class DataBean(
	    val currPage: Int,
	    val totalPage: Int
)

但是,直接写成上面的样子,是解析不到数据的,测试代码如下:

fun main() {
    val gson = Gson()
    val dataBean = gson.fromJson("{\"curr_page\":1,\"total_page\":20}", DataBean::class.java)
    println(dataBean) // 打印结果:DataBean(currPage=0, totalPage=0)
}

这时就要用到 @SerializedName 注解:

data class DataBean(
        @SerializedName("curr_page")
        val currPage: Int,
        @SerializedName("total_page")
        val totalPage: Int
)

再次运行测试代码,可以实现正常的反序列化,打印结果如下:

DataBean(currPage=1, totalPage=20)

实际上,@SerializedName 可以接收两个参数,valuealternate。其中,valueString 类型,alternateArray<String>类型。例如,本小节开头的 @SerializedName(value = "message", alternate = ["info"])message 赋值给了 value 参数,["info"] 赋值给了 alternate 参数,这里使用了Kotlin 中的命名参数的写法,这可以增加代码的可读性。

@SerializedName("curr_page") 中的 curr_page 是给 value参数赋值的。

alternate 参数的作用是把 json 转为对象的反序列过程中,给 json 中的属性提供备选。我们通过本小节开头的例子来说明:

fun main() {
    val gson = Gson()
    val normalJson = "{\"result\":\"结果数据\",\"status\":1,\"message\":\"success\"}"
    val response = gson.fromJson(normalJson, Response::class.java)
    println(response) // 打印结果:Response(message=success, result=结果数据, status=1)
    val json1 = "{\"data\":\"结果数据\",\"code\":-1,\"info\":\"error\"}"
    val response1 = gson.fromJson(json1, Response::class.java)
    println(response1) // 打印结果:Response(message=error, result=结果数据, status=-1)
}

可以看到,即便后台使用了多个不同的字段名,通过 alternate 参数指定它们,一样可以把它们统一映射为数据类中的一个字段。

那么,有同学可能会问:既然有了 alternate 参数,为什么还要 value 参数呢?

value 参数用于指定序列化的字段的名字。我们还是接着上面的例子来说明:

    // 序列化 response 和 response1
    val responseToJson = gson.toJson(response)
    println(responseToJson)
    val response1ToJson = gson.toJson(response1)
    println(response1ToJson)

打印结果如下:

{"message":"success","result":"结果数据","status":1}
{"message":"error","result":"结果数据","status":-1}

可以看到,序列化的名字由 value 参数来规定。

2.2 返回 json 数据中包含转义字符

返回的数据包含转义字符:

Mr. Anthony&#39;s Love Clinic 1

这样直接设置给 TextView 后,会照原样显示,这样的显示就会很难看了。如下图所以,

解决办法一:

使用 Html.fromHtml() 方法

tv.text = Html.fromHtml("Mr. Anthony&#39;s Love Clinic 1")

这样以后,显示在屏幕上就正常了:

Mr. Anthony's Love Clinic 1

如图所示:

但是,这种办法需要在每个给 TextView 设置文本的地方都这样处理。

可以使用下面的扩展方法:

fun String.fromHtml(): Spanned {
    return Html.fromHtml(this)
}

上面就可以写成这样,实现链式调用:

tv.text = "Mr. Anthony&#39;s Love Clinic 1".fromHtml()

解决办法二

添加依赖:

implementation group: 'org.apache.commons', name: 'commons-text', version: '1.3'

使用 StringEscapeUtils.unescapeHtml4(s) 进行反转义。

这种方法可以在拦截器里先拿到返回的 json 数据,再使用上面的方法反转义。

但是,我们客户端不得不添加这个依赖,会略微增加包体的大小。

另外,关于如何在拦截器里拿到返回的 json 数据,会在下边进行说明。

2.3 返回 json 数据中包含双引号(")

这种情况不做处理,直接去解析的话,无法解析成功。因为这不是合法的 json。

请看不合法的示例,这里的截图是把 json 放在 bejson 网站里进行格式化校验的:

本来应该服务端处理掉这个问题,但是那边想让客户端处理掉。

我的想法是这样的:

  • 先在一个地方获取到服务端返回的那一串 json;
  • 然后查找到那些导致不合法的双引号(")并把它们替换成单引号(')。

关键是第一步:先在一个地方获取到服务端返回的那一串 json

首先想到的是在 GsonConverterFactory.create()create方法中传入一个自己构建的 Gson 对象;希望在构建这个 Gson 对象时,可以拿到后台返回的 json 串。但是,这里有些麻烦。

然后,想到的是拦截器 Interceptor,希望在拦截器里拿到后端返回的 json 数据。

但是 Interceptor 是一个包含了 intercept 方法以及 Chain 接口的接口,真正的实现是要自己去完成的。那么,如何去定义一个拦截器呢?

这里,我们去查看一下 okhttp 给我们提供的 Interceptor 实现类,借鉴其实现思路。

大家使用过 HttpLoggingInterceptor 这个拦截器吧,它的作用就是打印请求和响应信息的。

先创建一个 HttpLoggingInterceptor 对象:

private val logInterceptor = HttpLoggingInterceptor {
    Timber.d(it) // 使用 Timber 日志类来打印
}

logInterceptor 添加到 OkHttpClient.Builder() 中:

OkHttpClient.Builder()
.addInterceptor(logInterceptor)

查看一下打印信息:

我在图中作了比较详细的标注,希望同学们明白的是HttpLoggingInterceptor确实具备提供 json 信息的能力。

所以,我们可以借鉴 HttpLoggingInterceptor 的实现方式来实现自己的拦截器也具备获取 json 信息的能力。

打开 HttpLoggingInterceptor的源码,不难定位到打印json信息的代码如下:

if (contentLength != 0) {
          logger.log("");
          logger.log(buffer.clone().readString(charset)); // 这行就是得到 json 信息的代码
 }

现在我们开始自定义拦截器,创建一个实现了 Interceptor 接口的 MyInterceptor 类,并重写 intercept 方法:

public class MyInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        return null;
    }
}

把我们在HttpLoggingInterceptor找到的获取 json 信息的代码buffer.clone().readString(charset),添加到自定义拦截器的 intercept方法中:

public class MyInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        String json = buffer.clone().readString(charset);
        return null;
    }
}

但是,目前我们没有 buffer 变量和 charset 变量,所以上面的两处变量是报错的。

接着,我们参照 HttpLoggingInterceptor 的代码,把获取上面两个变量的代码,以及其他需要的代码,都添加到自定义的拦截器里面。不要忘了,应该 return 的是 response

最终得到的 MyInterceptor 是这样的:

public class MyInterceptor implements Interceptor {
    private static final Charset UTF8 = Charset.forName("UTF-8");

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        Response response;
        try {
            response = chain.proceed(request);
        } catch (Exception e) {
            throw e;
        }
        Headers headers = response.headers();
        ResponseBody responseBody = response.body();

        BufferedSource source = responseBody.source();
        source.request(Long.MAX_VALUE); // Buffer the entire body.
        Buffer buffer = source.buffer();

        if ("gzip".equalsIgnoreCase(headers.get("Content-Encoding"))) {
            GzipSource gzippedResponseBody = null;
            try {
                gzippedResponseBody = new GzipSource(buffer.clone());
                buffer = new Buffer();
                buffer.writeAll(gzippedResponseBody);
            } finally {
                if (gzippedResponseBody != null) {
                    gzippedResponseBody.close();
                }
            }
        }

        Charset charset = UTF8;
        MediaType contentType = responseBody.contentType();
        if (contentType != null) {
            charset = contentType.charset(UTF8);
        }
        String json = buffer.clone().readString(charset);
        Timber.d(json); // 打印获取到的 json 信息。
        return response;
    }
}

现在把我们自定义的拦截器添加给OkHttpClient

OkHttpClient.Builder()
                .addInterceptor(MyInterceptor())

运行应用,获取打印信息如下图:
与上图绿框中的内容进行比较:它们是完全一样的。

好了,第一步已经完成了。

接着是第二步,查找到那些导致不合法的双引号(")并把它们替换成单引号('

public static String toJsonString(String s) {
    char[] tempArr = s.toCharArray();
    int tempLength = tempArr.length;
    for (int i = 0; i < tempLength; i++) {
        if (tempArr[i] == ':' && tempArr[i + 1] == '"') {
            for (int j = i + 2; j < tempLength; j++) {
                if (tempArr[j] == '"') {
                    if (tempArr[j + 1] != ',' && tempArr[j + 1] != '}') {
                        tempArr[j] = '\''; // 将value中的 双引号替换为单引号
                    } else if (tempArr[j + 1] == ',' || tempArr[j + 1] == '}') {
                        break;
                    }
                }
            }
        }
    }
    return new String(tempArr);
}

创建一个新的 Response 对象并返回:

// 创建一个新的response 对象并返回
MediaType type = response.body().contentType();
ResponseBody newRepsoneBody = ResponseBody.create(type, newJson);
Response newResponse = response.newBuilder().body(newRepsoneBody).build();

完整的自定义拦截器 MyInterceptor 代码如下:

public class MyInterceptor implements Interceptor {
    private static final Charset UTF8 = Charset.forName("UTF-8");

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        Response response;
        try {
            response = chain.proceed(request);
        } catch (Exception e) {
            throw e;
        }
        Headers headers = response.headers();
        ResponseBody responseBody = response.body();

        BufferedSource source = responseBody.source();
        source.request(Long.MAX_VALUE); // Buffer the entire body.
        Buffer buffer = source.buffer();

        if ("gzip".equalsIgnoreCase(headers.get("Content-Encoding"))) {
            GzipSource gzippedResponseBody = null;
            try {
                gzippedResponseBody = new GzipSource(buffer.clone());
                buffer = new Buffer();
                buffer.writeAll(gzippedResponseBody);
            } finally {
                if (gzippedResponseBody != null) {
                    gzippedResponseBody.close();
                }
            }
        }

        Charset charset = UTF8;
        MediaType contentType = responseBody.contentType();
        if (contentType != null) {
            charset = contentType.charset(UTF8);
        }
        String json = buffer.clone().readString(charset);
        Timber.d(json);
        String newJson = Utils.toJsonString(json);
        // 创建一个新的response 对象并返回
        MediaType type = response.body().contentType();
        ResponseBody newRepsoneBody = ResponseBody.create(type, newJson);
        Response newResponse = response.newBuilder().body(newRepsoneBody).build();
        return newResponse;
    }
}

再次运行程序,可以正常运行。

2.4 返回 json 数据格式不对

期望的 json 数据是:

{
	"result": {
		"name": "All Around Weekly"
	},
	"status": 1,
	"message": "success"
}

实际得到的却是这样的:

{
	"result": {
		"name": "All Around Weekly",
		"status": 1,
		"message": "success"
	}
}

这种问题,仍然可以用 2.3 节中自定义拦截器的思路来处理。
拿到 json 字符串后,作如下处理:

try {
    JSONObject jsonObject = new JSONObject(json);
    JSONObject result = jsonObject.getJSONObject("result");
    if (result != null) {
        if (result.has("status")) {
            jsonObject.put("status", result.get("status"));
            result.remove("status");
        }
        if (result.has("message")) {
            jsonObject.put("message", result.get("message"));
            result.remove("message");
        }
    }
    System.out.println(jsonObject.toString());
} catch (JSONException e) {
    e.printStackTrace();
}

3. 最后

本文重点介绍了参考源码来自定义拦截器获取 json 字符串的思路,以及几种日常开发中遇到的实际案例。希望对大家有所帮助。

参考

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

willwaywang6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值