Skip to content

Commit

Permalink
修复 List 和 Map 条目解析容错导致崩溃的问题
Browse files Browse the repository at this point in the history
优化 List 和 Map 类型返回空和数据异常赋值处理
优化 Gson 解析容错回调类名称及回调方法设计
  • Loading branch information
getActivity committed Nov 4, 2023
1 parent 09b9ee8 commit 29db125
Show file tree
Hide file tree
Showing 24 changed files with 262 additions and 148 deletions.
51 changes: 29 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ allprojects {
```groovy
dependencyResolutionManagement {
repositories {
// JitPack 远程仓库:https://jitpack.io
// JitPack 远程仓库:https://jitpack.io[NameThatColor-1.7.4-fix.jar](..%2FStudioPlugins%2Fplugin%2FNameThatColor-1.7.4-fix.jar)
maven { url 'https://jitpack.io' }
}
}
Expand All @@ -39,7 +39,7 @@ android {
dependencies {
// Gson 解析容错:https://github.com/getActivity/GsonFactory
implementation 'com.github.getActivity:GsonFactory:8.0'
implementation 'com.github.getActivity:GsonFactory:9.0'
// Json 解析框架:https://github.com/google/gson
implementation 'com.google.code.gson:gson:2.10.1'
}
Expand Down Expand Up @@ -76,16 +76,8 @@ 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));
}
});
// 设置 Json 解析容错回调对象
GsonFactory.setParseExceptionCallback(ParseExceptionCallback callback);
```

#### 框架混淆规则
Expand Down Expand Up @@ -165,6 +157,8 @@ class XxxBean {

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

* 另外针对 List 和 Map 类型的对象,后台如果有返回 null 或者错误类型数据的时候,框架也会返回一个不为空但是集合大小为 0 的 List 对象或者 Map 对象,避免在 Kotlin 字段上面自定义字段不为空,但是后台返回空的情况导致出现的空指针异常。

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

#### 适配 Kotlin 默认值介绍
Expand Down Expand Up @@ -411,7 +405,7 @@ public final class DataClassBean {
}
```

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

## 常见疑问解答

Expand Down Expand Up @@ -450,7 +444,7 @@ new GsonBuilder()

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

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

![](picture/bugly_report_error.jpg)

Expand Down Expand Up @@ -478,20 +472,33 @@ new GsonBuilder()

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

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

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

@Override
public void onTypeException(TypeToken<?> typeToken, String fieldName, JsonToken jsonToken) {
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("类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken);
} else {
// 上报到 Bugly 错误列表
CrashReport.postCatchedException(new IllegalArgumentException("类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken));
throw new IllegalArgumentException(message);
} else {
CrashReport.postCatchedException(new IllegalArgumentException(message));
}
}
});
Expand Down
9 changes: 5 additions & 4 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ android {
applicationId "com.hjq.gson.factory.demo"
minSdkVersion 16
targetSdkVersion 31
versionCode 80
versionName "8.0"
versionCode 900
versionName "9.0"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}

Expand Down Expand Up @@ -43,9 +43,9 @@ android {
}
}

applicationVariants.all { variant ->
applicationVariants.configureEach { variant ->
// apk 输出文件名配置
variant.outputs.all { output ->
variant.outputs.configureEach { output ->
outputFileName = rootProject.getName() + '.apk'
}
}
Expand All @@ -64,6 +64,7 @@ dependencies {
// Json 解析框架:https://github.com/google/gson
// noinspection GradleDependency
androidTestImplementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.google.code.gson:gson:2.10.1'

// AndroidX 库:https://github.com/androidx/androidx
implementation 'androidx.appcompat:appcompat:1.4.0'
Expand Down
15 changes: 12 additions & 3 deletions app/src/androidTest/assets/AbnormalJson.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"bigDecimal2" : [],
"bigDecimal3" : {},
"booleanTest1" : 0,
"booleanTest2" : 1,
"booleanTest2" : "hello",
"booleanTest3" : null,
"booleanTest4" : "true",
"booleanTest5" : [],
Expand All @@ -27,17 +27,26 @@
"intTest5" : {},
"jsonArray" : {},
"jsonObject" : [],
"listTest1" : true,
"listTest1" : [],
"listTest2" : {},
"listTest3" : "",
"listTest4" : null,
"listTest4" : ["true", "false"],
"listTest5" : ["你好", "1.2345", "are you ok", "5.4321"],
"longTest1" : 1.1,
"longTest2" : null,
"longTest3" : "2.2",
"longTest4" : [],
"longTest5" : {},
"map1" : "",
"map2" : false,
"map3" : [],
"map4" : {
"a": 1234,
"b": "1234",
"c": 1234.21,
"d": "哈哈",
"e": false
},
"stringTest1" : null,
"stringTest2" : false,
"stringTest3" : 123,
Expand Down
5 changes: 4 additions & 1 deletion app/src/androidTest/assets/NormalJson.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"listTest2" : ["a", "b", "c"],
"listTest3" : [1, 2, 3],
"listTest4" : [true, false],
"listTest5" : ["1.0", "1.1", "1.2"],
"longTest1" : 1,
"longTest2" : -2,
"longTest3" : 9223372036854775807,
Expand All @@ -37,8 +38,10 @@
"1" : false
},
"map2" : {
"number" : 123456789
"a" : 123456789
},
"map3" : {},
"map4" : null,
"stringTest1" : null,
"stringTest2" : "",
"stringTest3" : "字符串"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
package com.hjq.gson.factory.test

data class DataClassBean(val name: String?, val age: Int = 18, val address: String?, val birthday: Long = System.currentTimeMillis())
data class DataClassBean(
val name: String?,
val age: Int = 18,
val address: String?,
val birthday: Long = System.currentTimeMillis()
)
10 changes: 6 additions & 4 deletions app/src/androidTest/java/com/hjq/gson/factory/test/JsonBean.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package com.hjq.gson.factory.test;

import org.json.JSONArray;
import org.json.JSONObject;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import org.json.JSONArray;
import org.json.JSONObject;

/**
* author : Android 轮子哥
Expand All @@ -16,9 +15,10 @@
public final class JsonBean {

private List<String> listTest1;
private List<String> listTest2;
private List<Double> listTest2;
private List<Integer> listTest3;
private List<Boolean> listTest4;
private List<Double> listTest5;

private boolean booleanTest1;
private boolean booleanTest2;
Expand Down Expand Up @@ -66,6 +66,8 @@ public final class JsonBean {

private Map<String, String> map1;
private Map<String, String> map2;
private Map<String, Integer> map3;
private Map<String, Integer> map4;

private JSONObject jsonObject;
private JSONArray jsonArray;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonToken;
import com.hjq.gson.factory.GsonFactory;
import com.hjq.gson.factory.JsonCallback;
import com.hjq.gson.factory.ParseExceptionCallback;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -24,6 +24,8 @@
*/
public final class JsonUnitTest {

private static final String TAG = "GsonFactory";

private Gson mGson;

/**
Expand All @@ -34,13 +36,32 @@ public void onTestBefore() {
// CrashReport.initCrashReport(InstrumentationRegistry.getInstrumentation().getContext());
mGson = GsonFactory.getSingletonGson();
// 设置 Json 解析容错监听
GsonFactory.setJsonCallback(new JsonCallback() {
GsonFactory.setParseExceptionCallback(new ParseExceptionCallback() {

@Override
public void onTypeException(TypeToken<?> typeToken, String fieldName, JsonToken jsonToken) {
Log.e("GsonFactory", "类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken);
// 上报到 Bugly 错误列表
// CrashReport.postCatchedException(new IllegalArgumentException("类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken));
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));
}
*/
}
});
}
Expand All @@ -53,7 +74,8 @@ public void parseNormalJsonTest() {
Context context = InstrumentationRegistry.getInstrumentation().getContext();
String json = getAssetsString(context, "NormalJson.json");
//mGson.toJson(mGson.fromJson(json, JsonBean.class));
mGson.fromJson(json, JsonBean.class);
JsonBean jsonBean = mGson.fromJson(json, JsonBean.class);
Log.i(TAG, mGson.toJson(jsonBean));
}

/**
Expand All @@ -64,7 +86,8 @@ public void parseAbnormalJsonTest() {
Context context = InstrumentationRegistry.getInstrumentation().getContext();
String json = getAssetsString(context, "AbnormalJson.json");
//mGson.toJson(mGson.fromJson(json, JsonBean.class));
mGson.fromJson(json, JsonBean.class);
JsonBean jsonBean = mGson.fromJson(json, JsonBean.class);
Log.i(TAG, mGson.toJson(jsonBean));
}

/**
Expand All @@ -74,7 +97,8 @@ public void parseAbnormalJsonTest() {
public void kotlinDataClassDefaultValueTest() {
Context context = InstrumentationRegistry.getInstrumentation().getContext();
String json = getAssetsString(context, "NullJson.json");
mGson.fromJson(json, DataClassBean.class);
DataClassBean dataClassBean = mGson.fromJson(json, DataClassBean.class);
Log.i(TAG, mGson.toJson(dataClassBean));
}

/**
Expand Down
21 changes: 18 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,25 @@ allprojects {
jcenter()
}

// 将构建文件统一输出到项目根目录下的 build 文件夹
setBuildDir(new File(rootDir, "build/${path.replaceAll(':', '/')}"))
// 读取 local.properties 文件配置
def properties = new Properties()
def localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.withInputStream { inputStream ->
properties.load(inputStream)
}
}

String buildDirPath = properties.getProperty("build.dir")
if (buildDirPath != null && buildDirPath != "") {
// 将构建文件统一输出到指定的目录下
setBuildDir(new File(buildDirPath, rootProject.name + "/build/${path.replaceAll(':', '/')}"))
} else {
// 将构建文件统一输出到项目根目录下的 build 文件夹
setBuildDir(new File(rootDir, "build/${path.replaceAll(':', '/')}"))
}
}

task clean(type: Delete) {
tasks.register('clean', Delete) {
delete rootProject.buildDir
}
Loading

0 comments on commit 29db125

Please sign in to comment.