查看原文
其他

更优雅的使用Gson解析Json

沈剑心 郭霖
2024-07-19


/   今日科技快讯   /

近日,即将推出的 Android 15 系统可能引入一项全新功能:应用隔离。这一功能将更好地保护用户免受行为异常应用的侵害。 Android 系统一直拥有强大的安全防护机制,能够抵御恶意应用的侵害。谷歌应用商店的安全防护服务“Play Protect”也会自动移除检测到的恶意应用。然而,任何安全软件都并非完美无瑕,难免会存在误判的可能。“Play Protect”通常会采取谨慎策略,在遇到可疑应用时会询问用户是否将其移除。为了帮助用户更好地应对可疑应用,Android 15 可能加入的“应用隔离”功能或许能提供更好的解决方案。

/   作者简介   /

本篇文章转自沈剑心的博客,文章主要分享了如何更好地使用Gson进行Json解析,相信会对大家有所帮助!

原文地址:
https://juejin.cn/post/7355800792073469992

/   前言   /

Gson背靠Google这棵大树,拥有广泛的社区支持和相对丰富的文档资源,同时因其简单直观的API,一直以来基本稳坐Android开发序列化的头把交椅(直到Google宣布kotlin成为Android开发的首选语言)。本文对Gson的使用及主要流程做下分析。

/   Gson的基本使用   /

Gson的依赖

dependencies {
  implementation 'com.google.code.gson:gson:2.10.1'
}

配置Gson


Gson类是整个Gson类库的核心类。开发者只需要通过调用new Gson()即可获取到gson实例,但我建议你通过GsonBuilder来创建并配置Gson类的实例:

private val gson = GsonBuilder()
        // 为特定类型注册自定义的序列化器或反序列化器(不支持协变)
        .registerTypeAdapter(Boolean::class.java, BooleanTypeAdapter())
        // 为特定类型注册自定义的序列化器或反序列化器(支持协变)
        .registerTypeHierarchyAdapter(xxxx)
        // 注册一个能够为多种类型提供适配器的工厂
        .registerTypeAdapterFactory(xxxx)
        // 设置长整型(Long)字段的序列化策略,例如将其序列化为字符串而不是数字
        .setLongSerializationPolicy(LongSerializationPolicy.STRING)
        // 自定义日期/时间字段的序列化格式
        .setDateFormat("yyyy-MM-dd HH:mm:ss:SSS")
        // 设置字段命名策略,以控制字段如何映射到JSON键名(默认不改变命名风格)
        .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
        // 设置自定义的字段命名策略,用于控制字段如何映射到JSON键名
        .setFieldNamingStrategy(xxxx)
        // 排除具有特定Java修饰符(默认 transient 和 static)的字段
        .excludeFieldsWithModifiers(java.lang.reflect.Modifier.TRANSIENT or java.lang.reflect.Modifier.STATIC)
        // 设置只序列化和反序列化带有@Expose注解的字段
        .excludeFieldsWithoutExposeAnnotation()
        // 设置类或字段过滤规则
        .setExclusionStrategies(xxxx)
        // 设置过滤规则(只适用于序列化)
        .addSerializationExclusionStrategy(xxxx)
        // 设置过滤规则(只适用于反序列化)
        .addDeserializationExclusionStrategy(xxxx)
        // 设置版本号,Gson将忽略所有高于此版本号的@Since注解和@Until注解的字段
        .setVersion(1.0)
        // 启用非基础类型 Map Key
        .enableComplexMapKeySerialization()
        // 默认情况下,Gson在序列化时会忽略值为null的字段。启用该设置后,Gson将包括值为null的字段
        .serializeNulls()
        // Gson将以更易读的格式输出JSON字符串,即格式化后的JSON,其中包含换行符和缩进。
        .setPrettyPrinting()
        .create()

Gson实例在调用JSON进行序列化/反序列化操作的过程中不维护任何状态,不同的Gson实例的配置和缓存等也不会复用,我们可以自由地使用同一个Gson实例对多个JSON进行序列化/反序列化操作。因此,我们应该在项目中提供一个全局的Gson实例,避免创建冗余的Gson实例。

这些设置提供了强大的定制能力,使得Gson能够适应各种不同的序列化和反序列化需求。通过链式调用这些方法,开发者可以轻松地构建出满足特定需求的Gson实例。

完成以上配置,你就可以在项目中愉快的使用Gson完成序列化/反序列化工作了:

// Serialization
Gson gson = new Gson();
gson.toJson(1);            // ==> 1
gson.toJson("abcd");       // ==> "abcd"
gson.toJson(new Long(10)); // ==> 10
int[] values = { 1 };
gson.toJson(values);       // ==> [1]

// Deserialization
int i = gson.fromJson("1", int.class);
Integer intObj = gson.fromJson("1", Integer.class);
Long longObj = gson.fromJson("1", Long.class);
Boolean boolObj = gson.fromJson("false", Boolean.class);
String str = gson.fromJson("\"abc\"", String.class);
String[] strArray = gson.fromJson("[\"abc\"]", String[].class);

