Gson與枚舉的那些事兒

背景

相信大家在日常的開發(fā)過程中,一定會遇到這樣的一種情況:
在某個需求中,服務(wù)端同學(xué)約定了某一個int類型的字段表示了某個流程的”狀態(tài)“或者請求的”類型“,此時偷懶的移動端同學(xué)就會直接使用int接收,然后代碼中使用的時候,使用近乎于硬編碼的方式判斷int值,那么這次寫沒啥問題,過幾天或者別人來讀你代碼的時候,發(fā)現(xiàn)if(id == 1){}這種情況,肯定會發(fā)狂的

目標(biāo)

打造出一個可以順利的將服務(wù)端某些有特殊意義的int值,映射到枚舉中,這樣在后續(xù)閱讀代碼或者別人閱讀的時候,可以很方便的得到當(dāng)前值代表什么含義了,說白了就是將int和枚舉兩種類型進(jìn)行互轉(zhuǎn)

現(xiàn)狀

不同的json框架可能支持程度不一,android中為了搭配retrofit,json框架用的多的的可能是moshi或者gson了,因?yàn)楣P者項(xiàng)目中用的是gson,便對gson進(jìn)行了研究,gson本來就支持將某些int值字段映射到枚舉中,依賴于SerializedName注解

enum class Test {
    @SerializedName("1")
    ONE,

    @SerializedName("2")
    TWO,

    @SerializedName("3")
    THREE,

    @SerializedName("4")
    FOUR
}

gson構(gòu)造函數(shù)中會添加幾十種TypeAdapterFactory,上述枚舉轉(zhuǎn)換則對應(yīng)的是TypeAdapters.ENUM_FACTORY:


image.png

但是原本自帶的方案有一個缺陷:它只能把枚舉和字符串進(jìn)行轉(zhuǎn)換,更多的時候我們需要的結(jié)果是枚舉與int轉(zhuǎn)換,雖然很多框架對于數(shù)據(jù)有一定的兼容性,但最好還是int就是int,string就是string,保證數(shù)據(jù)類型的統(tǒng)一

大家先看下下面的json串將其轉(zhuǎn)為實(shí)體類Bean時的輸出:

"{"state":1,"type":"2"}"
data class Bean(val state: Test,
                    val type: Test)
                  

此時輸出為:

println(Gson().fromJson("{"state":1,"type":2}", Bean::class.java))
// 輸出:Bean(state=ONE, type=TWO)

可以看到gson自動將數(shù)字轉(zhuǎn)到了枚舉中

那么序列化的時候是什么樣子的呢?我們繼續(xù)做實(shí)驗(yàn)

println(Gson().toJson(Bean(Test.ONE, Test.TWO)))

// 輸出: {"state":"1","type":"2"}

可見在反序列化的時候它是轉(zhuǎn)成了string格式的值,但是此時服務(wù)端可能想要的是int,那么就出現(xiàn)數(shù)據(jù)不一致了

方案

下面介紹一個方案,來解決這種問題
思路:自定義一個TypeAdapterFactory,初始化gson的時候注入進(jìn)去,其實(shí)現(xiàn)思路幾乎與原本自帶的factory是一樣的,只不過專門處理了數(shù)據(jù)類型將其轉(zhuǎn)為了int類型,我們自定義的功能需要滿足兩個基本功能

  1. 支持枚舉與int轉(zhuǎn)換
  2. 支持特定的枚舉使用gson默認(rèn)的行為

代碼如下:
TypeAdapterFactory

import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public final class EnumIntTypeAdapterFactory implements TypeAdapterFactory {

    public static EnumIntTypeAdapterFactory create() {
        return new EnumIntTypeAdapterFactory();
    }

    private EnumIntTypeAdapterFactory() {
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        Class<? super T> rawType = type.getRawType();
        if (rawType == null || rawType == Enum.class) {
            return null;
        }
        if (!Enum.class.isAssignableFrom(rawType)) {
            return null;
        }
        if (!rawType.isEnum()) {
            return null;
        }

        if (rawType.getAnnotation(IgnoreEnumIntConvert.class) != null) {
            return null;
        }
        return (TypeAdapter<T>) new EnumTypeAdapter(rawType);
    }
}

