Gson与枚举的那些事儿
背景
相信大家在日常的开发过程中,一定会遇到这样的一种情况:
在某个需求中,服务端同学约定了某一个int类型的字段表示了某个流程的”状态“或者请求的”类型“,此时偷懒的移动端同学就会直接使用int接收,然后代码中使用的时候,使用近乎于硬编码的方式判断int值,那么这次写没啥问题,过几天或者别人来读你代码的时候,发现if(id == 1){}这种情况,肯定会发狂的
目标
打造出一个可以顺利的将服务端某些有特殊意义的int值,映射到枚举中,这样在后续阅读代码或者别人阅读的时候,可以很方便的得到当前值代表什么含义了,说白了就是将int和枚举两种类型进行互转
现状
不同的json框架可能支持程度不一,android中为了搭配retrofit,json框架用的多的的可能是moshi或者gson了,因为笔者项目中用的是gson,便对gson进行了研究,gson本来就支持将某些int值字段映射到枚举中,依赖于SerializedName注解
enum class Test {
@SerializedName("1")
ONE,
@SerializedName("2")
TWO,
@SerializedName("3")
THREE,
@SerializedName("4")
FOUR
}
gson构造函数中会添加几十种TypeAdapterFactory,上述枚举转换则对应的是TypeAdapters.ENUM_FACTORY:
image.png
但是原本自带的方案有一个缺陷:它只能把枚举和字符串进行转换,更多的时候我们需要的结果是枚举与int转换,虽然很多框架对于数据有一定的兼容性,但最好还是int就是int,string就是string,保证数据类型的统一
大家先看下下面的json串将其转为实体类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自动将数字转到了枚举中
那么序列化的时候是什么样子的呢?我们继续做实验
println(Gson().toJson(Bean(Test.ONE, Test.TWO)))
// 输出: {"state":"1","type":"2"}
可见在反序列化的时候它是转成了string格式的值,但是此时服务端可能想要的是int,那么就出现数据不一致了
方案
下面介绍一个方案,来解决这种问题
思路:自定义一个TypeAdapterFactory,初始化gson的时候注入进去,其实现思路几乎与原本自带的factory是一样的,只不过专门处理了数据类型将其转为了int类型,我们自定义的功能需要满足两个基本功能
- 支持枚举与int转换
- 支持特定的枚举使用gson默认的行为
代码如下:
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字段,其次都是默认用int来处理了,如果不想用此adapter处理某个枚举,那么在枚举类上加一个注解IgnoreEnumIntConvert即可,如果还有特殊的要求,可以在此基础上进行二次扩展
现在我们来看看上面的例子经过处理后的输出会是什么样子:
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)
可以看到实现了int与枚举的转换了
最终,Android上使用的时候注意添加混淆规则:
-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 *{*;}