Gson中的注解


Gson库提供了一些注解,通过这些注解可以更加灵活地控制Java对象到JSON字符串的序列化和反序列化过程。

  • @SerializedName:指定一个字段在JSON中的名称。常用于Java字段名和JSON键名不一致的情况。
  • @Expose:标记一个字段是否应该被序列化或反序列化。它用于在序列化/反序列化过程中包含或排除字段。
  • @Since:指定一个字段自某个版本号之后才被序列化或反序列化。这允许版本控制,可以用于向后兼容。
  • @Until:指定一个字段在某个版本号之前被序列化或反序列化。它与@Since注解相反,用于版本控制和向后兼容。
  • @JsonAdapter:指定一个字段使用自定义的序列化器和反序列化器。

data class User(
    // username字段在序列化/反序列化时会使用name作为键名
    @SerializedName("name")
    val username: String,
    // 标记password不参与序列化/反序列化
    @Expose
    val password: String,
    // phoneNumber字段只有在版本号为1.1或更高时才会参与序列化/反序列化
    @Since(1.1)
    val phoneNumber: String,
    // email字段只有在版本号低于1.2时才会参与序列化/反序列化
    @Until(1.2)
    val email: String,
    // 使用自定义的GenderAdapter解析器序列化/反序列化gender字段
    @JsonAdapter(GenderAdapter::class)
    val gender: Gender,
)

enum class Gender {
    MALE,
    FEMALE,
    UNKNOWN,
}

class GenderAdapter: TypeAdapter<Gender>() {
    override fun write(out: JsonWriter, value: Gender) {
        when (value) {
            Gender.UNKNOWN -> out.nullValue()
            Gender.MALE -> out.value("1")
            Gender.FEMALE -> out.value("2")
        }
    }
    override fun read(`in`: JsonReader): Gender {
        return when (`in`.peek()) {
            JsonToken.NULL -> {
                `in`.nextNull()
                Gender.UNKNOWN
            }
            JsonToken.NUMBER -> {
                when(`in`.nextInt()) {
                    1 -> Gender.MALE
                    2 -> Gender.FEMALE
                    else -> Gender.UNKNOWN
                }
            }
            else -> Gender.UNKNOWN
        }
    }
}

自定义解析


对于序列化/反序列化,Gson提供了三个关键接口:JsonSerializer、JsonDeserializer以及TypeAdapter,他们提供了不同级别的控制和灵活性。

JsonSerializer 和 JsonDeserializer


JsonSerializer和JsonDeserializer是Gson在1.x版本提供的用以自定义解析的两个接口,从名字也能明显看出来,JsonSerializer负责自定义序列化工作,而JsonDeserializer负责反序列化。

  • JsonSerializer 定义如何将类型T的对象转换成JSON。它只有一个方法serialize(T src, Type typeOfSrc, JsonSerializationContext context),返回一个JsonElement对象。
  • JsonDeserializer 定义如何将JSON转换回类型T的对象。它只有一个方法deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context),返回一个类型为T的对象。

可以看到,不管是 JsonSerializer 还是 JsonDeserializer,都依赖 JsonElement 类进行序列化/反序列化工作。JsonElement类是Gson中用以表示Json元素的抽象类,他有4个实现类:JsonObject、JsonArray、JsonPrimitive和JsonNull,分别对应表示Json中可能出现的所有类型:Json对象、数组、原始数据类型和空值。

  • JsonObject: 表示JSON对象,即一组键值对,其中键是字符串,值可以是任意类型的JsonElement。JsonObject提供了添加、删除和访问这些键值对的方法。
  • JsonArray: 表示JSON数组,即一个元素列表,这些元素本身可以是任意类型的JsonElement。JsonArray提供了添加、删除和访问这些元素的方法。
  • JsonPrimitive: 表示JSON的原始数据类型,如字符串、数字、布尔值等。JsonPrimitive封装了这些基本类型的值。
  • JsonNull: 表示JSON的空值。在Gson中,JsonNull是单例,用于表示值为null的情况。

通过操作JsonElement及其子类的实例,开发者可以灵活的构造、遍历和操作JSON数据结构。

TypeAdapter


TypeAdapter是Gson 从2.1版本后提供的、用来同时处理序列化和反序列化的接口。与JsonSerializer和JsonDeserializer不同的是,TypeAdapter提供了一个单一的实现点,通过两个方法write(JsonWriter out, T value)和read(JsonReader in)来分别处理序列化和反序列化。

