背景
相信大家在日常的開發(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:

但是原本自帶的方案有一個缺陷:它只能把枚舉和字符串進(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類型,我們自定義的功能需要滿足兩個基本功能
- 支持枚舉與int轉(zhuǎn)換
- 支持特定的枚舉使用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 *{*;}