Gson 增加額外校驗功能

貓尾巴發表於2018-11-08

Gson使用中遇到的幾種情況

  1. 解析完資料,使用時經常需要對資料判空,以免後臺返回資料格式有誤導致客戶端Crash,非常繁瑣,且容易遺漏。
  2. 解析完資料除了判空,有時還要對一些欄位有效性做判斷(例如id有效)。能否在解析過程中直接過濾掉無效異常資料,用的時候不需要再判斷呢?
  3. 介面返回的資料自動解析後,經常和最終需要的資料格式不完全一致,需要進行預處理。例如User的名稱返回空串,則需要在客戶端顯示成“匿名”;再例如介面返回二維陣列,而UI元件需要的是帶有Type資訊的一維陣列來實現分組列表。

解析的資料免判空

修改資料實現類

在資料Model實現類中過濾掉null值。例如設計一個List,只有非null值才能被新增進去,取出Item時就不需要判空了。

可以封裝一個ItemNonNullList,在ArrayList外面包一層,並特殊處理add/addAll相關方法,保證為null的Item不會被新增進去。不需要自定義TypeAdapter修改Gson解析過程。

public class ItemNonNullList<E> implements List<E>, RandomAccess, Cloneable, Serializable {

    @NotNull
    private final ArrayList<E> mList;

    public ItemNonNullList() {
        mList = new ArrayList<>();
    }

    @SuppressWarnings("unchecked")
    private ItemNonNullList(@NotNull ItemNonNullList<E> other) {
        mList = (ArrayList<E>) other.mList.clone();
    }

    @Override
    public int size() {
        return mList.size();
    }

    @Override
    public boolean isEmpty() {
        return mList.isEmpty();
    }

    @Override
    public boolean contains(Object o) {
        return mList.contains(o);
    }

    @NotNull
    @Override
    public Iterator<E> iterator() {
        return mList.iterator();
    }

    @NotNull
    @Override
    public Object[] toArray() {
        return mList.toArray();
    }

    @NotNull
    @Override
    public <T> T[] toArray(@NotNull T[] a) {
        return mList.toArray(a);
    }

    @Override
    public boolean add(E e) {
        return e != null && mList.add(e);
    }

    @Override
    public boolean remove(Object o) {
        return mList.remove(o);
    }

    @Override
    public boolean containsAll(@NotNull Collection<?> c) {
        return mList.containsAll(c);
    }