class GenderAdapter: TypeAdapter<Gender>() {
    // 自定义序列化过程:
    // Gender.UNKNOWN -> null
    // Gender.MALE -> "1"
    // Gender.FEMALE -> "2"
    override fun write(out: JsonWriter, value: Gender) {
        when (value) {
            Gender.UNKNOWN -> out.nullValue()
            Gender.MALE -> out.value("1")
            Gender.FEMALE -> out.value("2")
        }
    }
    // 自定义反序列化过程:
    // null -> Gender.UNKNOWN
    // "1" -> Gender.MALE
    // "2" -> Gender.FEMALE
    override fun read(`in`: JsonReader): Gender {
        return when (`in`.peek()) {
            JsonToken.NULL -> {
                `in`.nextNull()
                Gender.UNKNOWN
            }
            JsonToken.NUMBER -> {
                when(`in`.nextInt()) {
                    1 -> Gender.MALE
                    2 -> Gender.FEMALE
                    else -> Gender.UNKNOWN
                }
            }
            else -> Gender.UNKNOWN
        }
    }
}

TypeAdapter提供了更高的灵活性和控制力,因为它直接操作JsonReader和JsonWriter,这使得它可以更高效地处理JSON,避免了中间JsonElement的创建和解析。这在处理大量数据或需要高性能序列化/反序列化时特别有用。

TypeAdapter 和 JsonSerializer/JsonDeserializer 的区别



从上图TypeAdapter和JsonSerializer/JsonDeserializer工作流图可以明显看出来,TypeAdapter直接操作JsonReader和JsonWriter,相对于JsonSerializer/JsonDeserializer避免了中间JsonElement的创建和解析。

  • 使用场景: JsonSerializer和JsonDeserializer通常用于更简单的场景,当你只需要定制某个类型的序列化或反序列化行为时。TypeAdapter用于更复杂或性能敏感的场景,提供了完全控制序列化和反序列化过程的能力。
  • 性能: TypeAdapter通常比JsonSerializer和JsonDeserializer更高效,因为它避免了中间JsonElement的创建和解析。
  • 灵活性: TypeAdapter提供了对序列化和反序列化过程的完全控制,而JsonSerializer和JsonDeserializer则在某种程度上受限于Gson的序列化和反序列化框架。

根据具体需求选择合适的接口是关键。对于大多数简单用途,使用JsonSerializer和JsonDeserializer可能就足够了。但对于需要细粒度控制或优化性能的场景,TypeAdapter将是更好的选择。

事实上,Gson 2.x 版本也会将JsonSerializer和JsonDeserializer转换成TreeTypeAdapter。

/   Gson是如何进行解析工作的   /

通过上面的基本使用,我们应该能清晰的感知到,TypeAdapter在Gson的解析工作中担任重要角色。在创建Gson实例时,Gson内置了许多TypeAdapter:

#com.google.gson.Gson#Gson()
List<TypeAdapterFactory> factories = new ArrayList<>();

// 内置类型的适配器,不可覆盖
factories.add(TypeAdapters.JSON_ELEMENT_FACTORY);
factories.add(ObjectTypeAdapter.getFactory(objectToNumberStrategy));

// 忽略的字段、类型适配器
factories.add(excluder);

// 用户自定义的适配器
factories.addAll(factoriesToBeAdded);

// 平台的基础类型适配器
factories.add(TypeAdapters.STRING_FACTORY);
factories.add(TypeAdapters.INTEGER_FACTORY);
factories.add(TypeAdapters.BOOLEAN_FACTORY);
factories.add(TypeAdapters.BYTE_FACTORY);
factories.add(TypeAdapters.SHORT_FACTORY);
TypeAdapter<Number> longAdapter = longAdapter(longSerializationPolicy);
factories.add(TypeAdapters.newFactory(long.class, Long.class, longAdapter));
factories.add(TypeAdapters.newFactory(double.class, Double.class,
                                          doubleAdapter(serializeSpecialFloatingPointValues)));
factories.add(TypeAdapters.newFactory(float.class, Float.class,
                                          floatAdapter(serializeSpecialFloatingPointValues)));
factories.add(NumberTypeAdapter.getFactory(numberToNumberStrategy));
factories.add(TypeAdapters.ATOMIC_INTEGER_FACTORY);
factories.add(TypeAdapters.ATOMIC_BOOLEAN_FACTORY);
factories.add(TypeAdapters.newFactory(AtomicLong.class, atomicLongAdapter(longAdapter)));
factories.add(TypeAdapters.newFactory(AtomicLongArray.class, atomicLongArrayAdapter(longAdapter)));
factories.add(TypeAdapters.ATOMIC_INTEGER_ARRAY_FACTORY);
factories.add(TypeAdapters.CHARACTER_FACTORY);
factories.add(TypeAdapters.STRING_BUILDER_FACTORY);
factories.add(TypeAdapters.STRING_BUFFER_FACTORY);
factories.add(TypeAdapters.newFactory(BigDecimal.class, TypeAdapters.BIG_DECIMAL));
factories.add(TypeAdapters.newFactory(BigInteger.class, TypeAdapters.BIG_INTEGER));
factories.add(TypeAdapters.newFactory(LazilyParsedNumber.class, TypeAdapters.LAZILY_PARSED_NUMBER));
factories.add(TypeAdapters.URL_FACTORY);
factories.add(TypeAdapters.URI_FACTORY);
factories.add(TypeAdapters.UUID_FACTORY);
factories.add(TypeAdapters.CURRENCY_FACTORY);
factories.add(TypeAdapters.LOCALE_FACTORY);
factories.add(TypeAdapters.INET_ADDRESS_FACTORY);
factories.add(TypeAdapters.BIT_SET_FACTORY);
factories.add(DateTypeAdapter.FACTORY);
factories.add(TypeAdapters.CALENDAR_FACTORY);

