• Stars
    star
    633
  • Rank 71,037 (Top 2 %)
  • Language
    Java
  • License
    Apache License 2.0
  • Created almost 4 years ago
  • Updated 7 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Gson 解析容错框架,愿从此再无 Json 解析报错

Gson 解析容错框架

集成步骤

  • 如果你的项目 Gradle 配置是在 7.0 以下,需要在 build.gradle 文件中加入
allprojects {
    repositories {
        // JitPack 远程仓库:https://jitpack.io
        maven { url 'https://jitpack.io' }
    }
}
  • 如果你的 Gradle 配置是 7.0 及以上,则需要在 settings.gradle 文件中加入
dependencyResolutionManagement {
    repositories {
        // JitPack 远程仓库:https://jitpack.io
        maven { url 'https://jitpack.io' }
    }
}
  • 配置完远程仓库后,在项目 app 模块下的 build.gradle 文件中加入远程依赖
android {
    // 支持 JDK 1.8
    compileOptions {
        targetCompatibility JavaVersion.VERSION_1_8
        sourceCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    // Gson 解析容错:https://github.com/getActivity/GsonFactory
    implementation 'com.github.getActivity:GsonFactory:8.0'
    // Json 解析框架:https://github.com/google/gson
    implementation 'com.google.code.gson:gson:2.10.1'
}
  • 需要注意的是:Gson 框架必须使用 2.9.0 及以上版本,否则将会出现版本兼容问题

使用文档

  • 请使用框架返回的 Gson 对象来代替项目中的 Gson 对象
// 获取单例的 Gson 对象(已处理容错)
Gson gson = GsonFactory.getSingletonGson();
  • 因为框架中的 Gson 对象已经对解析规则进行了容错处理

其他 API

// 设置自定义的 Gson 对象
GsonFactory.setSingletonGson(Gson gson);

// 创建一个 Gson 构建器(已处理容错)
GsonBuilder gsonBuilder = GsonFactory.newGsonBuilder();

// 注册类型适配器
GsonFactory.registerTypeAdapterFactory(TypeAdapterFactory factory);

// 注册构造函数创建器
GsonFactory.registerInstanceCreator(Type type, InstanceCreator<?> creator);

// 添加反射访问过滤器
GsonFactory.addReflectionAccessFilter(ReflectionAccessFilter filter);

// 设置 Json 解析容错监听
GsonFactory.setJsonCallback(new JsonCallback() {

    @Override
    public void onTypeException(TypeToken<?> typeToken, String fieldName, JsonToken jsonToken) {
        // Log.e("GsonFactory", "类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken);
        // 上报到 Bugly 错误列表中
        CrashReport.postCatchedException(new IllegalArgumentException("类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken));
    }
});

框架混淆规则

  • 在混淆规则文件 proguard-rules.pro 中加入
-keep class com.hjq.gson.factory.** {*;}

数据类型容错介绍

  • 目前支持容错的数据类型有:

    • Bean 类

    • 数组集合

    • Map 集合

    • JSONArray

    • JSONObject

    • String(字符串)

    • boolean / Boolean(布尔值)

    • int / Integer(整数,属于数值类)

    • long / Long(长整数,属于数值类)

    • float / Float(单精度浮点数,属于数值类)

    • double / Double(双精度浮点数,属于数值类)

    • BigDecimal(精度更高的浮点数,属于数值类)

  • 基本涵盖 99.99% 的开发场景,可以运行 Demo 中的单元测试用例来查看效果:

数据类型 容错的范围 数据示例
bean 集合、字符串、布尔值、数值 []""false0
集合 bean、字符串、布尔值、数值 {}""false0
字符串 bean、集合、布尔值、数值 {}[]false0
布尔值 bean、集合、字符串、数值 {}[]""0
数值 bean、集合、字符串、布尔值 {}[]""false
  • 大家可能觉得 Gson 解析容错没什么,那是因为我们对 Gson 解析失败的场景没有了解过:

    • 惊吓不断:后台有数据时返回 JsonObject 类型,没数据返回 [],Gson 会直接抛出异常

    • 意想不到:如果客户端定义的是 boolean 类型,但是后台返回的是 0 或者 1,Gson 会直接抛出异常

    • 措手不及:如果客户端定义的是 int 或者 long 类型,但是后台返回的是 float 或者 double 类型,Gson 会直接抛出异常

  • 以上情况框架已经做了容错处理,具体处理的规则如下:

    • 如果后台返回的类型和客户端定义的类型不匹配,框架就跳过解析这个字段,不影响其他字段正常解析

    • 如果客户端定义的是 boolean 类型,但是后台返回整数,框架则将非 0 的数值则赋值为 true,否则为 false

    • 如果客户端定义的是 int 或者 long 类型,但后台返回浮点数,框架就对数值进行直接取整并赋值给字段

适配 Kotlin 空值介绍

  • 如果你在 Kotlin 中定义了以下内容的 Bean 类
class XxxBean {
    
    val age: Int = 18
}
  • 大家是不是以为在后台返回 { "age" : null }age 字段的值会等于 18 ?我帮大家测试过了,不会等于 18,会等于空。

  • 那么这到底是为什么呢?聊到这个就不得不先说一下 Gson 解析的机制,我们都知道 Gson 在解析一个 Bean 类的时候,会反射创建一个对象出来,但是大家不知道的是,Gson 会根据 Bean 类的字段名去解析 Json 串中对应的值,然后简单粗暴进行反射赋值,你没有听错,简单粗暴,如果后台返回这个 age 字段的值为空,那么 age 就会被赋值为空,但是你又在 Kotlin 中声明了 age 变量不为空,外层一调用,触发 NullPointerException 也是在预料之中。

  • 框架目前的处理方案是,如果后台没有返回这个字段的值,又或者返回这个值为空,则不会赋值给类的字段,因为 Gson 那样做是不合理的,会导致我在 Kotlin 上面使用 Gson 是有问题,变量不定义成可空,每次用基本数据类型还得去做判空,定义成非空,一用还会触发 NullPointerException,前后夹击,腹背受敌。

适配 Kotlin 默认值介绍

  • 如果你在 Kotlin 中定义了以下内容的 Bean 类
data class DataClassBean(val name: String?, val age: Int = 18)
  • 如果丢给 Gson 解析,最终会得到以下结果
name = null
age = 0
  • age 为什么不等于 18?为什么会等于 0 呢?要知道这个问题的原因,我们需要反编译看一下 DataClassBean 的源码
public final class DataClassBean {
    private final int age;
    private final String name;

    public static /* synthetic */ DataClassBean copy$default(DataClassBean bean, String str, int i, int i2, Object obj) {
        if ((i2 & 1) != 0) {
            str = bean.name;
        }
        if ((i2 & 2) != 0) {
            i = bean.age;
        }
        return bean.copy(str, i);
    }

    public final String component1() {
        return this.name;
    }

    public final int component2() {
        return this.age;
    }

    public final DataClassBean copy(String str, int i) {
        return new DataClassBean(str, i);
    }

    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj instanceof DataClassBean) {
            DataClassBean bean = (DataClassBean) obj;
            return Intrinsics.areEqual(this.name, bean.name) && this.age == bean.age;
        }
        return false;
    }

    public int hashCode() {
        String str = this.name;
        return ((str == null ? 0 : str.hashCode()) * 31) + this.age;
    }

    public String toString() {
        return "DataClassBean(name=" + ((Object) this.name) + ", age=" + this.age + ')';
    }

    public DataClassBean(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public /* synthetic */ DataClassBean(String str, int i, int i2, DefaultConstructorMarker defaultConstructorMarker) {
        this(str, (i2 & 2) != 0 ? 18 : i);
    }

    public final int getAge() {
        return this.age;
    }

    public final String getName() {
        return this.name;
    }
}
  • 不知道大家发现问题没有?DataClassBean 类里面并没有空参构造函数,那 Gson 到底是怎么创建对象的呢?让我们看一段源码
package com.google.gson.internal;

public final class ConstructorConstructor {

  public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
  
    ......

    ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType, filterResult);
    if (defaultConstructor != null) {
      return defaultConstructor;
    }
  
    ......
  
    if (filterResult == FilterResult.ALLOW) {
      // finally try unsafe
      return newUnsafeAllocator(rawType);
    } else {
      ........
    }
  }
  
  private <T> ObjectConstructor<T> newUnsafeAllocator(final Class<? super T> rawType) {

    ......

    ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType, filterResult);
    if (defaultConstructor != null) {
      return defaultConstructor;
    }
  
    ......

    if (useJdkUnsafe) {
      return new ObjectConstructor<T>() {
        @Override public T construct() {
          try {
            @SuppressWarnings("unchecked")
            T newInstance = (T) UnsafeAllocator.INSTANCE.newInstance(rawType);
            return newInstance;
          } catch (Exception e) {
            throw new RuntimeException(("Unable to create instance of " + rawType + "."
                + " Registering an InstanceCreator or a TypeAdapter for this type, or adding a no-args"
                + " constructor may fix this problem."), e);
          }
        }
      };
    } else {
      ......
    }
  }
  
  private static <T> ObjectConstructor<T> newDefaultConstructor(Class<? super T> rawType, FilterResult filterResult) {

    ......

    final Constructor<? super T> constructor;
    try {
      constructor = rawType.getDeclaredConstructor();
    } catch (NoSuchMethodException e) {
      return null;
    }

    ......

    return new ObjectConstructor<T>() {
      @Override public T construct() {
        try {
          @SuppressWarnings("unchecked") // T is the same raw type as is requested
          T newInstance = (T) constructor.newInstance();
          return newInstance;
        }
        // Note: InstantiationException should be impossible because check at start of method made sure
        //   that class is not abstract
        catch (InstantiationException e) {
          throw new RuntimeException("Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
              + " with no args", e);
        } catch (InvocationTargetException e) {
          // TODO: don't wrap if cause is unchecked?
          // TODO: JsonParseException ?
          throw new RuntimeException("Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
              + " with no args", e.getCause());
        } catch (IllegalAccessException e) {
          throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
        }
      }
    };
  }
}
package com.google.gson.internal;

