前言
最近使用Retrofit进行网络请求时,自带Gson解析json时遇到一个问题,返回的json数据中某个字段可能为jsonArray,也可能是jsonObject,也有可能为空(即同一个字段,返回可能是对象,数组,null)。
错误回顾
测试数据:
如果返回的数据类型有两种以上,但你定义json的实体类bean属性类型时,可能只使用了jsonObject或者jsonArray,网络请求时会报如下错误:
Caused by: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 4 column 514 path $.CanLoanBook
at com.google.gson.stream.JsonReader.beginArray(JsonReader.java:350)
at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:80)
at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:61)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:22
上面出错原因是字段CanLoanBook使用了数组进行映射,但CanLoanBook实际是对象。根据上面错误提示可能你会这样做:
把实体类的字段从原来的
private List<CanLoanBookBean> CanLoanBook;
改为如下:
private CanLoanBookBean CanLoanBook;
上面确实临时 解决了问题,但当后端CanLoanBook返回是数组时,上面还是会报错,不妨自己试一下。
另外,网上也有一些方案通过自定义Gson序列化器或者解析异常时,使用另一种是实体类处理,这两种方式要么处理麻烦,要么产生多余的类。
解决方案
1、与后端协商,规范数据格式,保证返回字段类型不变
这种情况虽然处理起来最省事,得到期望结果,但是后端的大佬可能不会随便改动数据接口,让你想办法解决。严格来说,这种字段数据类型变化是不规范化的。
2、把数据映射为ResponseBody再解析
扫描二维码关注公众号,回复: 11578429 查看本文章
这种情况要先把ResponseBody转成字符串,再采用对应框架,逐个字段解析封装到对应实体,然后需要对特殊字段进行数据类型判断。这种方案虽然可取,但解析麻烦,费时,显然不给力。
下面顺便介绍下两种方法怎么对json数据字段类型判断:
1.使用Android自带的JSONTokener解析
try {
JSONObject jsonObject = JSONObject(jsonStr);
String canLoanBook = jsonObject.getString("CanLoanBook");
Object obj = new JSONTokener(canLoanBook).nextValue();
if (obj instanceof JSONArray) {
JSONArray jsonArray = (JSONArray) obj;
for (int k = 0; k < jsonArray.length(); k++) {
JSONObject parameterObject = jsonArray.getJSONObject(k);
//开始逐个字段解析,封装到对应bean,省略
}
} else if (obj instanceof JSONObject) {
JSONObject jObj = (JSONObject) obj;
//开始逐个字段解析,封装到对应bean,省略
}
} catch (JSONException e) {
e.printStackTrace();
}
2.使用Gson框架解析
Gson gson = new Gson();
JsonParser parser = new JsonParser();
JsonElement element = parser.parse(jsonStr);
if (element.isJsonObject()) {//假设最外层是object
// 把JsonElement对象转换成JsonObject
JsonObject JsonObject = element.getAsJsonObject();
JsonElement jsonElement = JsonObject.get("CanLoanBook");
if (jsonElement.isJsonObject()) {
CanLoanBookBean canLoanBookBean = gson.fromJson(jsonElement, CanLoanBookBean.class);
} else if (jsonElement.isJsonArray()) {
Type type = new TypeToken<List<CanLoanBookBean>>() {}.getType();
// 把JsonElement对象转换成JsonArray
List<CanLoanBookBean> list = gson.fromJson(jsonElement, type);
}
}
3、把实体类特殊字段的数据类型改为Object再转换
也就是说不管json字段是对象还是数组,统一使用Object接收数据,这样不会出现异常。
/**
* 1.不管是jsonArray还是jsonObject,统一用Object接收
* 2.使用Gson对CanLoanBook字段统一换成List,反射赋值CanLoanBook
* 3.最后对CanLoanBook强转成对应类型
*/
private Object CanLoanBook;
这种方式需要再用gson对Object对象进行手动解析,可以参考上面方案2的第2种解析方式。
附上转换方法:
/**
* 转换和赋值
*
* @param sourceObj 原数据对象
* @param methodName 需要赋值原数据对象的方法名
* @param filedObjValue 需要处理的字段对象值
* @param filedMapClass 处理的字段对象映射Class
*/
public static void performTransformWithEvaluation(Object sourceObj, String methodName, Object filedObjValue,
Class filedMapClass) {
List<Object> listObjects = performTransform(filedObjValue, filedMapClass);
Class<?> clazz = sourceObj.getClass();
Method method = null;
try {
method = clazz.getMethod(methodName, Object.class);
method.invoke(sourceObj, listObjects);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 统一转换成List输出,对于基本类型,取出集合中的值即可
*
* @param filedObjValue 需要处理的字段对象值
* @param filedMapClass 处理的字段对象映射Class
* @return
*/
public static <T> List<T> performTransform(Object filedObjValue, Class filedMapClass) {
List<T> beanList = new ArrayList<T>();
Gson gson = new Gson();
String jsonStr = gson.toJson(filedObjValue);
JsonParser parser = new JsonParser();
JsonElement element = parser.parse(jsonStr);
if (element.isJsonObject()) {
// 把JsonElement对象转换成JsonObject
T t = (T) gson.fromJson(element, filedMapClass);
beanList.add(t);
} else if (element.isJsonArray()) {
//下面会导致T为LinkedTreeMap,说明Gson解析时不支持泛型
// Type type = new TypeToken<List<T>>() {}.getType();
// // 把JsonElement对象转换成JsonArray
// List<T> list = gson.fromJson(element, type);
List<T> list = jsonToList(element,filedMapClass);
beanList.addAll(list);
} else if (element.isJsonPrimitive()) {
T t = (T) gson.fromJson(element, filedMapClass);
beanList.add(t);
} else {
// element.isJsonNull()
return null;
}
return beanList;
}
/**
* 通过json字符串转List
* @param json
* @param clazz
* @param <T>
* @return
*/
public static <T> List<T> jsonToList(String json, Class clazz) {
Type type = new ParameterizedTypeImpl(clazz);
List<T> list = new Gson().fromJson(json, type);
return list;
}
/**
* 通过element转List
* @param element
* @param clazz
* @param <T>
* @return
*/
public static <T> List<T> jsonToList(JsonElement element, Class clazz) {
Type type = new ParameterizedTypeImpl(clazz);
List<T> list = new Gson().fromJson(element, type);
return list;
}
/**
* 自定义ParameterizedType
*/
private static class ParameterizedTypeImpl implements ParameterizedType {
Class clazz;
public ParameterizedTypeImpl(Class clz) {
clazz = clz;
}
/**
* 返回实际类型组成的数据,比如Map<String,Long> map的类型是:java.lang.String、java.lang.Long
* @return
*/
@Override
public Type[] getActualTypeArguments() {
return new Type[] { clazz };
}
/**
* 返回原生类型,比如Map<String,Long> map的原生类型为java.util.Map
* @return
*/
@Override
public Type getRawType() {
return List.class;
}
/**
* 返回 Type 对象,表示此类型是其成员之一的类型,比如Map.Entry<Long,Short> map的OwnerType为java.util.Map
* @return
*/
@Override
public Type getOwnerType() {
return null;
}
}
关于参数化类型ParameterizedType(除此之外,还有TypeVariable、GenericArrayType、WildcardType,其父接口Type是Java 编程语言中所有类型的公共高级接口) 的用法这里就不多说,在Android开发中很常见,比如:MVP模式中BaseActivity,经常指定一个泛型P(Presenter),让子类继承BaseActivity并制定具体的Presenter;RecyclerView.Adapter等等。
基本使用示例:
BookBean bookBean = gson.fromJson(jsonStr, BookBean.class);
performTransformWithEvaluation(bookBean, "setCanLoanBook", bookBean.getCanLoanBook(), CanLoanBookBean.class);
List<CanLoanBookBean> canLoanBookBeans= (List<CanLoanBookBean>) bookBean.getCanLoanBook();
System.out.println("canLoanBookBeans:" + canLoanBookBeans);
在Retrofit使用示例:
RetrofitUtils.getInstance().getService().getBookDetail(v_recno, v_curtable, time)
.map((Function<BookDetail, BookDetail>) bookDetail -> {
JsonUtils.performTransformWithEvaluation(bookDetail,"setCanLoanBook",bookDetail.getCanLoanBook(),BookDetail.CanLoanBookBean.class);
return bookDetail;
})
.compose(RxSchedulers.<BookDetail>applySchedulers())
.subscribe((Consumer<BookDetail>) bookDetail -> {
}, (Consumer<Throwable>) throwable -> {
});
上面还可以再优化一下,自定义一个Gson反序列化器,把上述方法放进序列化器中,这样网络请求后,不需要对数据进行map转换。
方案对比
方案1:是最省事的,不建议后端数据接口同字段返回不同类型数据(适合字段数据类型固定,正常情况都是这种数据)
方案2:解析费时费力,完全手动解析(适合大量字段数据类型变化的情况)
方案3(建议):不用改Retrofit现有Gson转换器,只需要对特殊字段处理即可(适合少量字段数据类型变化的情况,如果都变化,这样的json数据存在意义不大)
总结
本次就是开发过程中遇到的问题,只要网络请求出现类似的问题:同字段返回不同类型数据,解析框架出现IllegalStateException,都可以用上述方案3解决。