7.0 以下
,需要在 build.gradle
文件中加入allprojects {
repositories {
// JitPack 远程仓库:https://jitpack.io
maven { url 'https://jitpack.io' }
}
}
7.0 及以上
,则需要在 settings.gradle
文件中加入dependencyResolutionManagement {
repositories {
// JitPack 远程仓库:https://jitpack.io[NameThatColor-1.7.4-fix.jar](..%2FStudioPlugins%2Fplugin%2FNameThatColor-1.7.4-fix.jar)
maven { url 'https://jitpack.io' }
}
}
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:9.0'
// Json 解析框架:https://github.com/google/gson
implementation 'com.google.code.gson:gson:2.10.1'
}
// 获取单例的 Gson 对象(已处理容错)
Gson gson = GsonFactory.getSingletonGson();
// 设置自定义的 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.setParseExceptionCallback(ParseExceptionCallback callback);
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 | 集合、字符串、布尔值、数值 |
[] 、"" 、false 、0
|
集合 | bean、字符串、布尔值、数值 |
{} 、"" 、false 、0
|
字符串 | bean、集合、布尔值、数值 |
{} 、[] 、false 、0
|
布尔值 | bean、集合、字符串、数值 |
{} 、[] 、"" 、0
|
数值 | bean、集合、字符串、布尔值 |
{} 、[] 、"" 、false
|
大家可能觉得 Gson 解析容错没什么,那是因为我们对 Gson 解析失败的场景没有了解过:
惊吓不断:后台有数据时返回 JsonObject 类型,没数据返回 []
,Gson 会直接抛出异常
意想不到:如果客户端定义的是 boolean 类型,但是后台返回的是 0 或者 1,Gson 会直接抛出异常
措手不及:如果客户端定义的是 int 或者 long 类型,但是后台返回的是 float 或者 double 类型,Gson 会直接抛出异常
以上情况框架已经做了容错处理,具体处理的规则如下:
如果后台返回的类型和客户端定义的类型不匹配,框架就跳过解析这个字段,不影响其他字段正常解析
如果客户端定义的是 boolean 类型,但是后台返回整数,框架则将非 0 的数值则赋值为 true,否则为 false
如果客户端定义的是 int 或者 long 类型,但后台返回浮点数,框架就对数值进行直接取整并赋值给字段
class XxxBean {
val age: Int = 18
}
大家是不是以为在后台返回 { "age" : null }
,age
字段的值会等于 18
?我帮大家测试过了,不会等于 18
,会等于空。
那么这到底是为什么呢?聊到这个就不得不先说一下 Gson 解析的机制,我们都知道 Gson 在解析一个 Bean 类的时候,会反射创建一个对象出来,但是大家不知道的是,Gson 会根据 Bean 类的字段名去解析 Json 串中对应的值,然后简单粗暴进行反射赋值,你没有听错,简单粗暴,如果后台返回这个 age
字段的值为空,那么 age
就会被赋值为空,但是你又在 Kotlin 中声明了 age
变量不为空,外层一调用,触发 NullPointerException
也是在预料之中。
另外针对 List 和 Map 类型的对象,后台如果有返回 null 或者错误类型数据的时候,框架也会返回一个不为空但是集合大小为 0 的 List 对象或者 Map 对象,避免在 Kotlin 字段上面自定义字段不为空,但是后台返回空的情况导致出现的空指针异常。
框架目前的处理方案是,如果后台没有返回这个字段的值,又或者返回这个值为空,则不会赋值给类的字段,因为 Gson 那样做是不合理的,会导致我在 Kotlin 上面使用 Gson 是有问题,变量不定义成可空,每次用基本数据类型还得去做判空,定义成非空,一用还会触发 NullPointerException
,前后夹击,腹背受敌。
data class DataClassBean(val name: String?, val age: Int = 18)
name = null
age = 0
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;
}
}
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() {
}
}
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;
}
}
Integer.MAX_VALUE
,这样每次使用它去 & 不大于 0 的某个值,都会等于某个值,也就是不会等于 0,这样就能保证它的运算条件一直为 true,也就是使用默认值,其他参数传值的话,如果是基本数据类型,就传入基本数据类型的默认值,如果是对象类型,则直接传入 null。这样就解决了 Gson 反射 Kotlin Data Class 类出现字段默认值不生效的问题。Retrofit retrofit = new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(GsonFactory.getSingletonGson()))
.build();
// 替换调用
new Gson()
GsonFactory.getSingletonGson()
// 替换导包
import com.google.gson.Gson
import com.hjq.gson.factory.GsonFactory
// 再手动处理一些没有替换成功的
new GsonBuilder()
如果你们的后台用的是 PHP,那我十分推荐你使用这个框架,因为 PHP 返回的数据结构很乱,这块经历过的人都懂,说多了都是泪,没经历过的人怎么说都不懂。
如果你们的后台用的是 Java,那么可以根据实际情况而定,可用可不用,但是最好用,作为一种兜底方案,这样就能防止后台突然某一天不讲码德,例如我现在的公司的后台全是用 Java 开发的,但是 Bugly 还是有上报关于 Gson 解析的异常,下面是通过 GsonFactory.setParseExceptionCallback
采集到的数据,大家可以参考参考:
粗略估算了一下,总共上报了三千万多次错误,影响设备数三百多万,看到这里你还相信 Java 开发的后台不会有数据容错的问题么?事在人为,Java 只是一种开发语言,并不能担保不会有数据容错的问题,如果后续真的出现了这种问题,主要分为两种情况:
如果 iOS 没有做数据容错,那么锅是后台的,这点是毫无疑问的,后台想甩也甩不开
如果 iOS 做了数据容错,那么很可能的处理结果是,后台和 Android 两端的人要被拉出去各挨五十大板:
CTO:后台有问题,为什么 iOS 没事,就 Android 有事?
Android:iOS 那边做了容错,但是 Android 这边没有做。
CTO:你们为什么不做?这种兜底机制本来不应该就得有的吗?
Android:我们用了 Gson 框架,它的机制本身就是这样的。
CTO:我不管你们用了什么框架,在这点上崩溃了就是你们不对,后台返回错误的数据结构你们不解析就好了,你为什么还崩溃?同样的数据结构咋 iOS 就没有事呢?这个责任应该有你们 Android 一份子。
Android:。。。。。。(哑巴吃黄连,有苦说不出)
CTO 内心 OS:整个后台都是我在管的,出现这种事情,后面可能会吃不了兜着走,现在幸好拉到一个做垫背的来分担一下事故的责任。
Ps:以上故事纯属虚构,大家看看就好,切勿太过当真。不过有一点是真的,若不想日后扯皮,最好还是要留一手。
GsonFactory.setParseExceptionCallback
API,如果后台返回了错误的数据结构,在调试模式下,直接抛出异常即可,开发者可以第一时间得知;而到了线上模式,对这个问题进行上报即可,保证不漏掉任何一个问题(可上传到后台或者 Bugly 错误列表中),示例代码如下:// 设置 Json 解析容错监听
GsonFactory.setParseExceptionCallback(new ParseExceptionCallback() {
@Override
public void onParseObjectException(TypeToken<?> typeToken, String fieldName, JsonToken jsonToken) {
handlerGsonParseException("解析对象析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken);
}
@Override
public void onParseListException(TypeToken<?> typeToken, String fieldName, JsonToken listItemJsonToken) {
handlerGsonParseException("解析 List 异常:" + typeToken + "#" + fieldName + ",后台返回的条目类型为:" + listItemJsonToken);
}
@Override
public void onParseMapException(TypeToken<?> typeToken, String fieldName, String mapItemKey, JsonToken mapItemJsonToken) {
handlerGsonParseException("解析 Map 异常:" + typeToken + "#" + fieldName + ",mapItemKey = " + mapItemKey + ",后台返回的条目类型为:" + mapItemJsonToken);
}
private void handlerGsonParseException(String message) {
Log.e(TAG, message);
if (BuildConfig.DEBUG) {
throw new IllegalArgumentException(message);
} else {
CrashReport.postCatchedException(new IllegalArgumentException(message));
}
}
});
安卓技术中台:AndroidProject
安卓技术中台 Kt 版:AndroidProject-Kotlin
权限框架:XXPermissions
吐司框架:Toaster
网络框架:EasyHttp
标题栏框架:TitleBar
悬浮窗框架:EasyWindow
ShapeView 框架:ShapeView
ShapeDrawable 框架:ShapeDrawable
语种切换框架:MultiLanguages
日志查看框架:Logcat
嵌套滚动布局框架:NestedScrollLayout
Android 版本适配:AndroidVersionAdapter
Android 代码规范:AndroidCodeStandard
Android 资源大汇总:AndroidIndex
Android 开源排行榜:AndroidGithubBoss
Studio 精品插件:StudioPlugins
表情包大集合:EmojiPackage
AI 资源大汇总:AiIndex
省市区 Json 数据:ProvinceJson
Markdown 语法文档:MarkdownDoc
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中
【腾讯云】中小企业福利专场,多款刚需产品,满足企业通用场景需求
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.
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。