if (SqlTypesSupport.SUPPORTS_SQL_TYPES) {
    factories.add(SqlTypesSupport.TIME_FACTORY);
    factories.add(SqlTypesSupport.DATE_FACTORY);
    factories.add(SqlTypesSupport.TIMESTAMP_FACTORY);
}

factories.add(ArrayTypeAdapter.FACTORY);
factories.add(TypeAdapters.CLASS_FACTORY);

// map、集合类型适配器
factories.add(new CollectionTypeAdapterFactory(constructorConstructor));
factories.add(new MapTypeAdapterFactory(constructorConstructor, complexMapKeySerialization));
this.jsonAdapterFactory = new JsonAdapterAnnotationTypeAdapterFactory(constructorConstructor);
factories.add(jsonAdapterFactory);
factories.add(TypeAdapters.ENUM_FACTORY);
// JavaBean类型适配器
factories.add(new ReflectiveTypeAdapterFactory(
    constructorConstructor, fieldNamingStrategy, excluder, jsonAdapterFactory, reflectionFilters));

抛开用户自定义的适配器不谈,剩下的适配器我们将其大致分为三类:

  • 基础数据类型适配器
  • map、集合等容器类型适配器
  • 枚举类型适配器
  • JavaBean类型适配器

对应Gson解析工作的三种情况,Gson是如何解析基础数据类型的、Gson是如何解析map、集合容器类型的、Gson时如何解析枚举类型的、Gson是如何解析JavaBean类型的。我们一个一个来看。

找到合适的类型适配器


Gson进行解析操作的关键就是找到合适的类型适配器,代码在Gson.getAdapter中,源码如下:

#Gson.getAdapter()
public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {
    // 1.检查传入的TypeToken参数是否为null,若为null则抛出异常。
    Objects.requireNonNull(type, "type must not be null");
    // 2.从类型缓存typeTokenCache中尝试获取该类型的适配器,如果缓存中存在,则直接返回。
    TypeAdapter<?> cached = typeTokenCache.get(type);
    if (cached != null) {
        @SuppressWarnings("unchecked")
        TypeAdapter<T> adapter = (TypeAdapter<T>) cached;
        return adapter;
    }
    // 3.从线程本地缓存threadLocalAdapterResults中获取当前线程的适配器请求记录,如果不存在则创建一个新的HashMap,并将其设置到线程本地缓存中。
    Map<TypeToken<?>, TypeAdapter<?>> threadCalls = threadLocalAdapterResults.get();
    boolean isInitialAdapterRequest = false;
    if (threadCalls == null) {
        threadCalls = new HashMap<>();
        threadLocalAdapterResults.set(threadCalls);
        isInitialAdapterRequest = true;
    } else {
        // 在线程本地缓存中查找当前类型是否有适配器,如果存在则直接返回该适配器。
        @SuppressWarnings("unchecked")
        TypeAdapter<T> ongoingCall = (TypeAdapter<T>) threadCalls.get(type);
        if (ongoingCall != null) {
            return ongoingCall;
        }
    }

    TypeAdapter<T> candidate = null;
    try {
        // 4.创建一个FutureTypeAdapter对象,并将其设置为当前类型的适配器请求记录,存入threadLocalAdapterResults
        FutureTypeAdapter<T> call = new FutureTypeAdapter<>();
        threadCalls.put(type, call);

        for (TypeAdapterFactory factory : factories) {
            // 5.遍历TypeAdapterFactory工厂,尝试为当前类型创建适配器,若成功创建,则将该适配器设置给FutureTypeAdapter对象,并替换线程本地缓存中的适配器请求记录。
            candidate = factory.create(this, type);
            if (candidate != null) {
                call.setDelegate(candidate);
                threadCalls.put(type, candidate);
                break;
            }
        }
    } finally {
        if (isInitialAdapterRequest) {
            threadLocalAdapterResults.remove();
        }
    }

    if (candidate == null) {
        // 6.没有找到能够创建适配器的工厂,则抛出IllegalArgumentException异常。
        throw new IllegalArgumentException("GSON (" + GsonBuildConfig.VERSION + ") cannot handle " + type);
    }

    if (isInitialAdapterRequest) {
        // 7.当前线程首次请求适配器,则将线程本地缓存中的适配器发布到全局缓存typeTokenCache中。
        typeTokenCache.putAll(threadCalls);
    }
    return candidate;
}