EnumTypeAdapter


import com.google.gson.TypeAdapter;
import com.google.gson.annotations.SerializedName;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

final class EnumTypeAdapter<T extends Enum<T>> extends TypeAdapter<T> {
    private final Map<Integer, T> nameToConstant = new HashMap<>();
    private final Map<T, Integer> constantToName = new HashMap<>();

    public EnumTypeAdapter(Class<T> classOfT) {
        T[] enumConstants = classOfT.getEnumConstants();
        if (enumConstants == null) {
            throw new NullPointerException(classOfT.getName() + ".getEnumConstants() == null");
        }
        for (T constant : enumConstants) {
            String name = constant.name();
            Field field;
            try {
                field = classOfT.getField(name);
            } catch (NoSuchFieldException e) {
                throw new AssertionError(e);
            }
            SerializedName annotation = field.getAnnotation(SerializedName.class);
            if (annotation == null) {
                throw new IllegalArgumentException("Enum class Field must Annotation with SerializedName:" + classOfT.getName() + "." + name);
            }
            String value = annotation.value();
            Integer intValue;
            try {
                intValue = Integer.valueOf(value);
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException("Enum class Field must Annotation with SerializedName And value is int type current is:"
                        + value + "\n\t\tin " + classOfT.getName() + "." + name, e);
            }
            T previous = nameToConstant.put(intValue, constant);
            if (previous != null) {
                throw new IllegalArgumentException("Enum class fields are repeatedly identified by the serializedName annotation:" +
                        "\n\t\tserializedName = " + intValue + " And two enum are" +
                        "\n\t\t1." + classOfT.getName() + "." + previous.name() +
                        "\n\t\t2." + classOfT.getName() + "." + constant.name());
            }
            constantToName.put(constant, intValue);
        }
    }

    @Override
    public T read(JsonReader in) throws IOException {
        if (in.peek() == JsonToken.NULL) {
            in.nextNull();
            return null;
        }
        return nameToConstant.get(in.nextInt());
    }

    @Override
    public void write(JsonWriter out, T value) throws IOException {
        out.value(value == null ? null : constantToName.get(value));
    }
}

IgnoreEnumIntConvert

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface IgnoreEnumIntConvert {
}

整體思路也是利用了SerializedName注解,但是處理方案比較簡單,首先沒有處理注解中的alternate字段,其次都是默認(rèn)用int來處理了,如果不想用此adapter處理某個枚舉,那么在枚舉類上加一個注解IgnoreEnumIntConvert即可,如果還有特殊的要求,可以在此基礎(chǔ)上進(jìn)行二次擴(kuò)展

現(xiàn)在我們來看看上面的例子經(jīng)過處理后的輸出會是什么樣子:

val gson = Gson().newBuilder().registerTypeAdapterFactory(EnumIntTypeAdapterFactory.create())
        .create()
println(gson.toJson(Bean(Test.ONE, Test.TWO)))
println(gson.fromJson("{"state":1,"type":2}", Bean::class.java))

輸出

 {"state":1,"type":2}
 Bean(state=ONE, type=TWO)

可以看到實(shí)現(xiàn)了int與枚舉的轉(zhuǎn)換了

最終,Android上使用的時候注意添加混淆規(guī)則:

-keepattributes Signature

# For using GSON @Expose annotation
-keepattributes *Annotation*
-ignorewarnings
# Gson specific classes
-dontwarn sun.misc.**
#-keep class com.google.gson.stream.** { *; }

# Application classes that will be serialized/deserialized over Gson
-keep class com.google.gson.examples.android.model.** { <fields>; }

# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * extends com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer

# Prevent R8 from leaving Data object members always null
-keepclassmembers,allowobfuscation class * {
  @com.google.gson.annotations.SerializedName <fields>;
}

# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken

##---------------End: proguard configuration for Gson  ----------

-keep class com.relxtech.android.gson.adapter.IgnoreEnumIntConvert
-keep @com.relxtech.android.gson.adapter.IgnoreEnumIntConvert class * {*;}

-keepclasseswithmembers enum *{*;}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容