Android开发

Gson与枚举的那些事儿

2022-03-02  本文已影响0人  kunio

背景

相信大家在日常的开发过程中,一定会遇到这样的一种情况:
在某个需求中,服务端同学约定了某一个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类型,我们自定义的功能需要满足两个基本功能

  1. 支持枚举与int转换
  2. 支持特定的枚举使用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 *{*;}
上一篇下一篇

猜你喜欢

热点阅读