public abstract class UnsafeAllocator {

  public abstract <T> T newInstance(Class<T> c) throws Exception;

  public static final UnsafeAllocator INSTANCE = create();

  private static UnsafeAllocator create() {
    // try JVM
    // public class Unsafe {
    //   public Object allocateInstance(Class<?> type);
    // }
    try {
      Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
      Field f = unsafeClass.getDeclaredField("theUnsafe");
      f.setAccessible(true);
      final Object unsafe = f.get(null);
      final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
      return new UnsafeAllocator() {
        @Override
        @SuppressWarnings("unchecked")
        public <T> T newInstance(Class<T> c) throws Exception {
          assertInstantiable(c);
          return (T) allocateInstance.invoke(unsafe, c);
        }
      };
    } catch (Exception ignored) {
      // OK: try the next way
    }

    ......
  }
}
  • 相信你看完就懂了,Gson 确实是反射创建无参构造函数来创建对象,但是如果没有空参构造函数的情况下,它也会通过另外的手段创建对象,借助 sun.misc.Unsafe 创建对象,这样会有一个问题,这样创建出来的对象它不会走任何构造函数,通过查看刚刚反编译出来的 DataClassBean 类,就知道为什么这样 Kotlin 默认值都不会生效了

  • 框架的做法很简单,既然没有无参构造函数,那我就通过其他构造函数来创建,就拿 Kotlin 生成的 DataClassBean(String str, int i, int i2, DefaultConstructorMarker defaultConstructorMarker) 来创建对象