    @Override
    public boolean addAll(@NotNull Collection<? extends E> c) {
        mList.ensureCapacity(mList.size() + c.size());
        boolean result = false;
        for (E e : c) {
            result |= e != null && mList.add(e);
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    @Override
    public boolean addAll(int index, @NotNull Collection<? extends E> c) {
        ArrayList<E> list = null;
        for (Object o : c) {
            if (o != null) {
                if (list == null) {
                    list = new ArrayList<>(c.size());
                }
                list.add((E) o);
            }
        }
        return list != null && mList.addAll(index, list);
    }

    @Override
    public boolean removeAll(@NotNull Collection<?> c) {
        return mList.removeAll(c);
    }

    @Override
    public boolean retainAll(@NotNull Collection<?> c) {
        return mList.retainAll(c);
    }

    @Override
    public void clear() {
        mList.clear();
    }

    @NotNull
    @Override
    public E get(int index) {
        return mList.get(index);
    }

    @Nullable
    @Override
    public E set(int index, E element) {
        return element == null ? null : mList.set(index, element);
    }

    @Override
    public void add(int index, E element) {
        if (element != null) {
            mList.add(index, element);
        }
    }

    @NotNull
    @Override
    public E remove(int index) {
        return mList.remove(index);
    }

    @Override
    public int indexOf(Object o) {
        return mList.indexOf(o);
    }

    @Override
    public int lastIndexOf(Object o) {
        return mList.lastIndexOf(o);
    }

    @NotNull
    @Override
    public ListIterator<E> listIterator() {
        return mList.listIterator();
    }

    @NotNull
    @Override
    public ListIterator<E> listIterator(int index) {
        return mList.listIterator(index);
    }

    @NotNull
    @Override
    public List<E> subList(int fromIndex, int toIndex) {
        return mList.subList(fromIndex, toIndex);
    }

    @SuppressWarnings("MethodDoesntCallSuperMethod")
    @Override
    public ItemNonNullList<E> clone() {
        return new ItemNonNullList<>(this);
    }
}
複製程式碼

全域性替換解析過程

對於特定型別,全域性替換Gson解析過程。例如String型別null解析為空串"";陣列、List解析為空陣列、空List而不是null等。

以String為例,可以把TypeAdapters.STRING複製出來,並修改其中程式碼如下,將null解析為空字串,然後註冊到Gson中,覆蓋String預設的TypeAdapter。

修改前:

public static final TypeAdapter<String> STRING = new TypeAdapter<String>() {
    @Override
    public String read(JsonReader in) throws IOException {
        JsonToken peek = in.peek();
        if (peek == JsonToken.NULL) {
            in.nextNull();
            return null;
        }
        /* coerce booleans to strings for backwards compatibility */
        if (peek == JsonToken.BOOLEAN) {
            return Boolean.toString(in.nextBoolean());
        }
        return in.nextString();
    }
    
    @Override
    public void write(JsonWriter out, String value) throws IOException {
        out.value(value);
    }
};
複製程式碼

修改後:

public static final TypeAdapter<String> STRING = new TypeAdapter<String>() {
    public String read(JsonReader reader) {
        try {
            if (reader.peek() == JsonToken.NULL) {
                reader.nextNull();
                return ""; // 原先是返回null,這裡改為返回空字串
            }
            return reader.nextString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }
    public void write(JsonWriter writer, String value) {
        try {
            if (value == null) {
                writer.nullValue();
                return;
            }
            writer.value(value);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
};
複製程式碼

類似的,還可以覆蓋Gson內建的TypeAdapters.INTEGER、CollectionTypeAdapterFactory、ArrayTypeAdapter等,實現Integer、Collection、陣列等型別的免判空。

public static final 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) {
        throw new JsonSyntaxException(e);
      }
    }
    @Override
    public void write(JsonWriter out, Number value) throws IOException {
      out.value(value);
    }
};

/**
 * 自定義adapter,解決由於資料型別為Int,實際傳過來的值為Float,導致解析出錯的問題
 * 目前的解決方案為將所有Int型別當成Double解析,再強制轉換為Int
 */
public static final TypeAdapter<Number> INTEGER = new TypeAdapter<Number>() {
    @Override
    public Number read(JsonReader in) throws IOException {
        if (in.peek() == JsonToken.NULL) {
            in.nextNull();
            return 0;
        }
        try {
            double i = in.nextDouble();//當成double來讀取
            return (int) i;//強制轉為int
        } catch (NumberFormatException e) {
            throw new JsonSyntaxException(e);
        }
    }

    @Override
    public void write(JsonWriter out, Number value) throws IOException {
        out.value(value);
    }
};
複製程式碼

陣列部分略麻煩,由於gson用以陣列解析的Adapter是不可重寫的,只好拷貝出來,重新寫了個類。注意上面的TypeAdapterRuntimeTypeWrapper類不是public的,所以也得拷貝出來寫一個到本地。

/**
 * 自定義CollectionTypeAdapterFactory,使json內的陣列為null時,返回空陣列而不是null物件
 */
public final class CollectionTypeAdapterFactory implements TypeAdapterFactory {
    private final ConstructorConstructor constructorConstructor;

    public CollectionTypeAdapterFactory(ConstructorConstructor constructorConstructor) {
        this.constructorConstructor = constructorConstructor;
    }

    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
        Type type = typeToken.getType();

        Class<? super T> rawType = typeToken.getRawType();
        if (!Collection.class.isAssignableFrom(rawType)) {
            return null;
        }

        Type elementType = $Gson$Types.getCollectionElementType(type, rawType);
        TypeAdapter<?> elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType));
        ObjectConstructor<T> constructor = constructorConstructor.get(typeToken);