本质上就是通过传入的TypeToken参数来确定需要获取的类型适配器TypeAdapter,只不过加入了缓存相关的逻辑,通过缓存来提高获取效率。

tips:当多个线程并发调用getAdapter()方法请求同一类型的适配器时,此方法可能会返回不同的TypeAdapter实例。如果对应的TypeAdapter的实现是无状态的,这就不会有什么问题,反之就会带来意想不到的问题。谨记‼️

找到合适的适配器后,Gson会通过适配器来进行具体的序列化/反序列化操作。

Gson解析基础数据类型


Gson内置了平台的基础类型适配器,我们以boolean类型为例,来看看Gson是怎么对boolean类型进行序列化/反序列化工作的。boolean类型对应的适配器是TypeAdapters.Boolean,相关代码如下:

public static final TypeAdapter<Boolean> BOOLEAN = new TypeAdapter<Boolean>() {
    @Override
    public Boolean read(JsonReader in) throws IOException {
        JsonToken peek = in.peek();
        if (peek == JsonToken.NULL) {
            in.nextNull();
            return null;
        } else if (peek == JsonToken.STRING) {
            // GSON 1.7版本后支持将String解析成boolean类型
            return Boolean.parseBoolean(in.nextString());
        }
        return in.nextBoolean();
    }
    @Override
    public void write(JsonWriter out, Boolean value) throws IOException {
        out.value(value);
    }
};

Gson解析map和集合


Gson通过CollectionTypeAdapterFactory工厂来获取集合类型的适配器并进行序列化/反序列化操作。

#CollectionTypeAdapterFactory.java
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)) {
        // 判断传入类型是否为集合类型,如果不是则返回null
        return null;
    }

    // 获取集合元素的类型
    Type elementType = $Gson$Types.getCollectionElementType(type, rawType);
    // 使用gson.getAdapter()方法获取该元素类型的适配器。
    TypeAdapter<?> elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType));
    ObjectConstructor<T> constructor = constructorConstructor.get(typeToken);
    // 创建并返回对应的TypeAdapter
    @SuppressWarnings({"unchecked", "rawtypes"})
    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<>(context, elementTypeAdapter, elementType);
        this.constructor = constructor;
    }

    @Override public Collection<E> read(JsonReader in) throws IOException {
        if (in.peek() == JsonToken.NULL) {
            in.nextNull();
            return null;
        }
        // 通过constructor.construct()创建集合实例
        Collection<E> collection = constructor.construct();
        // 消费json字符串中的'['
        in.beginArray();
        while (in.hasNext()) {
            // 调用元素TypeAdapter.read(in)生成元素实例
            E instance = elementTypeAdapter.read(in);
            // 加入集合
            collection.add(instance);
        }
        // 消费json字符串中的']'
        in.endArray();
        // 返回集合实例
        return collection;
    }

    @Override public void write(JsonWriter out, Collection<E> collection) throws IOException {
        if (collection == null) {
            out.nullValue();
            return;
        }
        // 生成json字符串中的'['
        out.beginArray();
        // 遍历集合,调用子元素的TypeAdapter.write序列化子元素
        for (E element : collection) {
            elementTypeAdapter.write(out, element);
        }
        // 生成json字符串中的']'
        out.endArray();
    }
}

解析Map类型的过程和集合类似,只不过Map类型需要维护 Key 和 Value 两个TypeAdapter来进行序列化和反序列化操作,此处不再赘述,感兴趣的童鞋可以自行查看源码。

Gson解析枚举类型


Gson在处理枚举类型时,默认使用内置的EnumTypeAdapter。EnumTypeAdapter的工作原理基于枚举值的名称,而不是其序数或任何其他属性。代码如下:

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

    public EnumTypeAdapter(final Class<T> classOfT) {
        try {
            // 使用反射查找枚举常量,以解决混淆类名称的不匹配问题
            // 通过访问控制器以特权上下文运行代码,并获取类的声明字段,筛选出枚举常量字段
            Field[] constantFields = AccessController.doPrivileged(new PrivilegedAction<Field[]>() {
                @Override public Field[] run() {
                    Field[] fields = classOfT.getDeclaredFields();
                    ArrayList<Field> constantFieldsList = new ArrayList<>(fields.length);
                    for (Field f : fields) {
                        if (f.isEnumConstant()) {
                            constantFieldsList.add(f);
                        }
                    }

                    Field[] constantFields = constantFieldsList.toArray(new Field[0]);
                    // 将字段设置为可访问
                    AccessibleObject.setAccessible(constantFields, true);
                    return constantFields;
                }
            });
            // 遍历枚举常量字段,并构三个关键的map结构表:
            // 1.名称 -> 枚举常量 表 nameToConstant
            // 1.字符串 -> 枚举常量 表 stringToConstant
            // 3.枚举常量 -> 名称 表 constantToName
            for (Field constantField : constantFields) {
                @SuppressWarnings("unchecked")
                T constant = (T)(constantField.get(null));
                String name = constant.name();
                String toStringVal = constant.toString();

                SerializedName annotation = constantField.getAnnotation(SerializedName.class);
                if (annotation != null) {
                    name = annotation.value();
                    for (String alternate : annotation.alternate()) {
                        // 字段上有@SerializedName注解,则使用注解的值作为名称
                        nameToConstant.put(alternate, constant);
                    }
                }

                nameToConstant.put(name, constant);
                stringToConstant.put(toStringVal, constant);
                constantToName.put(constant, name);
            }
        } catch (IllegalAccessException e) {
            throw new AssertionError(e);
        }
    }
    @Override public T read(JsonReader in) throws IOException {
        if (in.peek() == JsonToken.NULL) {
            in.nextNull();
            return null;
        }
        String key = in.nextString();
        // 根据名称获取枚举常量
        T constant = nameToConstant.get(key);
        return (constant == null) ? stringToConstant.get(key) : constant;
    }

    @Override public void write(JsonWriter out, T value) throws IOException {
        // 根据常量获取名称
        out.value(value == null ? null : constantToName.get(value));
    }
}