这个构造函数特别有意思,最后第一个参数是 DefaultConstructorMarker 类,里面啥也没有

public final class DefaultConstructorMarker {
    private DefaultConstructorMarker() {
    }
}
  • 最后第二个参数是参数标记,标记是否使用 data class 定义的默认值
public final class DataClassBean {

    public /* synthetic */ DataClassBean(String str, int i, int i2, DefaultConstructorMarker defaultConstructorMarker) {
        this(str, (i2 & 2) != 0 ? 18 : i);
    }

    public DataClassBean(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
  • 框架的解决方案是:反射最后第一个参数类型为 DefaultConstructorMarker,然后传入空对象即可,最后第二个参数类型为 int 的构造函数,并且让最后第二个参数的位运算逻辑为 true,让它走到默认值赋值那里,这样可以选择传入 Integer.MAX_VALUE,这样每次使用它去 & 不大于 0 的某个值,都会等于某个值,也就是不会等于 0,这样就能保证它的运算条件一直为 true,也就是使用默认值,其他参数传值的话,如果是基本数据类型,就传入基本数据类型的默认值,如果是对象类型,则直接传入 null。这样就完成了对 Kotlin Data Class 类默认值不生效问题的处理。

常见疑问解答

Retrofit 怎么替换 Gson?

Retrofit retrofit = new Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create(GsonFactory.getSingletonGson()))
        .build();

如何替换项目中已有的原生 Gson ?

// 替换调用
new Gson()
GsonFactory.getSingletonGson()
// 替换导包
import com.google.gson.Gson
import com.hjq.gson.factory.GsonFactory
// 再手动处理一些没有替换成功的
new GsonBuilder()

有没有必要处理 Json 解析容错?

