前言
最近使用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再解析
這種情況要先把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解決。