目录
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
可以接收两个参数,value
和alternate
。其中,value
是 String
类型,alternate
是Array<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's Love Clinic 1
这样直接设置给 TextView
后,会照原样显示,这样的显示就会很难看了。如下图所以,
解决办法一:
使用 Html.fromHtml()
方法
tv.text = Html.fromHtml("Mr. Anthony's Love Clinic 1")
这样以后,显示在屏幕上就正常了:
Mr. Anthony's Love Clinic 1
如图所示:
但是,这种办法需要在每个给 TextView
设置文本的地方都这样处理。
可以使用下面的扩展方法:
fun String.fromHtml(): Spanned {
return Html.fromHtml(this)
}
上面就可以写成这样,实现链式调用:
tv.text = "Mr. Anthony'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 字符串的思路,以及几种日常开发中遇到的实际案例。希望对大家有所帮助。