  • 我觉得非常有必要,因为后台返回的数据结构是什么样我们把控不了,但是有一点是肯定的,我们都不希望它崩,因为一个接口的失败导致整个 App 崩溃退出实属不值得,但是 Gson 很敏感,动不动就崩。

我们后台用的是 Java,有必要处理容错吗?

  • 如果你们的后台用的是 PHP,那我十分推荐你使用这个框架,因为 PHP 返回的数据结构很乱,这块经历过的人都懂,说多了都是泪,没经历过的人怎么说都不懂。

  • 如果你们的后台用的是 Java,那么可以根据实际情况而定,可用可不用,但是最好用,作为一种兜底方案,这样就能防止后台突然某一天不讲码德,例如我现在的公司的后台全是用 Java 开发的,但是 Bugly 还是有上报关于 Gson 解析的异常,下面是通过 GsonFactory.setJsonCallback 采集到的数据,大家可以参考参考:

  • 粗略估算了一下,总共上报了三千万多次错误,影响设备数三百多万,看到这里你还相信 Java 开发的后台不会有数据容错的问题么?事在人为,Java 只是一种开发语言,并不能担保不会有数据容错的问题,如果后续真的出现了这种问题,主要分为两种情况:

    • 如果 iOS 没有做数据容错,那么锅是后台的,这点是毫无疑问的,后台想甩也甩不开

    • 如果 iOS 做了数据容错,那么很可能的处理结果是,后台和 Android 两端的人要被拉出去各挨五十大板:

      • CTO:后台有问题,为什么 iOS 没事,就 Android 有事?

      • Android:iOS 那边做了容错,但是 Android 这边没有做。

      • CTO:你们为什么不做?这种兜底机制本来不应该就得有的吗?

      • Android:我们用了 Gson 框架,它的机制本身就是这样的。

      • CTO:我不管你们用了什么框架,在这点上崩溃了就是你们不对,后台返回错误的数据结构你们不解析就好了,你为什么还崩溃?同样的数据结构咋 iOS 就没有事呢?这个责任应该有你们 Android 一份子。

      • Android:。。。。。。(哑巴吃黄连,有苦说不出)