Gson解析Java Bean


Gson中,Java Bean类型的TypeAdapter都是由ReflectiveTypeAdapterFactory工厂创建的。我们来看创建TypeAdapter的方法:

public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
    Class<? super T> raw = type.getRawType();

    // Object类型,返回null
    if (!Object.class.isAssignableFrom(raw)) {
      return null; // it's a primitive!
    }
    // 根据反射过滤器的配置,判断是否允许使用反射来访问该类型字段
    FilterResult filterResult =
        ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
    if (filterResult == FilterResult.BLOCK_ALL) {
      // 不允许,抛出 JsonIOException 异常
      throw new JsonIOException(
          "ReflectionAccessFilter does not permit using reflection for " + raw
              + ". Register a TypeAdapter for this type or adjust the access filter.");
    }
    boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;

    // 如果为Java记录(Record)类型,使用RecordAdapter来处理解析工作
    if (ReflectionHelper.isRecord(raw)) {
      @SuppressWarnings("unchecked")
      TypeAdapter<T> adapter = (TypeAdapter<T>) new RecordAdapter<>(raw,
          getBoundFields(gson, type, raw, blockInaccessible, true), blockInaccessible);
      return adapter;
    }
    // 根据该类型的构造函数和字段信息,创建FieldReflectionAdapter实例,并返回该实例作为TypeAdapter的实现
    ObjectConstructor<T> constructor = constructorConstructor.get(type);
    return new FieldReflectionAdapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible, false));
  }

这段代码关键点有以下几点:

  • 根据传入的类型是否为Java记录类型返回不同的TypeAdapter。Java记录(Record)类型,使用RecordAdapter来处理解析工作;反之使用FieldReflectionAdapter处理。
  • 无论是RecordAdapter还是FieldReflectionAdapter,都会通过getBoundFields()方法生成一个用以表示字段名和绑定字段(serializeName - boundFiled)的映射关系的Map对象。

Java 记录类型是Java 14中引入的一个预览特性,并在Java 16中成为正式特性。记录提供了一种简洁的方式来声明只包含数据的不可变类。其特性类似于 kotlin 中的 data class。

然后在TypeAdapter中借助这个map对象进行解析工作:

public static abstract class Adapter<T, A> extends TypeAdapter<T> {
    final Map<String, BoundField> boundFields;

    Adapter(Map<String, BoundField> boundFields) {
        this.boundFields = boundFields;
    }

    @Override
    public void write(JsonWriter out, T value) throws IOException {
        // value为null,输出null
        if (value == null) {
            out.nullValue();
            return;
        }
        // 生成json字符串"{"
        out.beginObject();
        try {
            // 遍历boundFields中的所有字段,调用boundField.write()方法将字段的值序列化为JSON格式并输出
            for (BoundField boundField : boundFields.values()) {
                boundField.write(out, value);
            }
        } catch (IllegalAccessException e) {
            throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
        }
        // 生成json字符串"}"
        out.endObject();
    }

    @Override
    public T read(JsonReader in) throws IOException {
        // null 值校验
        if (in.peek() == JsonToken.NULL) {
            in.nextNull();
            return null;
        }
        // 创建一个accumulator对象来存储字段的值
        A accumulator = createAccumulator();

        try {
            // 消费json字符串"{"
            in.beginObject();
            // 遍历JSON的所有字段,若字段存在于boundFields中且标记为可反序列化
            // 则调用readField()方法将JSON字段的值反序列化为对应的字段值,并存储到accumulator中
            while (in.hasNext()) {
                String name = in.nextName();
                BoundField field = boundFields.get(name);
                if (field == null || !field.deserialized) {
                    in.skipValue();
                } else {
                    readField(accumulator, in, field);
                }
            }
        } catch (IllegalStateException e) {
            throw new JsonSyntaxException(e);
        } catch (IllegalAccessException e) {
            throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
        }
        // 消费json字符串"}"
        in.endObject();
        // 将accumulator转换为目标对象的实例并返回
        return finalize(accumulator);
    }