        @SuppressWarnings({"unchecked", "rawtypes"}) // create() doesn't define a type parameter
                TypeAdapter<T> result = new Adapter(gson, elementType, elementTypeAdapter, constructor);
        return result;
    }

    private static final class Adapter<E> extends TypeAdapter<Collection<E>> {
        private final TypeAdapter<E> elementTypeAdapter;
        private final ObjectConstructor<? extends Collection<E>> constructor;

        public Adapter(Gson context, Type elementType,
                       TypeAdapter<E> elementTypeAdapter,
                       ObjectConstructor<? extends Collection<E>> constructor) {
            this.elementTypeAdapter =
                    new TypeAdapterRuntimeTypeWrapper<E>(context, elementTypeAdapter, elementType);
            this.constructor = constructor;
        }

        public Collection<E> read(JsonReader in) throws IOException {
            if (in.peek() == JsonToken.NULL) {
                in.nextNull();
                //這裡做了修改,原先是返回null,改為返回空陣列
                return constructor.construct();
            }

            Collection<E> collection = constructor.construct();
            in.beginArray();
            while (in.hasNext()) {
                E instance = elementTypeAdapter.read(in);
                collection.add(instance);
            }
            in.endArray();
            return collection;
        }

        public void write(JsonWriter out, Collection<E> collection) throws IOException {
            if (collection == null) {
                out.nullValue();
                return;
            }

            out.beginArray();
            for (E element : collection) {
                elementTypeAdapter.write(out, element);
            }
            out.endArray();
        }
    }
}
複製程式碼

進行註冊:

static {
        GsonBuilder gsonBulder = new GsonBuilder();
        gsonBulder.registerTypeAdapter(String.class, STRING);   //所有String型別null替換為字串“”
        gsonBulder.registerTypeAdapter(int.class, INTEGER); //int型別對float做相容

        //通過反射獲取instanceCreators屬性
        try {
            Class builder = (Class) gsonBulder.getClass();
            Field f = builder.getDeclaredField("instanceCreators");
            f.setAccessible(true);
            Map<Type, InstanceCreator<?>> val = (Map<Type, InstanceCreator<?>>) f.get(gsonBulder);//得到此屬性的值
            //註冊陣列的處理器
            gsonBulder.registerTypeAdapterFactory(new CollectionTypeAdapterFactory(new ConstructorConstructor(val)));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        gson = gsonBulder.create();
    }
複製程式碼

定製化解析

全域性替換的方式比較粗暴,對於複雜工程可能會引起預料不到的問題,可以結合註解等方式,對指定的元素進行特殊處理。

設計一個NonNullField欄位,註解到自定義類的成員變數上,可以確保解析時該欄位不會為null,會使用預設的例項來代替。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface NonNullField {
    Class<? extends InstanceCreator> value() default NonNullFieldConstructor.class;
}
複製程式碼

NonNullField註解通過NonNullFieldFactory實現如下。此Factory在建立TypeAdapter時,先搜尋Class和父類中包含NonNullField註解的成員變數:

  1. 如果沒找到,則返回null,由其他Factory建立TypeAdapter;
  2. 如果找到了,則在DelegateAdapter外包裹一層,DelegateAdapter解析完成後呼叫replaceNonNullFields方法,將NonNullField的null值替換為預設例項。
public class NonNullFieldFactory implements TypeAdapterFactory {

    private static final String ANNOTATION_NAME = NonNullField.class.getSimpleName();
    /**
     * 儲存Type及其對應的NonNullField
     */
    private static final Map<Type, List<Field>> fieldMap = new ConcurrentHashMap<>();
    /**
     * InstanceCreator快取
     */
    private static final Map<Class<? extends InstanceCreator>, InstanceCreator> creatorCache = new ConcurrentHashMap<>();

    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {

        List<Field> fields = findMatchedFields(typeToken);
        final Type type = typeToken.getType();

        // 如果找到了,則包裹一層Adapter
        if (fields != null && !fields.isEmpty()) {
            fieldMap.put(type, fields);

            final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, typeToken);
            log("create wrapper adapter, type = %s, find %d fields, delegate = %s", typeToken, fields.size(), delegate);
            return new TypeAdapter<T>() {
                @Override
                public void write(JsonWriter out, T value) throws IOException {
                    delegate.write(out, value);
                }

                @Override
                public T read(JsonReader in) throws IOException {
                    T t = delegate.read(in);
                    log("  finish read, data = %s, type = %s, delegate = %s", t, type, delegate);
                    replaceNonNullFields(t, typeToken);
                    return t;
                }
            };
        }
        return null;
    }

    private static void log(String msg, Object... args) {
        L.d(GsonUtils.TAG, "[NonNullFieldFactory]  " + msg, args);
    }

    /**
     * 是否需要搜尋Type中的Field
     */
    @SuppressWarnings("RedundantIfStatement")
    private static boolean shouldSearch(Class clazz) {
        // 跳過不需要搜尋的類
        if (clazz == null || clazz == Object.class || clazz.isPrimitive() || clazz.isEnum() || clazz.isArray()) {
            log("skip search class %s", clazz);
            return false;
        }
        // 跳過Java和Android系統中的類
        String packageName = clazz.getPackage().getName();
        if (packageName.startsWith("java") || packageName.startsWith("android")) {
            log("skip search class %s by package", clazz);
            return false;
        }
        // 只匹配特定的類、跳過其他第三方庫的類……
        return true;
    }

    /**
     * 找到某個Type中的NonNullField,包括繼承的
     */
    private static List<Field> findMatchedFields(TypeToken typeToken) {
        List<Field> list = null;
        Class raw = typeToken.getRawType();
        while (shouldSearch(raw)) {
            Field[] fields = raw.getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true);
                if (field.getAnnotation(NonNullField.class) != null) {
                    if (list == null) {
                        list = new ArrayList<>();
                    }
                    list.add(field);
                }
            }
            // 解析父類
            typeToken = TypeToken.get($Gson$Types.resolve(typeToken.getType(), typeToken.getRawType(), raw.getGenericSuperclass()));
            raw = typeToken.getRawType();
        }
        return list == null ? Collections.EMPTY_LIST : list;
    }

    /**
     * 解析Field的Type,處理泛型引數
     *
     * @param typeToken Field所在類的Type
     * @param field     要解析的Field
     */
    private static Type resolveFieldType(TypeToken typeToken, Field field) {
        return $Gson$Types.resolve(typeToken.getType(), typeToken.getRawType(), field.getGenericType());
    }

    /**
     * 填充物件中的NonNullField
     */
    private static void replaceNonNullFields(Object o, TypeToken typeToken) {
        if (o == null) {
            return;
        }
        // 對於巢狀註解的情況(NonNullField對應型別中又有NonNullField),
        // 由於Gson會先解析內部資料,其TypeAdapter已經建立,此處map可以取到值
        List<Field> fields = fieldMap.get(typeToken.getType());
        if (fields == null || fields.isEmpty()) {
            return;
        }
        for (Field field : fields) {
            try {
                Object fieldValue = field.get(o);
                if (fieldValue == null) {
                    Object value = constructField(field, resolveFieldType(typeToken, field));
                    if (value == null) {
                        throw new RuntimeException(String.format("Create field %s for type %s failure",
                                field.getName(), typeToken.getType()));
                    }
                    field.set(o, value);
                    log("    --> set field '%s.%s' to '%s'", typeToken.getType().getTypeName(), field.getName(), value);
                }
            } catch (IllegalArgumentException | IllegalAccessException e) {
                L.e(e);
            }
        }
    }

    private static Object constructField(Field field, Type type) {
        NonNullField annotation = field.getAnnotation(NonNullField.class);
        Class<? extends InstanceCreator> creatorClass = annotation.value();
        InstanceCreator creator = getCreator(creatorClass);
        Object instance = creator.createInstance(type);
        replaceNonNullFields(instance, TypeToken.get(type));
        return instance;
    }

    private static synchronized InstanceCreator getCreator(Class<? extends InstanceCreator> creatorClass) {
        InstanceCreator creator = creatorCache.get(creatorClass);
        if (creator == null) {
            try {
                creator = creatorClass.newInstance();
                creatorCache.put(creatorClass, creator);
            } catch (InstantiationException | IllegalAccessException e) {
                throw new RuntimeException("InstanceCreator " + creatorClass + " create failure", e);
            }
        }
        return creator;
    }
}
複製程式碼

在replaceNonNullFields方法中,呼叫InstanceCreator建立例項,然後通過反射設定給Java物件。建立預設例項時,還會遞迴替換預設例項中巢狀的NonNullField,從而支援巢狀class。

InstanceCreator可由NonNullField註解指定,預設值為NonNullFieldConstructor。NonNullFieldConstructor中先判斷如果是基本型別或陣列,則直接建立,否則呼叫Gson內部的ConstructorConstructor工具類建立例項。

public class NonNullFieldConstructor implements InstanceCreator<Object> {
    /**
     * 儲存基本型別及其預設值。基本型別預設值的內容不能被修改,因此可以重複利用,賦值給多個Field。
     */
    private static final Map<Class, Object> basicMap = new HashMap<>();
    /**
     * Gson的Constructor
     */
    private static final ConstructorConstructor constructor = new ConstructorConstructor(new HashMap<>());