      • CTO 内心 OS:整个后台都是我在管的,出现这种事情,后面可能会吃不了兜着走,现在幸好拉到一个做垫背的来分担一下事故的责任。

      • Ps:以上故事纯属虚构,大家看看就好,切勿太过当真。不过有一点是真的,若不想日后扯皮,最好还是要留一手。

使用了这个框架后,我如何知道出现了 Json 错误,从而保证问题不被掩盖?

  • 对于这个问题,解决方案也很简单,使用 GsonFactory.setJsonCallback API,如果后台返回了错误的数据结构,在调试模式下,直接抛出异常即可,开发者可以第一时间得知;而到了线上模式,对这个问题进行上报即可,保证不漏掉任何一个问题(可上传到后台或者 Bugly 错误列表中),示例代码如下:
// 设置 Json 解析容错监听
GsonFactory.setJsonCallback(new JsonCallback() {

    @Override
    public void onTypeException(TypeToken<?> typeToken, String fieldName, JsonToken jsonToken) {
        if (BuildConfig.DEBUG) {
            // 直接抛出异常
            throw new IllegalArgumentException("类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken);
        } else {
            // 上报到 Bugly 错误列表
            CrashReport.postCatchedException(new IllegalArgumentException("类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken));
        }
    }
});

作者的其他开源项目

微信公众号:Android轮子哥

Android 技术 Q 群:10047167

如果您觉得我的开源库帮你节省了大量的开发时间,请扫描下方的二维码随意打赏,要是能打赏个 10.24 🐵就太👍了。您的支持将鼓励我继续创作:octocat:点击查看捐赠列表

广告区

  • 我现在任腾讯云服务器推广大使,大家如果有购买服务器的需求,可以通过下面的链接购买

【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中

【腾讯云】中小企业福利专场,多款刚需产品,满足企业通用场景需求

License

Copyright 2020 Huang JinQun

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

More Repositories

1

AndroidProject

Android 技术中台,但愿人长久,搬砖不再有
Java
6,224
star
2

XXPermissions

Android 权限请求框架,已适配 Android 14
Java
5,398
star
3

Toaster

Android 吐司框架,专治 Toast 各种疑难杂症
Java
3,084
star
4

EmojiPackage

表情包资源合集,张张都是经典
2,602
star
5

TitleBar

Android 标题栏框架,从此布局属性不用记
Java
1,689
star
6

AndroidVersionAdapter

安卓版本适配全套指南
1,680
star
7

EasyWindow

Android 悬浮窗框架,好用不解释
Java
1,316
star
8

AndroidIndex

Android 资源大汇总
1,307
star
9

EasyHttp

Android 网络请求框架,简单易用,so easy
Java
1,304
star
10

AndroidProject-Kotlin

Android 技术中台 Kotlin 版本,但愿人长久,搬砖不再有
Kotlin
1,206
star
11

AndroidGithubBoss

Github Android 个人技术开源影响力排行榜
1,174
star
12

MultiLanguages

Android 多语种适配框架,兼容高版本,适配第三方库语种
Java
896
star
13

Logcat

Android 日志打印框架,在手机上可以直接看到 Logcat 日志啦
Java
789
star
14

ShapeView

Shape 支持在布局中直接定义啦,支持设置阴影,文字渐变色,状态选择器
Java
746
star
15

StudioPlugins

Android Studio 精品插件合集,不在于多只在于精
664
star
16

AndroidCodeStandard

Android 代码规范文档
565
star
17

AiIndex

ChatGPT
347
star
18

ProvinceJson

省市区Json
223
star
19

NestedScrollLayout

支持嵌套滚动的布局
Java
133
star
20

Donate

开源心得及支持人员列表
81
star
21

MarkdownDoc

57
star
22

ShapeDrawable

Java
54
star
23

getActivity

22
star
24

Markdown

21
star
25

EasyPermissions

Java
17
star
26

MyTomcat

Java
16
star
27

IssueTemplateGuide

8
star