Gson針對API返回欄位型別不確定的解決辦法

武動臨城發表於2018-08-07

遇到問題

最近得到使用者反饋,有些介面請求資料失敗,除錯介面發現,是後臺返回的型別不確定導致的,例如: 這是一段我們需要的正常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上。

相關文章