    static {
        basicMap.put(Boolean.class, false);
        basicMap.put(Byte.class, (byte) 0);
        basicMap.put(Character.class, (char) 0);
        basicMap.put(Short.class, (short) 0);
        basicMap.put(Integer.class, 0);
        basicMap.put(Long.class, 0L);
        basicMap.put(Float.class, 0F);
        basicMap.put(Double.class, (double) 0);
        basicMap.put(String.class, "");
    }

    @Override
    public Object createInstance(Type type) {
        if (type instanceof Class) {
            Object o = basicMap.get(type);
            if (o != null) { // Integer.class
                return o;
            } else if (((Class) type).isArray()) { // String[].class
                return Array.newInstance($Gson$Types.getRawType(((Class) type).getComponentType()), 0);
            }
        } else if (type instanceof GenericArrayType) { // String[]
            return Array.newInstance($Gson$Types.getRawType(((GenericArrayType) type).getGenericComponentType()), 0);
        }
        // 其他型別使用constructor建立
        TypeToken<?> typeToken = TypeToken.get(type);
        return constructor.get(typeToken).construct();
    }
}
複製程式碼

註冊:

Gson gson = new GsonBuilder().registerTypeAdapterFactory(new NonNullFieldFactory()).create();
複製程式碼

解析後校驗資料

可以設計一個IDataValidateAction介面定義如下。自動解析完成後,如果物件實現了這個介面,Gson就會呼叫isDataValid校驗資料,如果資料無效,則直接過濾掉這個物件,返回null。

public interface IDataValidateAction {
    boolean isDataValid();
}
複製程式碼
public static class User implements IAfterDeserializeAction {

    private long id;
    private String name;

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    @Override
    public void doAfterDeserialize() {
        if (name == null || name.length() == 0) {
            name = "匿名";
        }
    }
}

public static class ValidUser extends User implements IDataValidateAction {

    @Override
    public boolean isDataValid() {
        // 如果id為0或負值,說明介面異常,視為無效資料,丟棄不用
        return getId() > 0;
    }
}
複製程式碼

只需要給Gson註冊一個DeserializeActionAdapterFactory即可。這個Factory會判斷如果Type實現了DeserializeAction相關介面,則在DelegateAdapter外包裹一層進行相應的處理;否則直接返回DelegateAdapter。

public class DeserializeActionFactory implements TypeAdapterFactory {

    public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {

        // 獲取其他低優先順序Factory建立的DelegateAdapter
        final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);

        // 如果type實現了DeserializeAction,則返回包裹後的TypeAdapter
        if (shouldWrap(type.getRawType())) {

            L.d(GsonUtils.TAG, "[DeserializeAction] create return new adapter, type = %s, delegate = %s", type, delegate);
            return new TypeAdapter<T>() {

                public void write(JsonWriter out, T value) throws IOException {
                    delegate.write(out, value);
                }

                public T read(JsonReader in) throws IOException {
                    T t = delegate.read(in);
                    L.d(GsonUtils.TAG, "[DeserializeAction] finish read, data = %s, type = %s, delegate = %s", t, type, delegate);
                    if (isInvalidData(t)) {
                        return null;
                    }
                    doAfterDeserialize(t);
                    return t;
                }
            };

        } else {
            L.d(GsonUtils.TAG, "[DeserializeAction] create return delegate, type = %s, delegate = %s", type, delegate);
            return delegate;
        }
    }

    public static boolean isInvalidData(Object t) {
        if (t instanceof IDataValidateAction) {
            if (!((IDataValidateAction) t).isDataValid()) {
                L.d(GsonUtils.TAG, "[DeserializeAction]     --> data is invalid");
                return true;
            }
        }
        return false;
    }

    public static <T> void doAfterDeserialize(Object t) {
        if (t instanceof IAfterDeserializeAction) {
            ((IAfterDeserializeAction) t).doAfterDeserialize();
            L.d(GsonUtils.TAG, "[DeserializeAction]     --> processed data = %s", t);
        }
    }

    private boolean shouldWrap(Class clazz) {
        return IAfterDeserializeAction.class.isAssignableFrom(clazz) ||
                IDataValidateAction.class.isAssignableFrom(clazz);
    }
}
複製程式碼

註冊:

Gson gson = new GsonBuilder().registerTypeAdapterFactory(new DeserializeActionFactory()).create();
複製程式碼

相關文章