From 2792c229cea683a9fa260faa3d7e31af209b6426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BD=AE=E5=AD=90=E5=93=A5?= Date: Mon, 7 Aug 2023 20:44:55 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=82=E9=85=8D=20Kotlin=20Data=20Class=20?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 258 +++++++++++++++++- app/build.gradle | 17 +- ...NoSpecification.json => AbnormalJson.json} | 52 ++-- .../{Specification.json => NormalJson.json} | 56 ++-- app/src/androidTest/assets/NullJson.json | 3 + .../hjq/gson/factory/test/DataClassBean.kt | 3 + .../hjq/gson/factory/test/JsonUnitTest.java | 34 ++- .../hjq/gson/factory/demo/MainActivity.java | 2 - build.gradle | 3 + library/build.gradle | 4 +- library/src/main/AndroidManifest.xml | 1 + .../com/hjq/gson/factory/GsonFactory.java | 4 +- .../constructor/ConcurrentMapConstructor.java | 26 ++ .../ConcurrentSkipListMapConstructor.java | 25 ++ .../constructor/EnumMapConstructor.java | 39 +++ .../constructor/EnumSetConstructor.java | 39 +++ .../constructor/ExceptionConstructor.java | 24 ++ .../InstanceCreatorConstructor.java | 27 ++ ...otlinDataClassDefaultValueConstructor.java | 94 +++++++ .../constructor/LinkedHashMapConstructor.java | 25 ++ .../constructor/LinkedTreeMapConstructor.java | 25 ++ .../factory/constructor/ListConstructor.java | 26 ++ .../factory/constructor/MainConstructor.java | 246 +++++++++++++++++ .../factory/constructor/QueueConstructor.java | 26 ++ .../ReflectCreatorConstructor.java | 54 ++++ .../ReflectSafeCreatorConstructor.java | 41 +++ .../factory/constructor/SetConstructor.java | 26 ++ .../constructor/SortedMapConstructor.java | 26 ++ .../constructor/SortedSetConstructor.java | 26 ++ .../element/CollectionTypeAdapterFactory.java | 11 +- .../element/MapTypeAdapterFactory.java | 11 +- .../element/ReflectiveTypeAdapterFactory.java | 13 +- .../factory/element/ReflectiveTypeUtils.java | 15 +- 33 files changed, 1175 insertions(+), 107 deletions(-) rename app/src/androidTest/assets/{NoSpecification.json => AbnormalJson.json} (95%) rename app/src/androidTest/assets/{Specification.json => NormalJson.json} (60%) create mode 100644 app/src/androidTest/assets/NullJson.json create mode 100644 app/src/androidTest/java/com/hjq/gson/factory/test/DataClassBean.kt create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/ConcurrentMapConstructor.java create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/ConcurrentSkipListMapConstructor.java create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/EnumMapConstructor.java create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/EnumSetConstructor.java create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/ExceptionConstructor.java create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/InstanceCreatorConstructor.java create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/KotlinDataClassDefaultValueConstructor.java create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/LinkedHashMapConstructor.java create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/LinkedTreeMapConstructor.java create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/ListConstructor.java create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/MainConstructor.java create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/QueueConstructor.java create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/ReflectCreatorConstructor.java create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/ReflectSafeCreatorConstructor.java create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/SetConstructor.java create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/SortedMapConstructor.java create mode 100644 library/src/main/java/com/hjq/gson/factory/constructor/SortedSetConstructor.java diff --git a/README.md b/README.md index 83229a9..32c6150 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ android { dependencies { // Gson 解析容错:https://github.com/getActivity/GsonFactory - implementation 'com.github.getActivity:GsonFactory:6.6' + implementation 'com.github.getActivity:GsonFactory:8.0' // Json 解析框架:https://github.com/google/gson implementation 'com.google.code.gson:gson:2.10.1' } @@ -150,9 +150,7 @@ GsonFactory.setJsonCallback(new JsonCallback() { * 如果客户端定义的是 **int** 或者 **long** 类型,但后台返回浮点数,框架就对数值进行**直接取整**并赋值给字段 -#### 适配 Kotlin 默认值介绍 - -* 这个问题来源大家的反馈,issue 地址:[issues/24](https://github.com/getActivity/GsonFactory/issues/24) +#### 适配 Kotlin 空值介绍 * 如果你在 Kotlin 中定义了以下内容的 Bean 类 @@ -163,17 +161,261 @@ class XxxBean { } ``` -* 大家是不是以为在后台没有返回 `age` 字段的情况下,`age` 字段的值会等于 `18` ?我帮大家测试过了,不但不会等于 `18`,并且还会吃系统一记 `NullPointerException`。 +* 大家是不是以为在后台返回 `{ "age" : null }`,`age` 字段的值会等于 `18` ?我帮大家测试过了,不会等于 `18`,会等于空。 -* 那么这到底是为什么呢?聊到这个就不得不先说一下 Gson 解析的机制,我们都知道 Gson 在解析一个 Bean 类的时候,会反射创建一个对象出来,但是大家不知道的是,Gson 会根据 Bean 类的字段名去解析 Json 串中对应的值,然后简单粗暴进行反射赋值,你没有听错,简单粗暴,如果后台没有返回这个 `age` 字段的值,那么 `age` 就会被赋值为空,但是你又在 Kotlin 中声明了 `age` 变量不为空,这个时候塞一个 `null` 值进去,触发 `NullPointerException` 也是在预料之中。 +* 那么这到底是为什么呢?聊到这个就不得不先说一下 Gson 解析的机制,我们都知道 Gson 在解析一个 Bean 类的时候,会反射创建一个对象出来,但是大家不知道的是,Gson 会根据 Bean 类的字段名去解析 Json 串中对应的值,然后简单粗暴进行反射赋值,你没有听错,简单粗暴,如果后台返回这个 `age` 字段的值为空,那么 `age` 就会被赋值为空,但是你又在 Kotlin 中声明了 `age` 变量不为空,外层一调用,触发 `NullPointerException` 也是在预料之中。 * 框架目前的处理方案是,如果后台没有返回这个字段的值,又或者返回这个值为空,则不会赋值给类的字段,因为 Gson 那样做是不合理的,会导致我在 Kotlin 上面使用 Gson 是有问题,变量不定义成可空,每次用基本数据类型还得去做判空,定义成非空,一用还会触发 `NullPointerException`,前后夹击,腹背受敌。 -* 到这里可能会有人发出疑问了,为什么在 Java 上用没事,偏偏在 Kotlin 上用有问题,你能解释一下这个问题吗?这个问题也很简单,这是因为 Gson 在反射赋值的时候需要满足两个条件,第一个是值不为空,第二个是类型不是基本数据类型,这两个条件同时满足的情况才会进行赋值,而 Java 和 Kotlin 最大的不同是,Kotlin 没有基本数据类型,只有对象,拿短整数举例,Java 用基本数据类型表示则为 `int`,如果用对象类型表示则为 `Integer`,而 Kotlin 只能用对象类型 `Int` 表示,这下知道为什么了吧! +#### 适配 Kotlin 默认值介绍 + +* 如果你在 Kotlin 中定义了以下内容的 Bean 类 + +```kotlin +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 ObjectConstructor get(TypeToken typeToken) { + + ...... + + ObjectConstructor defaultConstructor = newDefaultConstructor(rawType, filterResult); + if (defaultConstructor != null) { + return defaultConstructor; + } + + ...... + + if (filterResult == FilterResult.ALLOW) { + // finally try unsafe + return newUnsafeAllocator(rawType); + } else { + ........ + } + } + + private ObjectConstructor newUnsafeAllocator(final Class rawType) { + + ...... + + ObjectConstructor defaultConstructor = newDefaultConstructor(rawType, filterResult); + if (defaultConstructor != null) { + return defaultConstructor; + } + + ...... + + if (useJdkUnsafe) { + return new ObjectConstructor() { + @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 ObjectConstructor newDefaultConstructor(Class rawType, FilterResult filterResult) { + + ...... + + final Constructor constructor; + try { + constructor = rawType.getDeclaredConstructor(); + } catch (NoSuchMethodException e) { + return null; + } + + ...... + + return new ObjectConstructor() { + @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); + } + } + }; + } +} +``` + +```java +package com.google.gson.internal; + +public abstract class UnsafeAllocator { + + public abstract T newInstance(Class 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 newInstance(Class 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 类,里面啥也没有 + +```java +public final class DefaultConstructorMarker { + private DefaultConstructorMarker() { + } +} +``` + +* 最后第二个参数是参数标记,标记是否使用 data class 定义的默认值 + +```java +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 怎么替换 Gson? ```java Retrofit retrofit = new Retrofit.Builder() diff --git a/app/build.gradle b/app/build.gradle index 2743b80..f7e389c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' android { compileSdkVersion 31 @@ -7,8 +8,8 @@ android { applicationId "com.hjq.gson.factory.demo" minSdkVersion 16 targetSdkVersion 31 - versionCode 66 - versionName "6.6" + versionCode 80 + versionName "8.0" testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } @@ -60,19 +61,19 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + // Json 解析框架:https://github.com/google/gson + // noinspection GradleDependency + androidTestImplementation 'com.google.code.gson:gson:2.10.1' + // AndroidX 库:https://github.com/androidx/androidx implementation 'androidx.appcompat:appcompat:1.4.0' // Material 库:https://github.com/material-components/material-components-android implementation 'com.google.android.material:material:1.4.0' - // Json 解析框架:https://github.com/google/gson - // noinspection GradleDependency - implementation 'com.google.code.gson:gson:2.10.1' - // 标题栏框架:https://github.com/getActivity/TitleBar - implementation 'com.github.getActivity:TitleBar:10.0' + implementation 'com.github.getActivity:TitleBar:10.5' // Bugly 异常捕捉:https://bugly.qq.com/docs/user-guide/instruction-manual-android/?v=20190418140644 - implementation 'com.tencent.bugly:crashreport:4.0.4' + implementation 'com.tencent.bugly:crashreport:4.1.9' implementation 'com.tencent.bugly:nativecrashreport:3.9.2' } \ No newline at end of file diff --git a/app/src/androidTest/assets/NoSpecification.json b/app/src/androidTest/assets/AbnormalJson.json similarity index 95% rename from app/src/androidTest/assets/NoSpecification.json rename to app/src/androidTest/assets/AbnormalJson.json index e4bf3cf..b3a2467 100644 --- a/app/src/androidTest/assets/NoSpecification.json +++ b/app/src/androidTest/assets/AbnormalJson.json @@ -1,46 +1,46 @@ { - "listTest1" : true, - "listTest2" : {}, - "listTest3" : "", - "listTest4" : null, + "bean1" : "", + "bean2" : [], + "bigDecimal1" : "22", + "bigDecimal2" : [], + "bigDecimal3" : {}, "booleanTest1" : 0, "booleanTest2" : 1, "booleanTest3" : null, "booleanTest4" : "true", "booleanTest5" : [], "booleanTest6" : {}, - "stringTest1" : null, - "stringTest2" : false, - "stringTest3" : 123, - "stringTest4" : [], - "stringTest5" : {}, + "doubleTest1" : 11, + "doubleTest2" : null, + "doubleTest3" : "22", + "doubleTest4" : [], + "doubleTest5" : {}, + "floatTest1" : 11, + "floatTest2" : null, + "floatTest3" : "22", + "floatTest4" : [], + "floatTest5" : {}, "intTest1" : 1.1, "intTest2" : null, "intTest3" : "1.1", "intTest4" : [], "intTest5" : {}, + "jsonArray" : {}, + "jsonObject" : [], + "listTest1" : true, + "listTest2" : {}, + "listTest3" : "", + "listTest4" : null, "longTest1" : 1.1, "longTest2" : null, "longTest3" : "2.2", "longTest4" : [], "longTest5" : {}, - "floatTest1" : 11, - "floatTest2" : null, - "floatTest3" : "22", - "floatTest4" : [], - "floatTest5" : {}, - "doubleTest1" : 11, - "doubleTest2" : null, - "doubleTest3" : "22", - "doubleTest4" : [], - "doubleTest5" : {}, - "bigDecimal1" : "22", - "bigDecimal2" : [], - "bigDecimal3" : {}, - "bean1" : "", - "bean2" : [], "map1" : "", "map2" : false, - "jsonObject" : [], - "jsonArray" : {} + "stringTest1" : null, + "stringTest2" : false, + "stringTest3" : 123, + "stringTest4" : [], + "stringTest5" : {} } \ No newline at end of file diff --git a/app/src/androidTest/assets/Specification.json b/app/src/androidTest/assets/NormalJson.json similarity index 60% rename from app/src/androidTest/assets/Specification.json rename to app/src/androidTest/assets/NormalJson.json index 1ea2ace..ec9dfa6 100644 --- a/app/src/androidTest/assets/Specification.json +++ b/app/src/androidTest/assets/NormalJson.json @@ -1,37 +1,45 @@ { - "listTest1" : [1, 2], - "listTest2" : ["a", "b", "c"], - "listTest3" : [1, 2, 3], - "listTest4" : [true, false], + "bean1" : {}, + "bean2" : { + "number" : 12345 + }, + "bigDecimal1" : 2.1111111111111112, + "bigDecimal2" : 90000000000000009, "booleanTest1" : false, "booleanTest2" : true, - "stringTest1" : null, - "stringTest2" : "", - "stringTest3" : "字符串", - "intTest1" : 1, - "intTest2" : -2, - "longTest1" : 90000000000000009, - "longTest2" : -90000000000000009, + "doubleTest1" : 1.1, + "doubleTest2" : -1.2, + "doubleTest3" : 1.7976931348623157E308, + "doubleTest4" : 4.9E-324, "floatTest1" : 1.1, "floatTest2" : -1.1, - "doubleTest1" : 2.1111111112, - "doubleTest2" : -2.1111111112, - "bigDecimal1" : 2.1111111111111112, - "bigDecimal2" : 90000000000000009, - "bean1" : {}, - "bean2" : { - "number" : 12345 + "floatTest3" : 3.4028235E38, + "floatTest4" : 1.4E-45, + "intTest1" : 1, + "intTest2" : -2, + "intTest3" : 2147483647, + "intTest4" : -2147483648, + "jsonArray" : [ + true, false, true + ], + "jsonObject" : { + "key" : "value" }, + "listTest1" : [1, 2], + "listTest2" : ["a", "b", "c"], + "listTest3" : [1, 2, 3], + "listTest4" : [true, false], + "longTest1" : 1, + "longTest2" : -2, + "longTest3" : 9223372036854775807, + "longTest4" : -9223372036854775808, "map1" : { "1" : false }, "map2" : { "number" : 123456789 }, - "jsonObject" : { - "key" : "value" - }, - "jsonArray" : [ - true, false, true - ] + "stringTest1" : null, + "stringTest2" : "", + "stringTest3" : "字符串" } \ No newline at end of file diff --git a/app/src/androidTest/assets/NullJson.json b/app/src/androidTest/assets/NullJson.json new file mode 100644 index 0000000..0e0dcd2 --- /dev/null +++ b/app/src/androidTest/assets/NullJson.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/hjq/gson/factory/test/DataClassBean.kt b/app/src/androidTest/java/com/hjq/gson/factory/test/DataClassBean.kt new file mode 100644 index 0000000..22af6ce --- /dev/null +++ b/app/src/androidTest/java/com/hjq/gson/factory/test/DataClassBean.kt @@ -0,0 +1,3 @@ +package com.hjq.gson.factory.test + +data class DataClassBean(val name: String?, val age: Int = 18, val address: String?, val birthday: Long = System.currentTimeMillis()) \ No newline at end of file diff --git a/app/src/androidTest/java/com/hjq/gson/factory/test/JsonUnitTest.java b/app/src/androidTest/java/com/hjq/gson/factory/test/JsonUnitTest.java index a3408dc..037d980 100644 --- a/app/src/androidTest/java/com/hjq/gson/factory/test/JsonUnitTest.java +++ b/app/src/androidTest/java/com/hjq/gson/factory/test/JsonUnitTest.java @@ -2,22 +2,18 @@ import android.content.Context; import android.util.Log; - import androidx.test.platform.app.InstrumentationRegistry; - import com.google.gson.Gson; 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 org.junit.After; -import org.junit.Before; -import org.junit.Test; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; /** * author : Android 轮子哥 @@ -49,22 +45,38 @@ public void onTypeException(TypeToken typeToken, String fieldName, JsonToken }); } + /** + * 后台返回正常的 Json 串测试 + */ @Test - public void onSpecification() { + public void parseNormalJsonTest() { Context context = InstrumentationRegistry.getInstrumentation().getContext(); - String json = getAssetsString(context, "Specification.json"); + String json = getAssetsString(context, "NormalJson.json"); //mGson.toJson(mGson.fromJson(json, JsonBean.class)); mGson.fromJson(json, JsonBean.class); } + /** + * 后台返回异常的 Json 串测试 + */ @Test - public void onNoSpecification() { + public void parseAbnormalJsonTest() { Context context = InstrumentationRegistry.getInstrumentation().getContext(); - String json = getAssetsString(context, "NoSpecification.json"); + String json = getAssetsString(context, "AbnormalJson.json"); //mGson.toJson(mGson.fromJson(json, JsonBean.class)); mGson.fromJson(json, JsonBean.class); } + /** + * Kotlin DataClass 默认值测试 + */ + @Test + public void kotlinDataClassDefaultValueTest() { + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + String json = getAssetsString(context, "NullJson.json"); + mGson.fromJson(json, DataClassBean.class); + } + /** * 测试完成 */ diff --git a/app/src/main/java/com/hjq/gson/factory/demo/MainActivity.java b/app/src/main/java/com/hjq/gson/factory/demo/MainActivity.java index a6f24b8..72ae45b 100644 --- a/app/src/main/java/com/hjq/gson/factory/demo/MainActivity.java +++ b/app/src/main/java/com/hjq/gson/factory/demo/MainActivity.java @@ -3,9 +3,7 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; - import androidx.appcompat.app.AppCompatActivity; - import com.hjq.bar.OnTitleBarListener; import com.hjq.bar.TitleBar; diff --git a/build.gradle b/build.gradle index 3ddc64c..40e8c35 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,9 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:4.1.2' + // Kotlin 插件:https://plugins.jetbrains.com/plugin/6954-kotlin + // noinspection GradleDependency + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31' } } diff --git a/library/build.gradle b/library/build.gradle index 814c1bd..08f4e6e 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -5,8 +5,8 @@ android { defaultConfig { minSdkVersion 11 - versionCode 66 - versionName "6.6" + versionCode 80 + versionName "8.0" } // 使用 JDK 1.8 diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml index 2d2a496..5b105f1 100644 --- a/library/src/main/AndroidManifest.xml +++ b/library/src/main/AndroidManifest.xml @@ -1 +1,2 @@ + \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/GsonFactory.java b/library/src/main/java/com/hjq/gson/factory/GsonFactory.java index 31bfb22..f47ce4f 100644 --- a/library/src/main/java/com/hjq/gson/factory/GsonFactory.java +++ b/library/src/main/java/com/hjq/gson/factory/GsonFactory.java @@ -6,9 +6,9 @@ import com.google.gson.InstanceCreator; import com.google.gson.ReflectionAccessFilter; import com.google.gson.TypeAdapterFactory; -import com.google.gson.internal.ConstructorConstructor; import com.google.gson.internal.Excluder; import com.google.gson.internal.bind.TypeAdapters; +import com.hjq.gson.factory.constructor.MainConstructor; import com.hjq.gson.factory.data.BigDecimalTypeAdapter; import com.hjq.gson.factory.data.BooleanTypeAdapter; import com.hjq.gson.factory.data.DoubleTypeAdapter; @@ -118,7 +118,7 @@ public void addReflectionAccessFilter(ReflectionAccessFilter filter) { */ public static GsonBuilder newGsonBuilder() { GsonBuilder gsonBuilder = new GsonBuilder(); - ConstructorConstructor constructor = new ConstructorConstructor(INSTANCE_CREATORS, true, REFLECTION_ACCESS_FILTERS); + MainConstructor constructor = new MainConstructor(INSTANCE_CREATORS, true, REFLECTION_ACCESS_FILTERS); gsonBuilder.registerTypeAdapterFactory(TypeAdapters.newFactory(String.class, new StringTypeAdapter())) .registerTypeAdapterFactory(TypeAdapters.newFactory(boolean.class, Boolean.class, new BooleanTypeAdapter())) .registerTypeAdapterFactory(TypeAdapters.newFactory(int.class, Integer.class, new IntegerTypeAdapter())) diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/ConcurrentMapConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/ConcurrentMapConstructor.java new file mode 100644 index 0000000..8d57ca6 --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/ConcurrentMapConstructor.java @@ -0,0 +1,26 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.internal.ObjectConstructor; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/08/01 + * desc : ConcurrentMap 创建器 + */ +public final class ConcurrentMapConstructor implements ObjectConstructor> { + + private static final ConcurrentMapConstructor INSTANCE = new ConcurrentMapConstructor(); + + @SuppressWarnings("unchecked") + public static > T getInstance() { + return (T) INSTANCE; + } + + @Override + public ConcurrentMap construct() { + return new ConcurrentHashMap<>(); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/ConcurrentSkipListMapConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/ConcurrentSkipListMapConstructor.java new file mode 100644 index 0000000..260be72 --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/ConcurrentSkipListMapConstructor.java @@ -0,0 +1,25 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.internal.ObjectConstructor; +import java.util.concurrent.ConcurrentSkipListMap; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/08/01 + * desc : ConcurrentSkipListMap 创建器 + */ +public final class ConcurrentSkipListMapConstructor implements ObjectConstructor> { + + private static final ConcurrentSkipListMapConstructor INSTANCE = new ConcurrentSkipListMapConstructor(); + + @SuppressWarnings("unchecked") + public static > T getInstance() { + return (T) INSTANCE; + } + + @Override + public ConcurrentSkipListMap construct() { + return new ConcurrentSkipListMap<>(); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/EnumMapConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/EnumMapConstructor.java new file mode 100644 index 0000000..dd1f8db --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/EnumMapConstructor.java @@ -0,0 +1,39 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.JsonIOException; +import com.google.gson.internal.ObjectConstructor; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.EnumMap; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/08/01 + * desc : EnumMap 创建器 + */ +public final class EnumMapConstructor implements ObjectConstructor { + + private final Type mType; + + public EnumMapConstructor(Type type) { + mType = type; + } + + @SuppressWarnings("unchecked") + @Override + public T construct() { + if (mType instanceof ParameterizedType) { + Type elementType = ((ParameterizedType) mType).getActualTypeArguments()[0]; + if (elementType instanceof Class) { + @SuppressWarnings({"unchecked", "rawtypes"}) + T map = (T) new EnumMap((Class) elementType); + return map; + } else { + throw new JsonIOException("Invalid EnumMap type: " + mType); + } + } else { + throw new JsonIOException("Invalid EnumMap type: " + mType.toString()); + } + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/EnumSetConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/EnumSetConstructor.java new file mode 100644 index 0000000..e0eb37f --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/EnumSetConstructor.java @@ -0,0 +1,39 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.JsonIOException; +import com.google.gson.internal.ObjectConstructor; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.EnumSet; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/08/01 + * desc : EnumSet 创建器 + */ +public final class EnumSetConstructor implements ObjectConstructor { + + private final Type mType; + + public EnumSetConstructor(Type type) { + mType = type; + } + + @SuppressWarnings("unchecked") + @Override + public T construct() { + if (mType instanceof ParameterizedType) { + Type elementType = ((ParameterizedType) mType).getActualTypeArguments()[0]; + if (elementType instanceof Class) { + @SuppressWarnings({"rawtypes"}) + T set = (T) EnumSet.noneOf((Class)elementType); + return set; + } else { + throw new JsonIOException("Invalid EnumSet type: " + mType); + } + } else { + throw new JsonIOException("Invalid EnumSet type: " + mType.toString()); + } + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/ExceptionConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/ExceptionConstructor.java new file mode 100644 index 0000000..135c025 --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/ExceptionConstructor.java @@ -0,0 +1,24 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.JsonIOException; +import com.google.gson.internal.ObjectConstructor; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/08/01 + * desc : 异常的构造器 + */ +public final class ExceptionConstructor implements ObjectConstructor { + + private final String mExceptionMessage; + + public ExceptionConstructor(String exceptionMessage) { + mExceptionMessage = exceptionMessage; + } + + @Override + public T construct() { + throw new JsonIOException(mExceptionMessage); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/InstanceCreatorConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/InstanceCreatorConstructor.java new file mode 100644 index 0000000..d72ba93 --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/InstanceCreatorConstructor.java @@ -0,0 +1,27 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.InstanceCreator; +import com.google.gson.internal.ObjectConstructor; +import java.lang.reflect.Type; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/08/01 + * desc : 自定义的创建器 + */ +public final class InstanceCreatorConstructor implements ObjectConstructor { + + private final InstanceCreator mInstanceCreator; + private final Type mType; + + public InstanceCreatorConstructor(InstanceCreator instanceCreator, Type type) { + mInstanceCreator = instanceCreator; + mType = type; + } + + @Override + public T construct() { + return mInstanceCreator.createInstance(mType); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/KotlinDataClassDefaultValueConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/KotlinDataClassDefaultValueConstructor.java new file mode 100644 index 0000000..7f9f9e2 --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/KotlinDataClassDefaultValueConstructor.java @@ -0,0 +1,94 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.internal.ObjectConstructor; +import com.google.gson.internal.Primitives; +import java.lang.reflect.Constructor; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/08/01 + * desc : Kotlin Data Class 创建器,用于处理反射创建 data class 类导致默认值不生效的问题 + */ +public final class KotlinDataClassDefaultValueConstructor implements ObjectConstructor { + + private static final String DEFAULT_CONSTRUCTOR_MARKER_CLASS_NAME = "kotlin.jvm.internal.DefaultConstructorMarker"; + + private final Class mRawType; + + public KotlinDataClassDefaultValueConstructor(Class rawType) { + mRawType = rawType; + } + + @SuppressWarnings("unchecked") + @Override + public T construct() { + T instance = null; + try { + Constructor[] constructors = mRawType.getConstructors(); + for (Constructor constructor : constructors) { + Class[] parameterTypes = constructor.getParameterTypes(); + + int parameterLength = parameterTypes.length; + + // 如果参数小于 3 个,证明不是我们要的目标构造函数,就继续下一个循环 + // 如果最后一个参数是 DefaultConstructorMarker 类型,并且最后第二个参数是 int 类型 + if (parameterLength < 3 || + (!DEFAULT_CONSTRUCTOR_MARKER_CLASS_NAME.equals(parameterTypes[parameterTypes.length - 1].getName()) && + !int.class.isAssignableFrom(parameterTypes[parameterTypes.length - 2]))) { + continue; + } + + Object[] parameterValue = new Object[parameterLength]; + // 这个参数是标记位,用于判断有没有设置默认值进去,会进行 & 位运算进行判断 + // 这里设置成 Integer.MAX_VALUE,Integer.MAX_VALUE & 大于 0 的值,都会等于那个值 + // 这样就能每次都能走到设置默认值那里 + // Github 地址:https://github.com/getActivity/GsonFactory/issues/27 + parameterValue[parameterLength - 2] = Integer.MAX_VALUE; + // 这个参数 DefaultConstructorMarker 类型,用于标记构造函数是 Kotlin 语法自动生成的 + parameterValue[parameterLength - 1] = null; + + for (int i = 0; i < parameterTypes.length - 2; i++) { + Class parameterType = parameterTypes[i]; + parameterValue[i] = getTypeDefaultValue(parameterType); + } + + instance = (T) constructor.newInstance(parameterValue); + break; + } + } catch (Exception e) { + e.printStackTrace(); + } + + return instance; + } + + private Object getTypeDefaultValue(Class clazz) { + if (Primitives.isWrapperType(clazz)) { + // 如果是包装类,使用默认值为 null + return null; + } + + if (clazz.isPrimitive()) { + // 如果是基本数据类型,使用对应的包装类的默认值 + if (clazz == byte.class) { + return (byte) 0; + } else if (clazz == short.class) { + return (short) 0; + } else if (clazz == int.class) { + return 0; + } else if (clazz == long.class) { + return 0L; + } else if (clazz == float.class) { + return 0.0f; + } else if (clazz == double.class) { + return 0.0; + } else if (clazz == char.class) { + return '\u0000'; + } else if (clazz == boolean.class) { + return false; + } + } + return null; + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/LinkedHashMapConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/LinkedHashMapConstructor.java new file mode 100644 index 0000000..9351423 --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/LinkedHashMapConstructor.java @@ -0,0 +1,25 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.internal.ObjectConstructor; +import java.util.LinkedHashMap; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/08/01 + * desc : LinkedHashMap 创建器 + */ +public final class LinkedHashMapConstructor implements ObjectConstructor> { + + private static final LinkedHashMapConstructor INSTANCE = new LinkedHashMapConstructor(); + + @SuppressWarnings("unchecked") + public static > T getInstance() { + return (T) INSTANCE; + } + + @Override + public LinkedHashMap construct() { + return new LinkedHashMap<>(); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/LinkedTreeMapConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/LinkedTreeMapConstructor.java new file mode 100644 index 0000000..0e1504f --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/LinkedTreeMapConstructor.java @@ -0,0 +1,25 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.internal.LinkedTreeMap; +import com.google.gson.internal.ObjectConstructor; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/08/01 + * desc : LinkedTreeMap 创建器 + */ +public final class LinkedTreeMapConstructor implements ObjectConstructor> { + + private static final LinkedTreeMapConstructor INSTANCE = new LinkedTreeMapConstructor(); + + @SuppressWarnings("unchecked") + public static > T getInstance() { + return (T) INSTANCE; + } + + @Override + public LinkedTreeMap construct() { + return new LinkedTreeMap<>(); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/ListConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/ListConstructor.java new file mode 100644 index 0000000..f8dbe0a --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/ListConstructor.java @@ -0,0 +1,26 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.internal.ObjectConstructor; +import java.util.ArrayList; +import java.util.List; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/08/01 + * desc : List 创建器 + */ +public final class ListConstructor implements ObjectConstructor> { + + private static final ListConstructor INSTANCE = new ListConstructor(); + + @SuppressWarnings("unchecked") + public static > T getInstance() { + return (T) INSTANCE; + } + + @Override + public List construct() { + return new ArrayList<>(); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/MainConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/MainConstructor.java new file mode 100644 index 0000000..0c08c8e --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/MainConstructor.java @@ -0,0 +1,246 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.InstanceCreator; +import com.google.gson.ReflectionAccessFilter; +import com.google.gson.ReflectionAccessFilter.FilterResult; +import com.google.gson.internal.ObjectConstructor; +import com.google.gson.internal.ReflectionAccessFilterHelper; +import com.google.gson.internal.reflect.ReflectionHelper; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentNavigableMap; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2022/03/30 + * desc : 构造函数构造器,参考:{@link com.google.gson.internal.ConstructorConstructor} + */ +public final class MainConstructor { + private final Map> mInstanceCreators; + private final boolean mUseJdkUnsafe; + private final List mReflectionFilters; + + public MainConstructor(Map> instanceCreators, boolean useJdkUnsafe, List reflectionFilters) { + mInstanceCreators = instanceCreators; + mUseJdkUnsafe = useJdkUnsafe; + mReflectionFilters = reflectionFilters; + } + + /** + * Check if the class can be instantiated by Unsafe allocator. If the instance has interface or abstract modifiers + * return an exception message. + * @param c instance of the class to be checked + * @return if instantiable {@code null}, else a non-{@code null} exception message + */ + static String checkInstantiable(Class c) { + int modifiers = c.getModifiers(); + if (Modifier.isInterface(modifiers)) { + return "Interfaces can't be instantiated! Register an InstanceCreator " + + "or a TypeAdapter for this type. Interface name: " + c.getName(); + } + if (Modifier.isAbstract(modifiers)) { + return "Abstract classes can't be instantiated! Register an InstanceCreator " + + "or a TypeAdapter for this type. Class name: " + c.getName(); + } + return null; + } + + public ObjectConstructor get(TypeToken typeToken) { + final Type type = typeToken.getType(); + final Class rawType = typeToken.getRawType(); + + // first try an instance creator + + @SuppressWarnings("unchecked") + final InstanceCreator typeCreator = (InstanceCreator) mInstanceCreators.get(type); + if (typeCreator != null) { + return new InstanceCreatorConstructor<>(typeCreator, type); + } + + // Next try raw type match for instance creators + @SuppressWarnings("unchecked") // types must agree + final InstanceCreator rawTypeCreator = + (InstanceCreator) mInstanceCreators.get(rawType); + if (rawTypeCreator != null) { + return new InstanceCreatorConstructor<>(rawTypeCreator, type); + } + + // First consider special constructors before checking for no-args constructors + // below to avoid matching internal no-args constructors which might be added in + // future JDK versions + ObjectConstructor specialConstructor = newSpecialCollectionConstructor(type, rawType); + if (specialConstructor != null) { + return specialConstructor; + } + + FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(mReflectionFilters, rawType); + ObjectConstructor defaultConstructor = newDefaultConstructor(rawType, filterResult); + if (defaultConstructor != null) { + return defaultConstructor; + } + + ObjectConstructor defaultImplementation = newDefaultImplementationConstructor(type, rawType); + if (defaultImplementation != null) { + return defaultImplementation; + } + + // Check whether type is instantiable; otherwise ReflectionAccessFilter recommendation + // of adjusting filter suggested below is irrelevant since it would not solve the problem + final String exceptionMessage = checkInstantiable(rawType); + if (exceptionMessage != null) { + return new ExceptionConstructor<>(exceptionMessage); + } + + // Consider usage of Unsafe as reflection, so don't use if BLOCK_ALL + // Additionally, since it is not calling any constructor at all, don't use if BLOCK_INACCESSIBLE + if (filterResult == FilterResult.ALLOW) { + // finally try unsafe + return newUnsafeAllocator(rawType); + } else { + final String message = "Unable to create instance of " + rawType + "; ReflectionAccessFilter " + + "does not permit using reflection or Unsafe. Register an InstanceCreator or a TypeAdapter " + + "for this type or adjust the access filter to allow using reflection."; + return new ExceptionConstructor<>(message); + } + } + + /** + * 为没有公共无参数构造函数的特殊 JDK 集合类型创建构造函数 + */ + private static ObjectConstructor newSpecialCollectionConstructor(final Type type, Class rawType) { + if (EnumSet.class.isAssignableFrom(rawType)) { + return new EnumSetConstructor<>(type); + } + // Only support creation of EnumMap, but not of custom subtypes; for them type parameters + // and constructor parameter might have completely different meaning + else if (rawType == EnumMap.class) { + return new EnumMapConstructor<>(type); + } + + return null; + } + + private static ObjectConstructor newDefaultConstructor(Class rawType, FilterResult filterResult) { + // Cannot invoke constructor of abstract class + if (Modifier.isAbstract(rawType.getModifiers())) { + return null; + } + + final Constructor constructor; + try { + constructor = rawType.getDeclaredConstructor(); + } catch (NoSuchMethodException e) { + return null; + } + + boolean canAccess = filterResult == FilterResult.ALLOW || (ReflectionAccessFilterHelper.canAccess(constructor, null) + // Be a bit more lenient here for BLOCK_ALL; if constructor is accessible and public then allow calling it + && (filterResult != FilterResult.BLOCK_ALL || Modifier.isPublic(constructor.getModifiers()))); + + if (!canAccess) { + final String message = "Unable to invoke no-args constructor of " + rawType + "; " + + "constructor is not accessible and ReflectionAccessFilter does not permit making " + + "it accessible. Register an InstanceCreator or a TypeAdapter for this type, change " + + "the visibility of the constructor or adjust the access filter."; + return new ExceptionConstructor<>(message); + } + + // Only try to make accessible if allowed; in all other cases checks above should + // have verified that constructor is accessible + if (filterResult == FilterResult.ALLOW) { + final String exceptionMessage = ReflectionHelper.tryMakeAccessible(constructor); + if (exceptionMessage != null) { + /* + * Create ObjectConstructor which throws exception. + * This keeps backward compatibility (compared to returning `null` which + * would then choose another way of creating object). + * And it supports types which are only serialized but not deserialized + * (compared to directly throwing exception here), e.g. when runtime type + * of object is inaccessible, but compile-time type is accessible. + */ + + // New exception is created every time to avoid keeping reference + // to exception with potentially long stack trace, causing a + // memory leak + return new ExceptionConstructor<>(exceptionMessage); + } + } + + return new ReflectCreatorConstructor<>(rawType, constructor); + } + + /** + * Constructors for common interface types like Map and List and their + * subtypes. + */ + private static ObjectConstructor newDefaultImplementationConstructor( + final Type type, Class rawType) { + + /* + * IMPORTANT: Must only create instances for classes with public no-args constructor. + * For classes with special constructors / factory methods (e.g. EnumSet) + * `newSpecialCollectionConstructor` defined above must be used, to avoid no-args + * constructor check (which is called before this method) detecting internal no-args + * constructors which might be added in a future JDK version + */ + + if (Collection.class.isAssignableFrom(rawType)) { + if (SortedSet.class.isAssignableFrom(rawType)) { + return SortedSetConstructor.getInstance(); + } else if (Set.class.isAssignableFrom(rawType)) { + return SetConstructor.getInstance(); + } else if (Queue.class.isAssignableFrom(rawType)) { + return QueueConstructor.getInstance(); + } else { + return ListConstructor.getInstance(); + } + } + + if (Map.class.isAssignableFrom(rawType)) { + if (ConcurrentNavigableMap.class.isAssignableFrom(rawType)) { + return ConcurrentSkipListMapConstructor.getInstance(); + } else if (ConcurrentMap.class.isAssignableFrom(rawType)) { + return ConcurrentMapConstructor.getInstance(); + } else if (SortedMap.class.isAssignableFrom(rawType)) { + return SortedMapConstructor.getInstance(); + } else if (type instanceof ParameterizedType && !(String.class.isAssignableFrom( + TypeToken.get(((ParameterizedType) type).getActualTypeArguments()[0]).getRawType()))) { + return LinkedHashMapConstructor.getInstance(); + } else { + return LinkedTreeMapConstructor.getInstance(); + } + } + + return null; + } + + private ObjectConstructor newUnsafeAllocator(final Class rawType) { + if (mUseJdkUnsafe) { + return new ReflectSafeCreatorConstructor<>(rawType); + } else { + final String exceptionMessage = "Unable to create instance of " + rawType + "; usage of JDK Unsafe " + + "is disabled. Registering an InstanceCreator or a TypeAdapter for this type, adding a no-args " + + "constructor, or enabling usage of JDK Unsafe may fix this problem."; + return new ExceptionConstructor<>(exceptionMessage); + } + } + + @Override + public String toString() { + return mInstanceCreators.toString(); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/QueueConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/QueueConstructor.java new file mode 100644 index 0000000..8ac23cf --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/QueueConstructor.java @@ -0,0 +1,26 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.internal.ObjectConstructor; +import java.util.ArrayDeque; +import java.util.Queue; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/08/01 + * desc : Queue 创建器 + */ +public final class QueueConstructor implements ObjectConstructor> { + + private static final QueueConstructor INSTANCE = new QueueConstructor(); + + @SuppressWarnings("unchecked") + public static > T getInstance() { + return (T) INSTANCE; + } + + @Override + public Queue construct() { + return new ArrayDeque<>(); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/ReflectCreatorConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/ReflectCreatorConstructor.java new file mode 100644 index 0000000..c5a742b --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/ReflectCreatorConstructor.java @@ -0,0 +1,54 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.internal.ObjectConstructor; +import com.google.gson.internal.reflect.ReflectionHelper; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/08/01 + * desc : 反射创建器 + */ +public final class ReflectCreatorConstructor implements ObjectConstructor { + + private final KotlinDataClassDefaultValueConstructor mKotlinDataClassDefaultValueConstructor; + + private final Constructor mConstructor; + + public ReflectCreatorConstructor(Class rawType, Constructor constructor) { + mConstructor = constructor; + mKotlinDataClassDefaultValueConstructor = new KotlinDataClassDefaultValueConstructor<>(rawType); + } + + @SuppressWarnings("unchecked") + @Override + public T construct() { + T instance = mKotlinDataClassDefaultValueConstructor.construct(); + + if (instance != null) { + return instance; + } + + try { + instance = (T) mConstructor.newInstance(); + return instance; + } + // 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( + mConstructor) + "'" + + " with no args", e); + } catch (InvocationTargetException e) { + // don't wrap if cause is unchecked? + // JsonParseException ? + throw new RuntimeException("Failed to invoke constructor '" + ReflectionHelper.constructorToString( + mConstructor) + "'" + + " with no args", e.getCause()); + } catch (IllegalAccessException e) { + throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e); + } + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/ReflectSafeCreatorConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/ReflectSafeCreatorConstructor.java new file mode 100644 index 0000000..d05e5e2 --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/ReflectSafeCreatorConstructor.java @@ -0,0 +1,41 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.internal.ObjectConstructor; +import com.google.gson.internal.UnsafeAllocator; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/08/01 + * desc : 反射(安全)创建器 + */ +public final class ReflectSafeCreatorConstructor implements ObjectConstructor { + + private final KotlinDataClassDefaultValueConstructor mKotlinDataClassDefaultValueConstructor; + + private final Class mRawType; + + public ReflectSafeCreatorConstructor(Class rawType) { + mRawType = rawType; + mKotlinDataClassDefaultValueConstructor = new KotlinDataClassDefaultValueConstructor<>(rawType); + } + + @SuppressWarnings("unchecked") + @Override + public T construct() { + T instance = mKotlinDataClassDefaultValueConstructor.construct(); + + if (instance != null) { + return instance; + } + + try { + instance = (T) UnsafeAllocator.INSTANCE.newInstance(mRawType); + return instance; + } catch (Exception e) { + throw new RuntimeException(("Unable to create instance of " + mRawType + ". " + + "Registering an InstanceCreator or a TypeAdapter for this type, or adding a no-args " + + "constructor may fix this problem."), e); + } + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/SetConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/SetConstructor.java new file mode 100644 index 0000000..362d946 --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/SetConstructor.java @@ -0,0 +1,26 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.internal.ObjectConstructor; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/08/01 + * desc : Set 创建器 + */ +public final class SetConstructor implements ObjectConstructor> { + + private static final SetConstructor INSTANCE = new SetConstructor(); + + @SuppressWarnings("unchecked") + public static > T getInstance() { + return (T) INSTANCE; + } + + @Override + public Set construct() { + return new LinkedHashSet<>(); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/SortedMapConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/SortedMapConstructor.java new file mode 100644 index 0000000..de4ccd5 --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/SortedMapConstructor.java @@ -0,0 +1,26 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.internal.ObjectConstructor; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/08/01 + * desc : SortedMap 创建器 + */ +public final class SortedMapConstructor implements ObjectConstructor> { + + private static final SortedMapConstructor INSTANCE = new SortedMapConstructor(); + + @SuppressWarnings("unchecked") + public static > T getInstance() { + return (T) INSTANCE; + } + + @Override + public SortedMap construct() { + return new TreeMap<>(); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/constructor/SortedSetConstructor.java b/library/src/main/java/com/hjq/gson/factory/constructor/SortedSetConstructor.java new file mode 100644 index 0000000..35861d7 --- /dev/null +++ b/library/src/main/java/com/hjq/gson/factory/constructor/SortedSetConstructor.java @@ -0,0 +1,26 @@ +package com.hjq.gson.factory.constructor; + +import com.google.gson.internal.ObjectConstructor; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * author : Android 轮子哥 + * github : https://github.com/getActivity/GsonFactory + * time : 2023/08/01 + * desc : SortedSet 创建器 + */ +public final class SortedSetConstructor implements ObjectConstructor> { + + private static final SortedSetConstructor INSTANCE = new SortedSetConstructor(); + + @SuppressWarnings("unchecked") + public static > T getInstance() { + return (T) INSTANCE; + } + + @Override + public SortedSet construct() { + return new TreeSet<>(); + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hjq/gson/factory/element/CollectionTypeAdapterFactory.java b/library/src/main/java/com/hjq/gson/factory/element/CollectionTypeAdapterFactory.java index 49e717c..aad7772 100644 --- a/library/src/main/java/com/hjq/gson/factory/element/CollectionTypeAdapterFactory.java +++ b/library/src/main/java/com/hjq/gson/factory/element/CollectionTypeAdapterFactory.java @@ -4,10 +4,9 @@ import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.internal.$Gson$Types; -import com.google.gson.internal.ConstructorConstructor; import com.google.gson.internal.ObjectConstructor; import com.google.gson.reflect.TypeToken; - +import com.hjq.gson.factory.constructor.MainConstructor; import java.lang.reflect.GenericArrayType; import java.lang.reflect.Type; import java.util.Collection; @@ -20,10 +19,10 @@ */ public class CollectionTypeAdapterFactory implements TypeAdapterFactory { - private final ConstructorConstructor mConstructorConstructor; + private final MainConstructor mMainConstructor; - public CollectionTypeAdapterFactory(ConstructorConstructor constructor) { - mConstructorConstructor = constructor; + public CollectionTypeAdapterFactory(MainConstructor constructor) { + mMainConstructor = constructor; } @SuppressWarnings({"unchecked", "rawtypes"}) @@ -47,7 +46,7 @@ public TypeAdapter create(Gson gson, TypeToken typeToken) { Type elementType = $Gson$Types.getCollectionElementType(type, rawType); TypeAdapter elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType)); - ObjectConstructor constructor = mConstructorConstructor.get(typeToken); + ObjectConstructor constructor = mMainConstructor.get(typeToken); // create() doesn't define a type parameter CollectionTypeAdapter collectionTypeAdapter = diff --git a/library/src/main/java/com/hjq/gson/factory/element/MapTypeAdapterFactory.java b/library/src/main/java/com/hjq/gson/factory/element/MapTypeAdapterFactory.java index 87c3f07..b9d59da 100644 --- a/library/src/main/java/com/hjq/gson/factory/element/MapTypeAdapterFactory.java +++ b/library/src/main/java/com/hjq/gson/factory/element/MapTypeAdapterFactory.java @@ -4,11 +4,10 @@ import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.internal.$Gson$Types; -import com.google.gson.internal.ConstructorConstructor; import com.google.gson.internal.ObjectConstructor; import com.google.gson.internal.bind.TypeAdapters; import com.google.gson.reflect.TypeToken; - +import com.hjq.gson.factory.constructor.MainConstructor; import java.lang.reflect.Type; import java.util.Map; @@ -20,12 +19,12 @@ */ public class MapTypeAdapterFactory implements TypeAdapterFactory { - private final ConstructorConstructor mConstructorConstructor; + private final MainConstructor mMainConstructor; final boolean mComplexMapKeySerialization; - public MapTypeAdapterFactory(ConstructorConstructor constructorConstructor, + public MapTypeAdapterFactory(MainConstructor mainConstructor, boolean complexMapKeySerialization) { - mConstructorConstructor = constructorConstructor; + mMainConstructor = mainConstructor; mComplexMapKeySerialization = complexMapKeySerialization; } @@ -43,7 +42,7 @@ public TypeAdapter create(Gson gson, TypeToken typeToken) { Type[] keyAndValueTypes = $Gson$Types.getMapKeyAndValueTypes(type, rawTypeOfSrc); TypeAdapter keyAdapter = getKeyAdapter(gson, keyAndValueTypes[0]); TypeAdapter valueAdapter = gson.getAdapter(TypeToken.get(keyAndValueTypes[1])); - ObjectConstructor constructor = mConstructorConstructor.get(typeToken); + ObjectConstructor constructor = mMainConstructor.get(typeToken); // we don't define a type parameter for the key or value types MapTypeAdapter result = new MapTypeAdapter(gson, keyAndValueTypes[0], keyAdapter, diff --git a/library/src/main/java/com/hjq/gson/factory/element/ReflectiveTypeAdapterFactory.java b/library/src/main/java/com/hjq/gson/factory/element/ReflectiveTypeAdapterFactory.java index bb78fb8..6365793 100644 --- a/library/src/main/java/com/hjq/gson/factory/element/ReflectiveTypeAdapterFactory.java +++ b/library/src/main/java/com/hjq/gson/factory/element/ReflectiveTypeAdapterFactory.java @@ -6,10 +6,9 @@ import com.google.gson.TypeAdapterFactory; import com.google.gson.annotations.JsonAdapter; import com.google.gson.internal.$Gson$Types; -import com.google.gson.internal.ConstructorConstructor; import com.google.gson.internal.Excluder; import com.google.gson.reflect.TypeToken; - +import com.hjq.gson.factory.constructor.MainConstructor; import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; import java.lang.reflect.Type; @@ -26,13 +25,13 @@ */ public class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { - private final ConstructorConstructor mConstructorConstructor; + private final MainConstructor mMainConstructor; private final FieldNamingStrategy mFieldNamingPolicy; private final Excluder mExcluder; - public ReflectiveTypeAdapterFactory(ConstructorConstructor constructor, + public ReflectiveTypeAdapterFactory(MainConstructor constructor, FieldNamingStrategy strategy, Excluder excluder) { - mConstructorConstructor = constructor; + mMainConstructor = constructor; mFieldNamingPolicy = strategy; mExcluder = excluder; } @@ -73,7 +72,7 @@ public TypeAdapter create(Gson gson, final TypeToken type) { return null; } ReflectiveTypeAdapter reflectiveTypeAdapter = - new ReflectiveTypeAdapter<>(mConstructorConstructor.get(type), getBoundFields(gson, type, raw)); + new ReflectiveTypeAdapter<>(mMainConstructor.get(type), getBoundFields(gson, type, raw)); reflectiveTypeAdapter.setReflectiveType(type, null); return reflectiveTypeAdapter; } @@ -103,7 +102,7 @@ private Map getBoundFields(Gson gson, TypeToken // only serialize the default name serialize = false; } - ReflectiveFieldBound fieldBound = ReflectiveTypeUtils.createBoundField(gson, mConstructorConstructor, field, name, + ReflectiveFieldBound fieldBound = ReflectiveTypeUtils.createBoundField(gson, mMainConstructor, field, name, TypeToken.get(fieldType), serialize, deserialize); ReflectiveFieldBound replaced = result.put(name, fieldBound); if (previous == null) { diff --git a/library/src/main/java/com/hjq/gson/factory/element/ReflectiveTypeUtils.java b/library/src/main/java/com/hjq/gson/factory/element/ReflectiveTypeUtils.java index 10e1d27..a1950b1 100644 --- a/library/src/main/java/com/hjq/gson/factory/element/ReflectiveTypeUtils.java +++ b/library/src/main/java/com/hjq/gson/factory/element/ReflectiveTypeUtils.java @@ -6,10 +6,10 @@ import com.google.gson.TypeAdapterFactory; import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; -import com.google.gson.internal.ConstructorConstructor; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; +import com.hjq.gson.factory.constructor.MainConstructor; import java.io.IOException; import java.lang.reflect.Field; import java.math.BigDecimal; @@ -86,7 +86,7 @@ public static boolean containsClass(Class clazz) { return TYPE_TOKENS.contains(clazz); } - public static ReflectiveFieldBound createBoundField(final Gson gson, final ConstructorConstructor constructor, final Field field, final String fieldName, + public static ReflectiveFieldBound createBoundField(final Gson gson, final MainConstructor constructor, final Field field, final String fieldName, final TypeToken fieldType, boolean serialize, boolean deserialize) { return new ReflectiveFieldBound(fieldName, serialize, deserialize) { @@ -107,7 +107,12 @@ public void read(JsonReader reader, Object value) throws IOException, IllegalAcc if (fieldValue == null) { return; } - // 如果不为空,则直接赋值 + // 如果不为空,则直接赋值,之所以这样写的原因是 + // 当后台给某个字段赋值 null,例如 { "age" : null } 这种,也会走到这里来 + // 这样就会导致一个问题,当字段有一个默认值了,后台返回 null 会把默认值给覆盖掉 + // 如果是在 Kotlin 上面使用,问题会更加严重,明明在字段上面定义了默认值,并且声明了字段不为空 + // 如果后台不返回还好,就不会走到这里来,啥事没有,但是如果后台硬是返回了 null + // Gson 再反射设置进去,这个时候外层的人一旦使用这个字段,就很可能会触发 NullPointerException field.set(value, fieldValue); } @@ -123,7 +128,7 @@ public boolean writeField(Object value) throws IOException, IllegalAccessExcepti }; } - public static TypeAdapter getFieldAdapter(Gson gson, ConstructorConstructor constructor, Field field, TypeToken fieldType, String fieldName) { + public static TypeAdapter getFieldAdapter(Gson gson, MainConstructor constructor, Field field, TypeToken fieldType, String fieldName) { TypeAdapter adapter = null; JsonAdapter annotation = field.getAnnotation(JsonAdapter.class); if (annotation != null) { @@ -144,7 +149,7 @@ public static TypeAdapter getFieldAdapter(Gson gson, ConstructorConstructor c return adapter; } - public static TypeAdapter getTypeAdapter(ConstructorConstructor constructor, + public static TypeAdapter getTypeAdapter(MainConstructor constructor, Gson gson, TypeToken fieldType, JsonAdapter annotation) {