    /** 用于创建用于存储字段值的中间对象 */
    abstract A createAccumulator();
    /**
     * 用于将JSON字段的值反序列化为对应的字段值,并存储到中间对象中
     */
    abstract void readField(A accumulator, JsonReader in, BoundField field)
    throws IllegalAccessException, IOException;
    /** 用于将中间对象转换为目标对象的实例 */
    abstract T finalize(A accumulator);
}

Gson如何创建对象?


与Gson解析一致,Gson创建也因对象类型的不同,分为4种情况:

  • 基础类型、以及基础类型的包装类型等由Gson提供的TypeAdapter通过 new 关键字创建;
  • 枚举类型在EnumTypeAdapter中只是通过枚举名称切换不同的枚举常量,不涉及对象的创建;
  • 集合和map等容器类型通过Gson内置的对象创建工厂,调用 new 关键字进行创建;
  • Java Bean对象的创建比较复杂,分为3种情况,优先级由上到下依次降低:
    • 开发者定义了对象创建工厂InstanceCreator,则使用该工厂创建;
    • 存在默认的无参构造函数,通过反射构造函数创建;
    • 使用Unsafe API 创建。

#ConstructorConstructor.java
public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
    // 1.获取TypeToken对应的类型(Type)和原始类型(Class<? super T>)
    final Type type = typeToken.getType();
    Class<? super T> rawType = typeToken.getRawType();
    // 2.从instanceCreators中根据类型(Type)获取对应的实例创建器(InstanceCreator)
    //   如果存在,则创建并返回一个新的对象构造器,该构造器使用该实例创建器来创建对象
    final InstanceCreator<T> typeCreator = (InstanceCreator)this.instanceCreators.get(type);
    if (typeCreator != null) {
        return new ObjectConstructor<T>() {
            public T construct() {
                return typeCreator.createInstance(type);
            }
        };
    } else {
        // 3.如果步骤2中没有获取到实例创建器,则尝试根据原始类型(rawType)从instanceCreators中获取实例创建器
        // 如果存在,则创建并返回一个新的对象构造器,该构造器使用该实例创建器来创建对象。
        final InstanceCreator<T> rawTypeCreator = (InstanceCreator)this.instanceCreators.get(rawType);
        if (rawTypeCreator != null) {
            return new ObjectConstructor<T>() {
                public T construct() {
                    return rawTypeCreator.createInstance(type);
                }
            };
        } else {
            // 4.如果步骤2和步骤3都没有获取到实例创建器,则尝试调用newDefaultConstructor方法创建一个默认构造器,如果存在,则返回该构造器(无参构造函数)
            ObjectConstructor<T> defaultConstructor = this.newDefaultConstructor(rawType);
            if (defaultConstructor != null) {
                return defaultConstructor;
            } else {
                // 5.如果步骤4中没有创建到默认构造器,则尝试调用newDefaultImplementationConstructor方法创建一个默认实现构造器,如果存在,则返回该构造器(容器类型)
                ObjectConstructor<T> defaultImplementation = this.newDefaultImplementationConstructor(type, rawType);
                // 6.如果步骤5中没有创建到默认实现构造器,则尝试调用newUnsafeAllocator方法创建一个不安全的分配器构造器,最后返回该构造器(Unsafe)
                return defaultImplementation != null ? defaultImplementation : this.newUnsafeAllocator(type, rawType);
            }
        }
    }
}

/   使用注意事项   /

通过上文我们分析可知,当Java类未提供默认的无参构造函数时,Gson会使用 Unsafe API 来创建对象,这种创建对象的方式不会调用构造函数,因此会导致以下几个可能的问题:

  • 默认值丢失;
  • Kotlin 非空类型失效;
  • 初始化块可能不会正常执行;

/   更优雅使用Gson的一些经验   /

正确的配置Gson


正确配置Gson实例对于确保JSON的序列化和反序列化过程满足应用程序的需求至关重要。不恰当的配置可能导致数据丢失、格式错误或性能低下。下面几条是本人的经验之谈:

注意null值


默认情况下,Gson不会序列化值为null的字段。这可能会导致生成的JSON字符串缺少某些字段,特别是在与严格依赖JSON结构的外部系统交互时可能会出现问题。可以通过在创建Gson实例时使用GsonBuilder并调用serializeNulls()方法来改变这一行为。

不区分大小写的枚举反序列化


在反序列化枚举类型时,Gson默认不区分大小写,这可能会导致一些意外行为。如果JSON字符串中的枚举值和Java枚举常量在大小写上不一致,Gson仍然会将其成功反序列化,这可能不是所有场景下都期望的行为。

合理设置FieldNamingPolicy


使用FieldNamingPolicy来适应服务端不同的字段命名风格。

合理定义字段类型


