遇到問題
最近得到使用者反饋,有些介面請求資料失敗,除錯介面發現,是後臺返回的型別不確定導致的,例如: 這是一段我們需要的正常json:
{
"id":1,
"number":100000000,
"user":{
"name":"aoteman",
"userId":110
}
}
複製程式碼
但是後臺有時會返回這樣一段json:
{
"id":"",
"number":"",
"user":""
}
複製程式碼
可以發現,本來是int型的id,long型的number和Object的型別的user都變成了空字串。只要是返回結果為null的,都有可能返回成空字串,可以猜到後臺可能是使用了PHP這種弱型別語言,並且沒有對欄位做型別校驗。這本來應該是後臺的鍋,但是部門之間溝通困難,只能先我們客戶端解決了。
解決方法
Gson在GsonBuilder提供了registerTypeAdapter這個方法,讓我們對特定的模型進行自定義序列化和反序列化。這裡提供了倆種序列化和反序列化的方式:
- 第一種是繼承自JsonSerializer和JsonDeserializer介面,我們主要看JsonDeserializer,程式碼如下:
public class Deserializer implements JsonDeserializer<Test> {
@Override
public Test deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
throws JsonParseException {
final JsonObject jsonObject = json.getAsJsonObject();
final JsonElement jsonId = jsonObject.get("id");
final JsonElement jsonNumber = jsonObject.get("number");
final Test test = new Test();
try {
test.setId(jsonId.getAsInt());
}catch (Exception e){
//id不是int型的時候,捕獲異常,並且設定id的值為0
test.setId(0);
}
try {
test.setNumber(jsonNumber.getAsLong());
}catch (Exception e){
test.setNumber(0);
}
return test;
}
}
複製程式碼
這種方式類似於查字典,gson會把json先轉換成JsonElement的結構,JsonElement有4種子類,JsonObject(物件結構)、JsonArray(陣列結構)、JsonPrimitive(基本型別)、JsonNull。 JsonObject內部其實維護了一個HashMap,jsonObject.get("id");其實就是查欄位。這種方法對效能的消耗比較大,因為它需要先把流資料轉換成JsonElement的結構物件,這樣會產生更大的記憶體消耗和執行時間。
- 第二種是繼承自TypeAdapter,程式碼如下:
new GsonBuilder().registerTypeAdapter(Test.class,
new TypeAdapter<Test>() {
public Test read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
in.beginObject();
Test test = new Test();
while (in.hasNext()) {
switch (in.nextName()) {
case "id":
try {
test.setId(in.nextInt());
} catch (Exception e) {
in.nextString();
test.setId(0);
}
break;
case "number":
try {
test.setNumber(in.nextLong());
} catch (Exception e) {
in.nextString();
test.setNumber(0);
}
break;
}
}
return test;
}
public void write(JsonWriter out, Test src) throws IOException {
if (src == null) {
out.nullValue();
return;
}
}
})
複製程式碼
這種方式相比JsonElement更加高效,因為它直接是用流來解析資料,去掉了JsonElement這個中介軟體,它流式的API相比於第一種的樹形解析API將會更加高效。
那麼問題的解決辦法可以這樣,如下程式碼:
//int型別的解析器
private static TypeAdapter<Number> INTEGER = new TypeAdapter<Number>() {
@Override
public Number read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
try {
return in.nextInt();
} catch (NumberFormatException e) {
//這裡解析int出錯,那麼捕獲異常並且返回預設值,因為nextInt出錯中斷了方法,沒有完成位移,所以呼叫nextString()方法完成位移。
in.nextString();
return 0;
}
}
@Override
public void write(JsonWriter out, Number value) throws IOException {
out.value(value);
}
};
private static TypeAdapter<Number> LONG = new TypeAdapter<Number>() {
@Override
public Number read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
//這裡同
try {
return in.nextLong();
} catch (Exception e) {
in.nextString();
}
return 0;
}
@Override
public void write(JsonWriter out, Number value) throws IOException {
out.value(value);
}
};
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(TypeAdapters.newFactory(int.class, Integer.class, INTEGER))
.registerTypeAdapterFactory(TypeAdapters.newFactory(long.class, Long.class, LONG))
.create();
複製程式碼
以上程式碼可以解決int和long型別的返回空字串問題。但是自定義型別怎麼辦,不可能把每種型別都註冊進來,我們先看看Gson是怎麼做的,檢視Gson的構造方法,發現下面一段程式碼:
factories.add(new ReflectiveTypeAdapterFactory(
constructorConstructor, fieldNamingPolicy, excluder));
複製程式碼
檢視ReflectiveTypeAdapterFactory內容發現,這個是對所有使用者定義型別的解析器。修改問題主要定位在ReflectiveTypeAdapterFactory類中的Adapter的read方法。我們只要修改read方法就可以解決問題:
@Override
public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
T instance = constructor.construct();
//這裡對beginObject進行異常捕獲,如果不是object,說明可能是"",直接返回null,不中斷解析
try {
in.beginObject();
} catch (Exception e) {
in.nextString();
return null;
}
try {
int count = 0;
while (in.hasNext()) {
count++;
String name = in.nextName();
BoundField field = boundFields.get(name);
if (field == null || !field.deserialized) {
in.skipValue();
} else {
field.read(in, instance);
}
}
if (count == 0) return null;
} catch (IllegalStateException e) {
throw new JsonSyntaxException(e);
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
in.endObject();
return instance;
}
複製程式碼
ReflectiveTypeAdapterFactory是個final類不能繼承,並且構造方法有幾個重要引數從Gson傳入,所以我們只能把ReflectiveTypeAdapterFactory複製一份出來修改,並且利用反射修改Gson,吧修改的類替換掉原來的,程式碼如下:
public static Gson buildGson() {
Gson gson = new GsonBuilder().registerTypeAdapterFactory(TypeAdapters.newFactory(int.class, Integer.class, INTEGER))
.registerTypeAdapterFactory(TypeAdapters.newFactory(long.class, Long.class, LONG))
.create();
try {
Field field = gson.getClass().getDeclaredField("constructorConstructor");
field.setAccessible(true);
ConstructorConstructor constructorConstructor = (ConstructorConstructor) field.get(gson);
Field factories = gson.getClass().getDeclaredField("factories");
factories.setAccessible(true);
List<TypeAdapterFactory> data = (List<TypeAdapterFactory>) factories.get(gson);
List<TypeAdapterFactory> newData = new ArrayList<>(data);
newData.remove(data.size() - 1);
newData.add(new MyReflectiveTypeAdapterFactory(constructorConstructor, FieldNamingPolicy.IDENTITY, Excluder.DEFAULT));
newData = Collections.unmodifiableList(newData);
factories.set(gson, newData);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return gson;
}
複製程式碼
問題到此得到解決,因為專案很少用到float、byte等型別的欄位,所以就沒有適配,如果有需要也可以通過以上方式解決。 示例程式碼託管在github上。