Skip to content

Commit 29db125

Browse files
committed
修复 List 和 Map 条目解析容错导致崩溃的问题
优化 List 和 Map 类型返回空和数据异常赋值处理 优化 Gson 解析容错回调类名称及回调方法设计
1 parent 09b9ee8 commit 29db125

24 files changed

+262
-148
lines changed

README.md

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ allprojects {
2020
```groovy
2121
dependencyResolutionManagement {
2222
repositories {
23-
// JitPack 远程仓库:https://jitpack.io
23+
// JitPack 远程仓库:https://jitpack.io[NameThatColor-1.7.4-fix.jar](..%2FStudioPlugins%2Fplugin%2FNameThatColor-1.7.4-fix.jar)
2424
maven { url 'https://jitpack.io' }
2525
}
2626
}
@@ -39,7 +39,7 @@ android {
3939
4040
dependencies {
4141
// Gson 解析容错:https://github.com/getActivity/GsonFactory
42-
implementation 'com.github.getActivity:GsonFactory:8.0'
42+
implementation 'com.github.getActivity:GsonFactory:9.0'
4343
// Json 解析框架:https://github.com/google/gson
4444
implementation 'com.google.code.gson:gson:2.10.1'
4545
}
@@ -76,16 +76,8 @@ GsonFactory.registerInstanceCreator(Type type, InstanceCreator<?> creator);
7676
// 添加反射访问过滤器
7777
GsonFactory.addReflectionAccessFilter(ReflectionAccessFilter filter);
7878

79-
// 设置 Json 解析容错监听
80-
GsonFactory.setJsonCallback(new JsonCallback() {
81-
82-
@Override
83-
public void onTypeException(TypeToken<?> typeToken, String fieldName, JsonToken jsonToken) {
84-
// Log.e("GsonFactory", "类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken);
85-
// 上报到 Bugly 错误列表中
86-
CrashReport.postCatchedException(new IllegalArgumentException("类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken));
87-
}
88-
});
79+
// 设置 Json 解析容错回调对象
80+
GsonFactory.setParseExceptionCallback(ParseExceptionCallback callback);
8981
```
9082

9183
#### 框架混淆规则
@@ -165,6 +157,8 @@ class XxxBean {
165157

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

160+
* 另外针对 List 和 Map 类型的对象,后台如果有返回 null 或者错误类型数据的时候,框架也会返回一个不为空但是集合大小为 0 的 List 对象或者 Map 对象,避免在 Kotlin 字段上面自定义字段不为空,但是后台返回空的情况导致出现的空指针异常。
161+
168162
* 框架目前的处理方案是,如果后台没有返回这个字段的值,又或者返回这个值为空,则不会赋值给类的字段,因为 Gson 那样做是不合理的,会导致我在 Kotlin 上面使用 Gson 是有问题,变量不定义成可空,每次用基本数据类型还得去做判空,定义成非空,一用还会触发 `NullPointerException`,前后夹击,腹背受敌。
169163

170164
#### 适配 Kotlin 默认值介绍
@@ -411,7 +405,7 @@ public final class DataClassBean {
411405
}
412406
```
413407

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

416410
## 常见疑问解答
417411

@@ -450,7 +444,7 @@ new GsonBuilder()
450444

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

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

455449
![](picture/bugly_report_error.jpg)
456450

@@ -478,20 +472,33 @@ new GsonBuilder()
478472

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

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

483477
```java
484478
// 设置 Json 解析容错监听
485-
GsonFactory.setJsonCallback(new JsonCallback() {
479+
GsonFactory.setParseExceptionCallback(new ParseExceptionCallback() {
486480

487481
@Override
488-
public void onTypeException(TypeToken<?> typeToken, String fieldName, JsonToken jsonToken) {
482+
public void onParseObjectException(TypeToken<?> typeToken, String fieldName, JsonToken jsonToken) {
483+
handlerGsonParseException("解析对象析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken);
484+
}
485+
486+
@Override
487+
public void onParseListException(TypeToken<?> typeToken, String fieldName, JsonToken listItemJsonToken) {
488+
handlerGsonParseException("解析 List 异常:" + typeToken + "#" + fieldName + ",后台返回的条目类型为:" + listItemJsonToken);
489+
}
490+
491+
@Override
492+
public void onParseMapException(TypeToken<?> typeToken, String fieldName, String mapItemKey, JsonToken mapItemJsonToken) {
493+
handlerGsonParseException("解析 Map 异常:" + typeToken + "#" + fieldName + ",mapItemKey = " + mapItemKey + ",后台返回的条目类型为:" + mapItemJsonToken);
494+
}
495+
496+
private void handlerGsonParseException(String message) {
497+
Log.e(TAG, message);
489498
if (BuildConfig.DEBUG) {
490-
// 直接抛出异常
491-
throw new IllegalArgumentException("类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken);
492-
} else {
493-
// 上报到 Bugly 错误列表
494-
CrashReport.postCatchedException(new IllegalArgumentException("类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken));
499+
throw new IllegalArgumentException(message);
500+
} else {
501+
CrashReport.postCatchedException(new IllegalArgumentException(message));
495502
}
496503
}
497504
});

app/build.gradle

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ android {
88
applicationId "com.hjq.gson.factory.demo"
99
minSdkVersion 16
1010
targetSdkVersion 31
11-
versionCode 80
12-
versionName "8.0"
11+
versionCode 900
12+
versionName "9.0"
1313
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
1414
}
1515

@@ -43,9 +43,9 @@ android {
4343
}
4444
}
4545

46-
applicationVariants.all { variant ->
46+
applicationVariants.configureEach { variant ->
4747
// apk 输出文件名配置
48-
variant.outputs.all { output ->
48+
variant.outputs.configureEach { output ->
4949
outputFileName = rootProject.getName() + '.apk'
5050
}
5151
}
@@ -64,6 +64,7 @@ dependencies {
6464
// Json 解析框架:https://github.com/google/gson
6565
// noinspection GradleDependency
6666
androidTestImplementation 'com.google.code.gson:gson:2.10.1'
67+
implementation 'com.google.code.gson:gson:2.10.1'
6768

6869
// AndroidX 库:https://github.com/androidx/androidx
6970
implementation 'androidx.appcompat:appcompat:1.4.0'

app/src/androidTest/assets/AbnormalJson.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"bigDecimal2" : [],
66
"bigDecimal3" : {},
77
"booleanTest1" : 0,
8-
"booleanTest2" : 1,
8+
"booleanTest2" : "hello",
99
"booleanTest3" : null,
1010
"booleanTest4" : "true",
1111
"booleanTest5" : [],
@@ -27,17 +27,26 @@
2727
"intTest5" : {},
2828
"jsonArray" : {},
2929
"jsonObject" : [],
30-
"listTest1" : true,
30+
"listTest1" : [],
3131
"listTest2" : {},
3232
"listTest3" : "",
33-
"listTest4" : null,
33+
"listTest4" : ["true", "false"],
34+
"listTest5" : ["你好", "1.2345", "are you ok", "5.4321"],
3435
"longTest1" : 1.1,
3536
"longTest2" : null,
3637
"longTest3" : "2.2",
3738
"longTest4" : [],
3839
"longTest5" : {},
3940
"map1" : "",
4041
"map2" : false,
42+
"map3" : [],
43+
"map4" : {
44+
"a": 1234,
45+
"b": "1234",
46+
"c": 1234.21,
47+
"d": "哈哈",
48+
"e": false
49+
},
4150
"stringTest1" : null,
4251
"stringTest2" : false,
4352
"stringTest3" : 123,

app/src/androidTest/assets/NormalJson.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"listTest2" : ["a", "b", "c"],
3030
"listTest3" : [1, 2, 3],
3131
"listTest4" : [true, false],
32+
"listTest5" : ["1.0", "1.1", "1.2"],
3233
"longTest1" : 1,
3334
"longTest2" : -2,
3435
"longTest3" : 9223372036854775807,
@@ -37,8 +38,10 @@
3738
"1" : false
3839
},
3940
"map2" : {
40-
"number" : 123456789
41+
"a" : 123456789
4142
},
43+
"map3" : {},
44+
"map4" : null,
4245
"stringTest1" : null,
4346
"stringTest2" : "",
4447
"stringTest3" : "字符串"
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
package com.hjq.gson.factory.test
22

3-
data class DataClassBean(val name: String?, val age: Int = 18, val address: String?, val birthday: Long = System.currentTimeMillis())
3+
data class DataClassBean(
4+
val name: String?,
5+
val age: Int = 18,
6+
val address: String?,
7+
val birthday: Long = System.currentTimeMillis()
8+
)

app/src/androidTest/java/com/hjq/gson/factory/test/JsonBean.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package com.hjq.gson.factory.test;
22

3-
import org.json.JSONArray;
4-
import org.json.JSONObject;
5-
63
import java.math.BigDecimal;
74
import java.util.List;
85
import java.util.Map;
6+
import org.json.JSONArray;
7+
import org.json.JSONObject;
98

109
/**
1110
* author : Android 轮子哥
@@ -16,9 +15,10 @@
1615
public final class JsonBean {
1716

1817
private List<String> listTest1;
19-
private List<String> listTest2;
18+
private List<Double> listTest2;
2019
private List<Integer> listTest3;
2120
private List<Boolean> listTest4;
21+
private List<Double> listTest5;
2222

2323
private boolean booleanTest1;
2424
private boolean booleanTest2;
@@ -66,6 +66,8 @@ public final class JsonBean {
6666

6767
private Map<String, String> map1;
6868
private Map<String, String> map2;
69+
private Map<String, Integer> map3;
70+
private Map<String, Integer> map4;
6971

7072
private JSONObject jsonObject;
7173
private JSONArray jsonArray;

app/src/androidTest/java/com/hjq/gson/factory/test/JsonUnitTest.java

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import com.google.gson.reflect.TypeToken;
88
import com.google.gson.stream.JsonToken;
99
import com.hjq.gson.factory.GsonFactory;
10-
import com.hjq.gson.factory.JsonCallback;
10+
import com.hjq.gson.factory.ParseExceptionCallback;
1111
import java.io.ByteArrayOutputStream;
1212
import java.io.IOException;
1313
import java.io.InputStream;
@@ -24,6 +24,8 @@
2424
*/
2525
public final class JsonUnitTest {
2626

27+
private static final String TAG = "GsonFactory";
28+
2729
private Gson mGson;
2830

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

3941
@Override
40-
public void onTypeException(TypeToken<?> typeToken, String fieldName, JsonToken jsonToken) {
41-
Log.e("GsonFactory", "类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken);
42-
// 上报到 Bugly 错误列表
43-
// CrashReport.postCatchedException(new IllegalArgumentException("类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken));
42+
public void onParseObjectException(TypeToken<?> typeToken, String fieldName, JsonToken jsonToken) {
43+
handlerGsonParseException("解析对象析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken);
44+
}
45+
46+
@Override
47+
public void onParseListException(TypeToken<?> typeToken, String fieldName, JsonToken listItemJsonToken) {
48+
handlerGsonParseException("解析 List 异常:" + typeToken + "#" + fieldName + ",后台返回的条目类型为:" + listItemJsonToken);
49+
}
50+
51+
@Override
52+
public void onParseMapException(TypeToken<?> typeToken, String fieldName, String mapItemKey, JsonToken mapItemJsonToken) {
53+
handlerGsonParseException("解析 Map 异常:" + typeToken + "#" + fieldName + ",mapItemKey = " + mapItemKey + ",后台返回的条目类型为:" + mapItemJsonToken);
54+
}
55+
56+
private void handlerGsonParseException(String message) {
57+
Log.e(TAG, message);
58+
/*
59+
if (BuildConfig.DEBUG) {
60+
throw new IllegalArgumentException(message);
61+
} else {
62+
CrashReport.postCatchedException(new IllegalArgumentException(message));
63+
}
64+
*/
4465
}
4566
});
4667
}
@@ -53,7 +74,8 @@ public void parseNormalJsonTest() {
5374
Context context = InstrumentationRegistry.getInstrumentation().getContext();
5475
String json = getAssetsString(context, "NormalJson.json");
5576
//mGson.toJson(mGson.fromJson(json, JsonBean.class));
56-
mGson.fromJson(json, JsonBean.class);
77+
JsonBean jsonBean = mGson.fromJson(json, JsonBean.class);
78+
Log.i(TAG, mGson.toJson(jsonBean));
5779
}
5880

5981
/**
@@ -64,7 +86,8 @@ public void parseAbnormalJsonTest() {
6486
Context context = InstrumentationRegistry.getInstrumentation().getContext();
6587
String json = getAssetsString(context, "AbnormalJson.json");
6688
//mGson.toJson(mGson.fromJson(json, JsonBean.class));
67-
mGson.fromJson(json, JsonBean.class);
89+
JsonBean jsonBean = mGson.fromJson(json, JsonBean.class);
90+
Log.i(TAG, mGson.toJson(jsonBean));
6891
}
6992

7093
/**
@@ -74,7 +97,8 @@ public void parseAbnormalJsonTest() {
7497
public void kotlinDataClassDefaultValueTest() {
7598
Context context = InstrumentationRegistry.getInstrumentation().getContext();
7699
String json = getAssetsString(context, "NullJson.json");
77-
mGson.fromJson(json, DataClassBean.class);
100+
DataClassBean dataClassBean = mGson.fromJson(json, DataClassBean.class);
101+
Log.i(TAG, mGson.toJson(dataClassBean));
78102
}
79103

80104
/**

build.gradle

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,25 @@ allprojects {
3232
jcenter()
3333
}
3434

35-
// 将构建文件统一输出到项目根目录下的 build 文件夹
36-
setBuildDir(new File(rootDir, "build/${path.replaceAll(':', '/')}"))
35+
// 读取 local.properties 文件配置
36+
def properties = new Properties()
37+
def localPropertiesFile = rootProject.file("local.properties")
38+
if (localPropertiesFile.exists()) {
39+
localPropertiesFile.withInputStream { inputStream ->
40+
properties.load(inputStream)
41+
}
42+
}
43+
44+
String buildDirPath = properties.getProperty("build.dir")
45+
if (buildDirPath != null && buildDirPath != "") {
46+
// 将构建文件统一输出到指定的目录下
47+
setBuildDir(new File(buildDirPath, rootProject.name + "/build/${path.replaceAll(':', '/')}"))
48+
} else {
49+
// 将构建文件统一输出到项目根目录下的 build 文件夹
50+
setBuildDir(new File(rootDir, "build/${path.replaceAll(':', '/')}"))
51+
}
3752
}
3853

39-
task clean(type: Delete) {
54+
tasks.register('clean', Delete) {
4055
delete rootProject.buildDir
4156
}

0 commit comments

Comments
 (0)