很多时候,服务端往往会通过String类型或者数字类型返回表示枚举的含义,这个时候我们如果按服务端返回类型定义字段就会对代码的可读性造成影响。例如电商app的商品类我们定义如下:

data class Good(
    val id: Int,
    val name: String,
    val price: Double,
    val type: String,
) {
    companion object {
        public const val GOOD_TYPE_FRESH = "fresh"
        public const val GOOD_TYPE_BOOK = "book"
        public const val GOOD_TYPE_FOOD = "food"
    }
}

其中type表示商品类型,我们按服务端返回类型将其定义成String类型,type可能有三个值,在使用type字段时,我们往往不知道这个值包含哪几种情况,需要跳转Good类查看代码才能了解到,如果我们将type直接定义为枚举类型呢?

data class Good(
    val id: Int,
    val name: String,
    val price: Double,
    val type: GoodType,
)
enum class GoodType {
    @SerializedName("fresh")FRESH,
    @SerializedName("book")BOOK,
    @SerializedName("food")FOOD,
}

两种代码的可读性就有了明显的差距。所以,合理定义字段类型很重要。

巧用TypeAdapter


结合自身产品服务端返回字段的规范合理使用TypeAdapter


服务端常常使用1表示true,0或者其他数字表示false。对于boolean类型,Gson默认是没有这样的解析规则的,所以我们往往只能将字段定义成int类型,再对getter方法做处理:

data class Good(
    val id: Int,
    val name: String,
    val price: Double,
    val type: GoodType,
    // 是否为热销商品,1代表热销商品
    val isHot: Int,
) {
    fun isHot(): Boolean {
        return isHot == 1
    }
}

如果服务端对于字段的返回规范就是1表示true其余表示false,那客户端项目中少不了类似的处理。这个时候我们不妨自定义一个TypeAdapter:

class BooleanTypeAdapter: TypeAdapter<Boolean>() {
    override fun write(out: JsonWriter, value: Boolean) {
        out.value(value)
    }

    override fun read(`in`: JsonReader): Boolean? {
        return when (`in`.peek()) {
            JsonToken.NULL -> {
                `in`.nextNull()
                null
            }

            JsonToken.STRING -> {
                java.lang.Boolean.parseBoolean(`in`.nextString())
            }
            // 对数字类型做解析,1为true,其余为false
            JsonToken.NUMBER -> {
                `in`.nextInt() == 1
            }

            else -> `in`.nextBoolean()
        }
    }
}

然后在配置Gson时全局应用下这个TypeAdapter:

val gson = GsonBuilder()
    .registerTypeAdapter(Boolean::class.java, BooleanTypeAdapter())
    .create()

这样就可以使用boolean值定义isHot字段了:

data class Good(
    val id: Int,
    val name: String,
    val price: Double,
    val type: GoodType,
    val isHot: Boolean,
)

结合平台特性合理使用TypeAdapter


对于Android开发者,我们往往需要为View设置颜色相关的属性,这些颜色有时是通过服务端返回字段控制的,如换肤相关的背景色,默认情况下,我们只能这么处理:

data class Good(
    val id: Int,
    val name: String,
    val price: Double,
    val type: GoodType,
    val isHot: Boolean,
    val bgColor: String,
)

// 使用时,需要将颜色字符串通过Color.parseColor方法解析成Android平台的色值Int类型I
var bgColor: Int
try {
    bgColor = Color.parseColor(good.bgColor)
} catch (e: Exception) {
    bgColor = Color.WHITE
}
view.setBackgroundColor(bgColor)

这个时候自定义TypeAdapter又可以派上用场了:

class ColorTypeAdapter : TypeAdapter<Int>() {
    override fun write(out: JsonWriter, value: Int?) {
        if (value == null) {
            out.nullValue()
        } else {
            // 将Int颜色值转换为16进制字符串
            val colorStr = "#${Integer.toHexString(value).toUpperCase()}"
            out.value(colorStr)
        }
    }

    override fun read(`in`: JsonReader): Int? {
        if (`in`.peek() == JsonToken.NULL) {
            `in`.nextNull()
            return null
        }
        // 从JSON读取字符串并转换为Int颜色值
        val colorStr = `in`.nextString()
        return try {
            // 使用Android的Color类来解析16进制颜色字符串
            Color.parseColor(colorStr)
        } catch (e: IllegalArgumentException) {
            null
        }
    }
}

然后对bgColor字段应用ColorTypeAdapter:

data class Good(
    val id: Int,
    val name: String,
    val price: Double,
    val type: GoodType,
    val isHot: Boolean,
    @JsonAdapter(ColorTypeAdapter::class)
    val bgColor: String,
)
// 使用时,直接使用bgColor,无需手动解析
view.setBackgroundColor(bgColor)

所以,使用Gson时,结合平台特性,合理自定义TypeAdapte也能大大提升开发效率。

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
Jetpack Bluetooth——更优雅地使用蓝牙
使用KMP & Compose开发鸿蒙应用

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注
继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存